우리는 더블 클릭을 하여 프로그램을 실행시킵니다. 프로그램은 .exe로 구성되어 있는데, 이 file에 어떤 정보가 담겨 있어야 프로그램을 실행 시킬 수 있을까요?
Program은 특정 file format은 따릅니다.
우리는 Program 예시로 아래 test.c를 사용할 것입니다.
#include <stdio.h>
int big_big_array[10 * 1024 * 1024];
char *a_string = "Hello, World!";
int a_var_with_value = 100;
int main(void) {
big_big_array[0] = 100;
printf("%s\n", a_string);
a_var_with_value += 20;
printf("main is : %p\n", &main);
return 0;
}
ELF File은 ELF Header, Program header table, Section header table를 담고 있습니다.
먼저 ELF Header에는 호환성 정보로 실행 가능한 코드의 Entry point가 담겨 있고, Program header table에는 프로그램 load와 실행을 위한 파일의 모든 segment들이 담겨 있습니다. Section header table에는 linker단계에서 section 단위로 code, data를 다룰 때 사용합니다.
typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine; // ISA 실행 코드
Elf32_Word e_version;
Elf32_Addr e_entry; // 실행 가능한 코드의 Entry point
Elf32_Off e_phoff; // Program header offset
Elf32_Off e_shoff; // Section header offset
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum; // Program header
Elf32_Half e_shentsize;
Elf32_Half e_shnum; // Section header
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
C로 작성된 ELF Header Format입니다. ISA 실행 코드, Program, Section header offset, program 수와 section 수 등과 같은 정보를 구조체로 정의하여 Program 수만큼 loop를 돌면서 각 program file 별 정보들을 Memory에 load할 수 있게 됩니다. Elf32_Addr e_entry;에 저장된 주소는 memory의 주소가 되는데, 이때 EIP(Instruction pointer)를 Elf32_Addr e_entry;값으로 초기화 시키면 해당 주소부터 프로그램을 실행 시킵니다.
linux환경에서 test.c를 컴파일하여 readelf 프로그램을 통해 header option을 줘서 실행시킨 결과입니다.
C로 작성된 code는 무조건 main함수부터 시작하여 프로그램의 시작점을 나타내는 entry point와 main의 주소가 같을거라고 예상 했지만, readelf 프로그램을 통해 test.c의 entry point를 출력하고, test.c를 실행했을 때 main함수의 주소가 서로 다른 것을 확인할 수 있습니다.
objdump --disassemble -M intel ./test명령을 수행했을 때 위에서 Entry point의 주소가 main이 아닌 start로 되어 있는 것을 확인 할 수 있습니다. 우리가 작성하진 않았지만 main 함수를 무조건 호출하는 코드가 담겨 있는 주소입니다. main함수도 argument가 필요하고, stack에 대한 처리, argument 설정 등과 같은 과정을 main 함수가 할 수 없고 개발하는 사람들이 고려하지 않기 때문에 start가 이 과정을 main() 실행 전과 후에 모두 처리 해줍니다.
Section은 code 또는 data가 들어 있는 하나의 덩어리입니다. 그리고 Segment는 Section들의 집합입니다. Segment 안에 하나의 section 또는 다수의 section이 담겨 있습니다. Segment는 과거에 가상 메모리 관리를 위해 사용하던건데 현재는 Segment 대신 Paging을 사용합니다.
Section의 종류는 아래와 같이 나눕니다.
– .text : 실행 가능한 코드
– .bss : 전역 변수 선언시 초기화 하지 않았을 때 들어감 (0으로 초기화)
– .data, .rodata(read only)
– .strtab : 함수, 변수 이름
– .symtab : Debug symbols
Segment는 logical하게 section을 묶는 단위기 때문에 사실 편의대로 해도 되지만 보통 초기화 데이터와 관련된 section, debug 정보를 담고 있는 section 등 연관 된 section들끼리 묶습니다.
Segment를 메모리에 올릴 때 read only냐 write가능하냐 executable이냐와 같은 커미션도 같이 올리기 때문에 code와 data를 같이 묶어 버리면 커미션 수행시 문제가 발생할 수 있기 때문입니다.
Program에 ELF header에 .text, .data와 같은 section들이 저장이 되어 있고, Segment들이 각각을 파싱하면 파일의 구조를 알 수 있게 되고, 각각을 memory 주소 공간에 올리게 됩니다. 다른 section들과 달리 .bss는 read만 하고, 크기만큼 메모리 주소 공간을 할당하게 됩니다. 그 후 stack과 heap을 생성하여 EIP (Instruction Pointer)와 ESP를 위치하여 프로그램을 실행 시킵니다.
실제로 Program을 돌릴 때 단일 process를 사용하는 경우는 없지만 다수의 process를 사용하는 것은 가상 메모리에서 보도록 하고 여기서는 단일 process만 보도록 하겠습니다.
생성된 stack은 함수 호출 시 사용하고, heap은 동적 메모리 할당 공간입니다. 이때 stack과 heap이 만나면 해당 process가 사용할 수 있는 공간을 다 썼다는 의미이고, process가죽게 됩니다.
정리를 해보면 .text, .data, .rodata가 프로그램에 저장되는 부분이고, stack과 heap은 process가 실행될 때만 읽어오는 부분입니다.