ELF 파일

ELF 파일

2025년 3월 15일

ref #

https://blog.k3170makan.com/2018/10/introduction-to-elf-format-part-vi.html

https://noise.getoto.net/2021/03/02/how-to-execute-an-object-file-part-1/

LLVM: ELF 파일 포맷


ELF (Executable and Linkable Format) #

unix like 운영체제에서 사용되는 표준 파일 포맷으로 실행 파일, 오브젝트파일, 공유라이브러리의 포맷이다.

kernel.elf 파일도 ELF 파일형식이며, 이전 bootloader 문서에서 발견된 버그도 kernel.elf 파일을 ELF 로 읽어야 하지만 메모리에 바로 쓰고 entrypoint로 점프하려 해서 발생했던 버그였다.

d7b2f767-5aea-47ab-bdc4-e8b77acb250d


ELF Header #

 1typedef struct {
 2    unsigned char e_ident[16];  // ELF 식별 정보 (매직 넘버, 클래스, 엔디언 등)
 3    uint16_t e_type;            // 파일 타입 (실행 파일, 공유 라이브러리 등)
 4    uint16_t e_machine;         // 대상 아키텍처 (x86_64: 0x3E)
 5    uint32_t e_version;         // ELF 버전 (보통 1)
 6    uint64_t e_entry;           // 엔트리 포인트 가상 주소 (프로그램 시작 주소)
 7    uint64_t e_phoff;           // 프로그램 헤더 테이블 오프셋
 8    uint64_t e_shoff;           // 섹션 헤더 테이블 오프셋
 9    uint32_t e_flags;           // 프로세서 별 플래그
10    uint16_t e_ehsize;          // ELF 헤더 크기 (64-bit에서는 일반적으로 64바이트)
11    uint16_t e_phentsize;       // 프로그램 헤더 엔트리 크기
12    uint16_t e_phnum;           // 프로그램 헤더 엔트리 개수
13    uint16_t e_shentsize;       // 섹션 헤더 엔트리 크기
14    uint16_t e_shnum;           // 섹션 헤더 엔트리 개수
15    uint16_t e_shstrndx;        // 섹션 헤더 문자열 테이블의 인덱스
16} Elf64_Ehdr;

e261adbd-73b0-4e4c-a083-1c10a50cd82c

프로그램 헤더와 섹션 헤더의 파일 오프셋과 크기를 알 수 있다.


kernel.elf 의 구조 #

 1+---------------------------+   <- 0x0
 2| ELF Header (64byte)       |
 3+---------------------------+   <- 0x40
 4| Program Header 1 (56byte) |
 5|       ...                 |   (224 byte)
 6| Program Header 4          |
 7+---------------------------+   <- 0x120
 8| section 1                 |
 9|       ...                 |   (3352 byte)
10| section 16                |
11+---------------------------+   <- 0xE38
12| Section Header 1 (64byte) |
13|       ...                 |   (1024 byte)
14| Section Header 16         |
15+---------------------------+   <- 0x1238
16total: 4664 byte

Section Header String Index #

.shstrtab 섹션의 인덱스 번호가 저장되어 있으며, .shstrtab 에는 ELF 파일의 각 섹션의 이름이 문자열 테이블 형태로 저장된다.

8f77aa57-0068-4453-a86c-0537c33fd00f


Program Header #

실행파일이 실행될 때 로더가 어떤 섹션을 어떤 메모리 주소에 로드해야 하는지, 어떤 권한을 줘야하는지 등이 저장되어 있다.

다시말하면 프로그램이 실행되면 Program Header에 담긴 섹션만 메모리에 로드된다는 의미이다.

 1typedef struct {
 2    uint32_t   p_type;      // 세그먼트 타입
 3    uint32_t   p_flags;     // 권한(R/W/X)
 4    uint64_t   p_offset;    // 파일 내 오프셋
 5    uint64_t   p_vaddr;     // 가상 메모리 주소
 6    uint64_t   p_paddr;     // 물리 메모리 주소
 7    uint64_t   p_filesz;    // 파일에서 세그먼트 크기
 8    uint64_t   p_memsz;     // 메모리에서 세그먼트 크기
 9    uint64_t   p_align;     // 메모리 정렬
10} Elf64_Phdr;

