ELF 파일

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
d7b2f767-5aea-47ab-bdc4-e8b77acb250d

ELF Header

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

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

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

kernel.elf 의 구조

+---------------------------+   <- 0x0
| ELF Header (64byte)       |
+---------------------------+   <- 0x40
| Program Header 1 (56byte) |
|       ...                 |   (224 byte)
| Program Header 4          |
+---------------------------+   <- 0x120
| section 1                 |
|       ...                 |   (3352 byte)
| section 16                |
+---------------------------+   <- 0xE38
| Section Header 1 (64byte) |
|       ...                 |   (1024 byte)
| Section Header 16         |
+---------------------------+   <- 0x1238
total: 4664 byte

Section Header String Index

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

8f77aa57-0068-4453-a86c-0537c33fd00f
8f77aa57-0068-4453-a86c-0537c33fd00f

Program Header

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

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

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

세그먼트 타입

  • LOAD (0x1): 실행 시 메모리에 로드될 코드/데이터 영역에 대한 정보를 알려준다. 파일 오프셋과 사이즈만큼 메모리 주소에 로드(mmap)한다.
    • 실제로 확인해보면 다른 타입들은 LOAD 타입의 영역 안에 있기 때문에 로더는 LOAD 타입만 확인해서 메모리에 로드해주면 된다.
    • 다른 타입들은 특별한 역할을 맡기 때문에 프로그램 헤더의 엔트리로 존재하는 것이다.
  • DYNAMIC (0x2): .dynamic 섹션을 메모리에 로드할때 사용되는 정보이며 동적 링킹 정보가 포함된다.
    • readelf -d 로 확인할 수 있는 내용들이 담겨져 있다.
      055bee78-ba31-42a9-8688-dc23eaacc517
      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
      9c9fd92e-986c-40fa-9782-9963871cd86f
    • 동적 링커의 필요 여부는 ldd로도 확인할 수 있다.
      kdh@DESKTOP-MHEA7GE:~/min-os/kernel$ ldd ./a.out
        linux-vdso.so.1 (0x00007fffa7495000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8b4e864000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f8b4ea9b000)
      
    • 컴파일 시 사용하는 ld (링커) 와 런타임에 GOT에 점프주소를 찾아주는 ld*.so (동적링커)는 완전히 다르다.
  • NOTE (0x4): GNU 빌드 ID, ABI 정보 등 메타데이터가 저장된다.
    • read -n 으로 확인할 수 있다.
      0ec08b5b-7de1-414d-a265-8dbbdc23d328
      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
e4537cac-1070-407a-9d18-3eb6f308e977

ef02b338-256c-4c0f-9281-f7adc43dd3cb
ef02b338-256c-4c0f-9281-f7adc43dd3cb

Section Header

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

typedef struct {
    uint32_t sh_name;         // shstrtab 섹션에서의 현재 섹션이름 오프셋
    uint32_t sh_type;         // 섹션의 타입
    uint64_t sh_flags;        // 섹션의 RWX 권한
    uint64_t sh_addr;         // 메모리에 로드될 가상주소
    uint64_t sh_offset;       // 
    uint64_t sh_size;
    uint32_t sh_link;
    uint32_t sh_info;
    uint64_t sh_addralign;
    uint64_t sh_entsize;
} Elf64_Shdr;

sh_name

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

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

sh_type

  • NULL(0): 사용되지 않음. 섹션 헤더의 0번째 인덱스는 무조건 NULL 타입이다.
  • PROGBITS(1): 일반적인 데이터나 코드를 담는 섹션이다. .text, .data, .rodata 등의 섹션이 해당된다.
  • SYMTAB(2): 정적 심볼 테이블 섹션. 심볼에 대한 정보가 담겨있고 이름은 스트링 테이블 .strtab을 참조한다. 필수가 아니기 때문에 strip 할때 제거된다.
    struct Elf64_Sym {
        uint32_t st_name;        // 각 심볼 스트링 테이블(STRTAB)의 오프셋이 담겨있다. 
        unsigned char st_info;   // 스코프를 지정. LOCAL(static), GLOBAL, WEAK(override). 이 정보로 심볼이 충돌났는지 검사 가능
        unsigned char st_other;  // 심볼 가시성(접근 스코프)에 대한 정보. __attribute__((visibility(...)) 로 임의지정할 수 있다. 
        uint64_t st_shndx;       // 해당 심볼이 포함된 섹션의 인덱스
        uint64_t st_value;       // 심볼의 주소. (함수의 시작주소, 변수의 메모리주소, 섹션심볼이면 섹션 시작주소 등)
        uint64_t st_size;        // 심볼의 크기. (함수의 코드 바이트 길이, 변수의 데이터 사이즈 등)
    }
    
    ecf6447e-d6db-4d0f-a929-be3bad4e37ce
    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

링커뷰

메모리 로드

ESC
Type to search...