-
x86-64 Assembly Language 3 Hello world와 메모리 구조Linux Development/Kernel 2021. 7. 23. 00:21
1. Hello world in 32bit Architecture
이전 설명에서 본 nasm 코드를 다시 보자.
코드의 내용 중 "section. text"라는 라인과 "section. data"라는 라인이 보인다. 이는 32bit 아키텍처 cpu가 제공해주는
4GB의 가상 메모리 주소 공간에 "코드 세그먼트"와 "데이터 세그먼트"에 배치될 내용이라는 걸 의미한다.
4GB의 가상 메모리 주소 공간
- 보통 메인 메모리(RAM)는 1바이트(8비트) 단위로 내용을 기록한다.
- 이때 우리는 메모리 주소로 2비트를 사용할 수 있다고 가정해보자. 00, 01, 11, 10의 네 가지 표현이 가능하고 따라서 최대 4바이트의 물리 주소를 맵핑할 수 있다.
- 32bit 아키텍처에서는 그 레지스터들도 32비트고, 내부 버스(비트 정보들이 이동하는 회로 라인)도 32가닥이다. 모든 것들이 32bit의 정보를 동시에 처리하기 쉽게 만들어 놨다. 따라서 메모리 주소도 32bit로 표현한다.
- 32비트를 사용해서 주소를 표현하면 2의 32승만큼 주소 표현이 가능하다.
- 2의 32승 바이트는 정확히 4GB이다.
- 오른쪽에 보이는 그림은 32bit 환경에서 커널이 각각의 프로세스에게 할당해주는 4GB만큼의 주소 값들이다.
- 실제 물리 메모리와 통째로 맵핑되는 kernel Logical Address를 제외한 그 밖의 주소 값들은 그때그때 필요할 때마다 필요한 위치에 물리 메모리가 맵핑된다. 이것을 '커널이 페이징(Paging) 해준다'라고 한다.
- 유저 프로그램이 실제로 사용하는 주소 값들은 0x00000000부터 시작해서 0xC0000000까지의 3GB의 주소 값들이다
애플리케이션에 할당된 3GB의 주소 공간은 보통 위와 같은 구조를 띄게 된다.
앞의 설명에서 소위 '메모리 덩어리'라고 표현했던 각 구역은 '세그먼트'라고 불리며 코드 세그먼트, 데이터 세그먼트, BSS세그먼트, 힙 영역, 스택 영역으로 불린다.
Code Segment (text)
- 위의 Hello World code에서 "section .text" 항목의 실제 실행 코드들이 이 세그먼트에 들어간다.
- 앞에서 설명한 레지스터 중 코드 레지스터(CS)가 이곳의 주소를 가리킨다.
Data Segment (data)
- 데이터들은 보통 그 종류와 용도에 따라서 4가지 영역 중 한 곳으로 저장되는 경우가 많은데 이 데이터 세그먼트에는 소위 전역 변수들, 그중에서 초기 값이 이미 설정되어 있는 전역 변수들이 들어간다.
#include <stdio.h>
int loop_count = 20; // Data Segment 행
void main()
{
static short check_flag = 5; // static 이라 전역변수 역할을 함, Data Segment 행
}- 위의 Hello World code에서 "section .data" 항목이 데이터 세그먼트로 들어간다.
- 앞에서 설명한 레지스터 중 데이터 레지스터(DS)가 이곳의 주소를 가리킨다.
- 참고로 C 코드도 컴파일할 때 어셈블러 코드로 번역된 이후 기계어 코드가 된다.
BSS Segment (bss)
- 전역 변수들 중 초기값이 없거나 0으로 설정된 변수들이 들어간다.
- 위의 예제 코드에서 변수들이, 대입 문 "= 20"이 없거나, 0, NULL 등으로 설정돼 있으면 BSS세그먼트로 들어간다.
- 참고로 이런 전역 변수들 말고, 상수들은( #define MAX_COUNT 100 ) 보통 컴파일 전에 이 코드가 쓰이는 모든 코드 라인상에 하드 코딩돼버린다. 따라서 text 영역 안에 있다고 보면 된다.
Heap Area
- C 언어를 했다면 포인터와 함께 쓰이는 malloc() 함수, calloc() 함수를 이미 알 것이다. 이런 동적 할당을 위한 공간이다.
- 동적 메모리 할당이 많을수록, 힙 영역은 커지며 더 많은 물리 메모리를 할당받게 된다.
- 힙을 위한 레지스터는 따로 지원되지 않는다. 그냥 OS가 알아서 heap의 주소를 적절히 저장하거나, 아주 가끔 어셈블러로 개발을 할 때 필요한 heap의 주소를 우리가 주의 깊게 정하기도 한다.
- 힙을 사용함에 있어서는 OS가 주도적인 역할을 한다. 기계가 제공할 수 있는 인터페이스의 한계보다, 더 복잡하고 유연하게 처리해 줘야 할 필요가 있어서 하위 레이어(CPU)보단 상위 레이어(OS)가 그 역할을 해줄 수밖에 없다.
Stack Area
- 앞에서 설명한 레지스터 중 SS가 이 영역을 가리킨다.
- C를 포함한 모든 프로그래밍 언어에서는 함수 안에서 쓰이는 지역 변수들은 그 함수가 끝나는 시점에 증발한다.
- 만약 함수가 다른 함수(서브루틴)를 불렀다면, 원래 함수의 지역변수들은 서브루틴이 끝날 때까지 어디다 잘 모셔놔야 한다.
- 보통 프로그램은, 어떤 함수가 서브루틴을 부르고.. 그 서브루틴이 또 다른 서브루틴을 부르며, 그 서브루틴이 끝나면, 다음 서브루틴을 부르는 Call과 return 과정의 연속이다. 따라서 다양한 지역 변수들이 매우 빠르게 정의되고 사라지고를 반복한다.
- 이런 지역변수에 대한 특성이 '스택'이라는 자료 구조와 매우 잘 맞는다.
Stack 자료구조
- cpu 안에는 프로그램의 모든 변수들을 동시에 가리키고 있을 만큼 충분한 레지스터가 없다.
- 그래서 데이터가 입력된 순서대로 차곡차곡 쌓아놓고 가장 최근에 들어온 자료를 하나의 레지스터(ESP)가 가리키고 있다, 따라서 밑에 쌓여있는 데이터들보다 위에 놓여있는, 가장 최근에 들어온 데이터가 다루기가 훨씬 편하다.
- 맨 위에 있는 데이터가 다쓰이고 사라지고 나면(pop 되면) 그전에 들어온 데이터가 top에 있게 되며 가장 쓰기 편한 상태가 된다.(포인터는 빠져나간 데이터 크기만큼 주소 값을 다시 내린다.)
- 이런 스택 구조를 지원해주기 위해 CPU 개발 그룹에선 스택용 레지스터들을 (ESP, EBP, SS) 제공해 준다.
- Linux나 Windows에서의 스택은 높은 주소에서 낮은 주소 방향으로 쌓인다고 생각하면 된다.
thread_struct 정보
- x86-64 아키텍처는 다른 서버급 cpu들에 비해 레지스터가 상대적으로 부족하다.
- 다른 서버급 아키텍처에서의 리눅스는 현재 실행 중인 프로세스 자체에 대한 정보를 구조체로 묶어서 저장해 두고 그것을 하나의 레지스터로 가리킨다.
- x86-64 위에서의 리눅스는 범용 레지스터인 스택 포인터(ESP in 32bit, RSP in 64bit)를 활용해서 프로세스 정보를 저장하고 접근하는데 이때 종종 아래와 같은 코드를 쓴다.
movl $-8192, %eax
andl %esp, %eax- $를 붙여서 상수 값으로 쓰인 숫자 -8192는 2의 보수로 표현되므로 0xffff ffff ffff E000이다.
- 이 수를 2진수 그대로 표현하면 "1111...1111111110000..000" 이 될 것이다. (E는 1000이다)
- 이 수를 EAX에 넣는다.
- 현재 스택의 최신 데이터를 가리키는 데 쓰이는 ESP는 범용 레지스터라 우리가 마음대로 읽고 쓸 수 있다.
- 프로그램이 제대로 실행되고 있다면 이 레지스터는 분명 스택 영역 어딘가를 가리키고 있을 것이다.
- 이 주소 값을 -8192 값과 앤드 연산하면 주소의 뒤쪽이 모두 0이 되면서 상대적으로 작은 어떤 주소가 된다.
- 프로그램이 스택을 얼마 사용하지 않았다면 아마 8 Kbyte정도 밑으로(매우 넉넉하게) 내려간 어떤 주소가 될 것이다.
- 스택은 위에서 밑으로 데이터가 쌓여가기 때문에 맨 밑에는 아무것도 없다.
- 이 and 연산 값을 EAX에 다시 넣고 프로세스 정보를 담은 구조체중 하나인 thread_struct를 이 위치에다 읽고 쓰고 한다.
- 사실 OS가 하나의 프로그램을 로드하는 과정에서, 가상 주소 공간에다 실제 물리 주소를 할당할 때 보통 4 Kbyte단위로 할당한다.
- 만약 스택영역에 두 개의 메모리 단위(page, 페이지)를 할당받았고 추가 메모리 할당이 없는 경우 가장 밑바닥에다 현재 프로세스 정보를 담아두는 것이다.
2. Hello world in 64bit Architecture
- 64bit 구조에선 레지스터도 64bit, cpu의 내부 버스도 64가닥이기 때문에 최대 2의 64승만큼의 주소 값들을 확보할 수 있다.
- 이는 너무 많다. 4GB 스택의 2의 32 승배, 일단 백만 TB정도는 가볍게 넘는다.
- 개발자들은 한동안은 이렇게 넓은 영역이 필요 없다고 판단했다.
- 이 주소 영역 모두를 각 프로그램들이 자기 마음대로 쓰면 오버해드(운영에 필요한 노동력, 비용)가 너무 크기도 하다.
- 그래서 주소 공간의 위아래만 조금씩 잘라서 쓰기로 했다.
- 4GB 주소 공간과 다른 한 가지는, 유저 영역과 커널 영역의 크기가 1:1이라는 것, 이는 Windows와 같다.
- 앞으로 주소 공간을 확장시킨다 해도 이 비율은 유지될 것으로 보인다.
- 따라서 주소 공간을 이진수로 표현했을 경우 맨 앞 비트(MSB)가 1이면 커널 영역이며 0이면 유저 영역이다.
- 이렇게 조금만 사용하는데도 4GB의 65536배에 이른다.
- 메모리 영역 안에서의 세그먼트들이나, 스택, 힙 등은 cpu구조가 비슷하기 때문에 크게 다르지 않다.
pmap
64bit Hellw World가 사용하는 공간을 pmap명령어로 확인해보자. 이를 위해 일단 Hello World가 실행 상태로 있어야 하기 때문에 C를 이용해서 다시 만들었고, 돌아가는 상태로 둬야 하기 때문에 printf문 바로 뒤에 getchar() 함수를 이용해서 키보드 입력 대기 상태로 두었다.
사용하는 주소 영역들이 모두 위쪽 그림상의 메모리 스택 범주 안에 있는 것이 보인다.