세그먼트 타입 #

  • LOAD (0x1): 실행 시 메모리에 로드될 코드/데이터 영역에 대한 정보를 알려준다. 파일 오프셋과 사이즈만큼 메모리 주소에 로드(mmap)한다.
    • 실제로 확인해보면 다른 타입들은 LOAD 타입의 영역 안에 있기 때문에 로더는 LOAD 타입만 확인해서 메모리에 로드해주면 된다.
    • 다른 타입들은 특별한 역할을 맡기 때문에 프로그램 헤더의 엔트리로 존재하는 것이다.
  • DYNAMIC (0x2): .dynamic 섹션을 메모리에 로드할때 사용되는 정보이며 동적 링킹 정보가 포함된다.
    • readelf -d 로 확인할 수 있는 내용들이 담겨져 있다. 055bee78-ba31-42a9-8688-dc23eaacc517
    • tag - val/ptr 한 쌍으로 구성되며 필요한 라이브러리(NEEDED tag) 이름 주소(.dynstr 섹션)를 담고 있거나, .got.plt(PLTGOT tag) 섹션의 주소를 담는 등 동적링커가 필요로 하는 정보의 주소 엔트리가 저장되는 곳에 대한 메모리 주소이다.
  • INTERP (0x3): ELF 로더 (동적링커) 경로 문자열이 저장된 위치(.interp 섹션)를 의미한다. 이 문자열이 있다면 프로그램 내에서 .so 의 함수를 사용한다는 의미하며, 없다면 단독으로 실행 가능한 프로그램이라는 뜻이다.
    • 프로그램 헤더에서 확인한 오프셋 위치를 확인해보면 동적링커의 문자열이 포함되어 있다. 9c9fd92e-986c-40fa-9782-9963871cd86f
    • 동적 링커의 필요 여부는 ldd로도 확인할 수 있다.
      1kdh@DESKTOP-MHEA7GE:~/min-os/kernel$ ldd ./a.out
      2  linux-vdso.so.1 (0x00007fffa7495000)
      3  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8b4e864000)
      4  /lib64/ld-linux-x86-64.so.2 (0x00007f8b4ea9b000)
      
    • 컴파일 시 사용하는 ld (링커) 와 런타임에 GOT에 점프주소를 찾아주는 ld*.so (동적링커)는 완전히 다르다.
  • NOTE (0x4): GNU 빌드 ID, ABI 정보 등 메타데이터가 저장된다.
    • read -n 으로 확인할 수 있다. 0ec08b5b-7de1-414d-a265-8dbbdc23d328
  • PHDR (0x6): 프로그램 헤더 테이블 타입이다. kernel.elf 를 예로 보면 프로그램 헤더 테이블이 파일 오프셋이 0x40 으로 ELF 헤더 다음에 위치한 것을 알 수 있으며 메모리상에는 이미지베이스를 더한 값에 로드된다.
    • 레퍼런스 글 에서 바이너리 파일을 수정해서 PHDR 다음 헤더부터 가리키게 오프셋을 수정했는데, 제대로 동작하는 것을 볼 수 있었다.
    • 두번째 프로그램 헤더 로드주소와 겹쳐있는걸 볼 수 있는데 ELF Header + Program Header + .rodata 영역이기 때문이다.
  • TLS (0x7): 스레드별 독립적인 데이터를 저장하는 영역을 말한다. 일부 컴파일 방법의 경우 이 영역이 전역변수처럼 사용되어 data나 bss 처럼 배치될수도 있다.
  • GNU_STACK (0x6474e551): 스택을 NX(Non-Executable)로 설정할지 정하는 헤더이다. 이 헤더를 삭제하면 리눅스 버전마다 기본값으로 실행되고, 이 헤더에 RWX 권한을 세팅하면 NX 비트가 해제된다.
  • GNU_RELRO (0x6474e552): 이것도 GNU_STACK 처럼 RELRO 설정 영역을 프로그램 헤더로 지정한 것이다.
    • 초기에 쓰기 권한으로 시작해서 수정하고 더이상의 수정이 필요 없는 메모리 영역 (.plt.got 등)은 보안을 위해 mprotect로 ReadOnly 가 된다. got overwrite 등을 막기 위해 사용됨

kernel.elf #

내용을 확인해보면 파일의 오프셋과 로드되는 가상메모리 주소가 다른데, 자세히 보면 커널 빌드 시 지정한 image base 의 문제만은 아닌 것을 알 수 있다.

.text 섹션은 file offset이 0x1b0 이고, image base 는 0x100000인데, 메모리 로드 주소는 0x1011b0 으로 0x1000 바이트만큼 차이가 난다.

이 0x1000 바이트는 Align 값이며 메모리에 로드될때 최소 정렬 기준이기 때문에 파일 그대로 메모리에 복사하면 entry_point를 찾지 못하는 문제가 발생했던 것이다.

e4537cac-1070-407a-9d18-3eb6f308e977

ef02b338-256c-4c0f-9281-f7adc43dd3cb


Section Header #

ELF 헤더에서 섹션헤더의 시작 오프셋, 수, 크기(x64=64byte)를 알 수 있으며, 각 섹션헤더는 섹션에 대한 정보가 저장된다.

 1typedef struct {
 2    uint32_t sh_name;         // shstrtab 섹션에서의 현재 섹션이름 오프셋
 3    uint32_t sh_type;         // 섹션의 타입
 4    uint64_t sh_flags;        // 섹션의 RWX 권한
 5    uint64_t sh_addr;         // 메모리에 로드될 가상주소
 6    uint64_t sh_offset;       // 
 7    uint64_t sh_size;
 8    uint32_t sh_link;
 9    uint32_t sh_info;
10    uint64_t sh_addralign;
11    uint64_t sh_entsize;
12} Elf64_Shdr;

sh_name #

섹션 이름이 저장된 shstrtab 섹션에서의 오프셋이 담겨있다.

6a02e7fd-5ee3-434e-80cc-d61634dbe1a3


sh_type #

  • NULL(0): 사용되지 않음. 섹션 헤더의 0번째 인덱스는 무조건 NULL 타입이다.
  • PROGBITS(1): 일반적인 데이터나 코드를 담는 섹션이다. .text, .data, .rodata 등의 섹션이 해당된다.
  • SYMTAB(2): 정적 심볼 테이블 섹션. 심볼에 대한 정보가 담겨있고 이름은 스트링 테이블 .strtab을 참조한다. 필수가 아니기 때문에 strip 할때 제거된다.
    1struct Elf64_Sym {
    2    uint32_t st_name;        // 각 심볼 스트링 테이블(STRTAB)의 오프셋이 담겨있다. 
    3    unsigned char st_info;   // 스코프를 지정. LOCAL(static), GLOBAL, WEAK(override). 이 정보로 심볼이 충돌났는지 검사 가능
    4    unsigned char st_other;  // 심볼 가시성(접근 스코프)에 대한 정보. __attribute__((visibility(...)) 로 임의지정할 수 있다. 
    5    uint64_t st_shndx;       // 해당 심볼이 포함된 섹션의 인덱스
    6    uint64_t st_value;       // 심볼의 주소. (함수의 시작주소, 변수의 메모리주소, 섹션심볼이면 섹션 시작주소 등)
    7    uint64_t st_size;        // 심볼의 크기. (함수의 코드 바이트 길이, 변수의 데이터 사이즈 등)
    8}
    
    ecf6447e-d6db-4d0f-a929-be3bad4e37ce
  • STRTAB(3): 문자열 테이블 섹션 타입. .shstrtab, .strtab
  • RELA(4), REL(9): 재배치 오프셋 정보를 담는 섹션.
  • NOTE(7): build ID, core dump info, OS, ABI 등 메타데이터 정보
  • NOBITS(8): 파일에는 없지만 실행되면 메모리 공간이 할당되는 섹션. .bss (초기화되지 않은 전역/정적변수)
  • DYNAMIC(11): 동적 심볼 테이블 섹션. Elf64_Sym 구조체를 동일하게 사용하지만, strip 되지 않는 영역이다.
  • INIT_ARRAY(14), FINI_ARRAY(15): 프로그램 시작/종료 시 자동으로 호출되는 함수에 대한 포인터배열이 담겨있다.


Section #


링커뷰 #


메모리 로드 #

comments powered by Disqus