Skip to content

ELF

  • ELF는 Executable and Linking Format의 약어로, UNIX / LINUX 기반에서 사용되는 실행 및 링킹 파일 포맷이다.

  • 사용하는 운영체제는 유닉스, BSD, 솔라리스, 리눅스가 있다. (같은 바이너리 포멧이어도 각 운영체제간에는 호환이 되지 않는다.)

  • ELF 파일은 아래와 같은 구조를 가지고 있다.

    구조
    ELF Header
    Program header table
    .text
    .rodata
    .data
    Section header table

ELF header

  • ELF 헤더는 파일의 시작 부분에 있고, 파일에 대한 메타데이터를 가지고 있다.

  • ELF 헤더에는 프로세서 아키텍처에 대한 정보가 포함되어있어 (32bit or 64bit, little-endian or big-endian 등) 다양한 프로세서 아키텍처가 ELF 파일을 해석할 수 있도록 도와준다.

  • /usr/include/elf.hElf64_Ehdr 정의

    typedef struct {
    unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
    Elf64_Half e_type; /* Object file type */
    Elf64_Half e_machine; /* Architecture */
    Elf64_Word e_version; /* Object file version */
    Elf64_Addr e_entry; /* Entry point virtual address */
    Elf64_Off e_phoff; /* Program header table file offset */
    Elf64_Off e_shoff; /* Section header table file offset */
    Elf64_Word e_flags; /* Processor-specific flags */
    Elf64_Half e_ehsize; /* ELF header size in bytes */
    Elf64_Half e_phentsize; /* Program header table entry size */
    Elf64_Half e_phnum; /* Program header table entry count */
    Elf64_Half e_shentsize; /* Section header table entry size */
    Elf64_Half e_shnum; /* Section header table entry count */
    Elf64_Half e_shstrndx; /* Section header string table index */
    } Elf64_Ehdr;
  • readelf 명령어를 사용해 특정 프로그램의 ELF 헤더를 확인할 수 있다.

Terminal window
$ readelf -h /bin/ls
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x6180
Start of program headers: 64 (bytes into file)
Start of section headers: 145256 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 11
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29

Section Headers

  • ELF 바이너리의 코드와 데이터는 섹션(section)으로 나누어져 있다.

  • 섹션의 구조는 각 섹션의 내용이 구성된 방식에 따라 다른데, 각 섹션 헤더(section header)에서 그 속성을 찾을 수 있다. 바이너리 내부의 모든 섹션에 대한 헤더 정보는 섹션 헤더 테이블(section header table)에서 찾을 수 있다.

  • 섹션은 링커가 바이너리를 해석할 때 편리한 단위로 나눈 것이다.

  • 링킹이 수행되지 않은 경우에는 섹션 헤더 테이블이 필요하지 않다. 만약 섹션 헤더 테이블 정보가 없다면 e_shoff 필드는 0이다.

  • 바이너리를 실행할 때 바이너리 내부의 코드와 데이터를 세그먼트(segment)라는 논리적인 영역으로 구분한다.

  • 세그먼트는 링크 시 사용되는 섹션과는 달리 실행 시점에 사용된다.

  • /usr/include/elf.hElf64_Shdr 구조체 정의

    typedef struct {
    Elf64_Word sh_name; /* Section name (string tbl index) */
    Elf64_Word sh_type; /* Section type */
    Elf64_Xword sh_flags; /* Section flags */
    Elf64_Addr sh_addr; /* Section virtual addr at execution */
    Elf64_Off sh_offset; /* Section file offset */
    Elf64_Xword sh_size; /* Section size in bytes */
    Elf64_Word sh_link; /* Link to another section */
    Elf64_Word sh_info; /* Additional section information */
    Elf64_Xword sh_addralign; /* Section alignment */
    Elf64_Xword sh_entsize; /* Entry size if section holds table */
    } Elf64_Shdr;
  • sh_name: 이름이 저장되어 있는 문자열 테이블상의 인덱스를 의미한다. 인덱스는 ELF 헤더의 e_shstrndx 필드에 대응되는 문자열 테이블을 따른다. 섹션의 이름이 없으면 0이다.

  • sh_type: 섹션을 구분하는 enum

    • SHT_PROGBITS:
      기계어 명령이나 상수값 등의 데이터를 포함하고 있다. 이런 섹션은 링커가 분석해야 할 별도의 구조를 가지고 있지 않다.
    • SHT_SYMTAB, SHT_DYNSYM, SHT_STRTAB:
      심볼 테이블을 위한 섹션 타입(정적 심볼 테이블을 위한 SHT_SYMTAB, 동적 링킹 시에 필요한 심볼 테이블을 위한 SHT_DYNSYM)도 있고, 문자열 테이블(SHT_STRTAB)도 있다.
      심볼 테이블에는 파일 오프셋 또는 주소에 위치한 심볼의 명칭과 타입 정보를 명시해 둔 잘 정의된 형식의 심볼 정보가 포함된다. (struct Elf64_Sym 참고)
    • SHT_REL, SHT_RELA:
      이 타입의 섹션은 링커가 다른 섹션들 간의 필수적인 재배치 관계를 파악할 수 있도록 하고자 잘 정의된 형식(struct Elf64_Rel과 struct Elf64_Sym 참고)에 맞춰 재배치 엔트리 정보를 제공한다.
      각각의 재배치 엔트리 정보는 재배치가 필요한 부분의 주소와, 재배치 시 해결해야 하는 심볼 정보를 포함한다. 이 두 타입의 섹션은 정적 링킹을 위한 목적으로 사용된다.
    • SHT_DYNAMIC:
      이 타입의 섹션은 동적 링킹에 필요한 정보를 담고 있다. (struct Elf64_Dyn 참고)
  • sh_flags: 섹션과 관련된 추가 정보를 제공한다.

    • SHF_WRITE:
      실행 시점에 해당 섹션이 쓰기 가능한 상태임을 나타낸다. 이 정보를 통해 정적 데이터(상수값 등)에 해당하는 섹션과 변수 값을 저장하는 섹션을 구분할 수 있다.
    • SHF_ALLOC:
      바이너리가 실행될 때 해당 섹션의 정보가 가상 메모리에 적재된다는 의미다. (실제로는 섹션이 아닌 세그먼트 단위로 처리)
    • SHF_EXECINSTR:
      실행 가능한 명령어들을 담고 있는 섹션임을 의미한다.
  • sh_addr, sh_offset, sh_size: sh_addr는 가상 메모리의 주소, sh_offset은 파일 오프셋, sh_size는 섹션의 크기를 나타낸다.

  • sh_link: 관련된 섹션 헤더 테이블상 섹션들의 인덱스 정보들을 표기한다.

  • sh_info: 섹션의 추가적인 정보를 제공한다.

  • sh_addralign: 배치 관련 규칙들이 명시된다.

  • sh_entsize: 심볼 테이블이나 재배치 테이블과 같은 일부 섹션들은 잘 설계된 자료 구조(Elf64_Sym 또는 Elf64_Rela) 형태로 테이블을 갖는다. 이런 섹션들에는 해당 테이블의 각 엔트리의 크기가 몇 바이트인지를 명시하는 sh_entsize 필드가 존재한다. 사용하지 않는다면 0이다.

Sections

GNU/Linux 시스템의 ELF 파일들은 대부분 표준적인 섹션 구성으로 이루어져 있다.

Terminal window
$ readelf --sections --wide a.out
There are 29 section headers, starting at offset 0x1168:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 0000000000400238 000238 00001c 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 000254 000020 00 A 0 0 4
[ 3] .note.gnu.build-id NOTE 0000000000400274 000274 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 000298 00001c 00 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 0002b8 000060 18 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400318 000318 00003d 00 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400356 000356 000008 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400360 000360 000020 00 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400380 000380 000018 18 A 5 0 8
[10] .rela.plt RELA 0000000000400398 000398 000030 18 AI 5 24 8
[11] .init PROGBITS 00000000004003c8 0003c8 00001a 00 AX 0 0 4
[12] .plt PROGBITS 00000000004003f0 0003f0 000030 10 AX 0 0 16
[13] .plt.got PROGBITS 0000000000400420 000420 000008 00 AX 0 0 8
[14] .text PROGBITS 0000000000400430 000430 000192 00 AX 0 0 16
[15] .fini PROGBITS 00000000004005c4 0005c4 000009 00 AX 0 0 4
[16] .rodata PROGBITS 00000000004005d0 0005d0 000012 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 00000000004005e4 0005e4 000034 00 A 0 0 4
[18] .eh_frame PROGBITS 0000000000400618 000618 0000f4 00 A 0 0 8
[19] .init_array INIT_ARRAY0000000000600e10 000e10 000008 00 WA 0 0 8
[20] .fini_array FINI_ARRAY0000000000600e18 000e18 000008 00 WA 0 0 8
[21] .jcr PROGBITS 0000000000600e20 000e20 000008 00 WA 0 0 8
[22] .dynamic DYNAMIC 0000000000600e28 000e28 0001d0 10 WA 6 0 8
[23] .got PROGBITS 0000000000600ff8 000ff8 000008 08 WA 0 0 8
[24] .got.plt PROGBITS 0000000000601000 001000 000028 08 WA 0 0 8
[25] .data PROGBITS 0000000000601028 001028 000010 00 WA 0 0 8
[26] .bss NOBITS 0000000000601038 001038 000008 00 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 001038 000034 01 MS 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00106c 0000fc 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
  • .init 섹션에는 초기화에 필요한 실행 코드가 포함된다. 운영체제의 제어권이 바이너리의 메인 엔트리로 넘어가면 이 섹션의 코드부터 실행된다.

  • .fini 섹션은 메인 프로그램이 완료된 후에 실행된다. .init과 반대로 소멸자 역할을 한다.

  • .init_array 섹션은 일종의 생성자로 사용할 함수에 대한 포인터 배열이 포함된다. 각 함수는 메인 함수가 호출되기 전에 초기화되며 차례로 호출된다. .init_array는 데이터 섹션으로 사용자 정의 생성자에 대한 포인터를 포함해 원하는 만큼 함수 포인터를 포함할 수 있다.

  • .fini_array 섹션은 소멸자에 대한 포인터 배열이 포함된다. .init_array와 유사하다. 이전 버전의 gcc로 생성한 바이너리는 .ctors와 .dtors라고 부른다.

  • .text 섹션에는 메인 함수 코드가 존재한다. 사용자 정의 코드를 포함하기 때문에 SHT_PROGBITS라는 타입으로 설정되어 있다. 또한 실행 가능하지만 쓰기는 불가능해 섹션 플래그는 AX다. _start, register_tm_clones, frame_dummy와 같은 여러 표준 함수가 포함된다.

  • .rodata 섹션에는 상숫값과 같은 읽기 전용 데이터가 저장된다.

  • .data 섹션은 초기화된 변수의 기본값이 저장된다. 이 값은 변경되어야 하므로 쓰기 가능한 영역이다.

  • .bss 섹션은 초기화되지 않은 변수들을 위해 예약된 공간이다. BSS는 심벌에 의해 시작되는 블록 영역(Block Strarted by Symbol)이라는 의미로, (심벌) 변수들이 저장될 메모리 블록으로 사용한다.

  • .rel.*.rela.* 형식의 섹션들은 재배치 과정에서 링커가 활용할 정보를 담고 있다. 모두 SHT_RELA 타입이며 재배치 항목들을 기재한 테이블이다. 테이블의 각 항목은 재배치가 적용돼야 하는 주소와 해당 주소에 연결해야 하는 정보를 저장한다. 동적 링킹 단계에서 수행할 동적 재배치만 남아 있다. 다음은 동적 링킹의 가장 일반적인 두 타입이다.

    • GLOB_DAT(global data): 이 재배치는 재배치는 데이터 심벌의 주소를 계산하고 .got의 오프셋에 연결하는 데 사용된다. 오프셋이 .got 섹션의 시작 주소를 나타낸다.
    • JUMP_SLO(jump slots): .got.plt 섹션에 오프셋이 있으며 라이브러리 함수의 주소가 연결될 수있는 슬롯을 나타낸다. 엔트리는 점프 슬롯(jump slot)이라고 부른다. 이 엔트리의 오프셋은 해당 함수에서 간접 점프하는 점프 슬롯의 주소다. (rip로부터의 상대 주소로 계산)
  • .dynamic 섹션은 바이너리가 로드될 때 운영체제와 동적 링커에게 일종의 road map을 제시하는 역할을 한다. 일명 태그(tag)라고 하는 Elf64_dyn 구조의 테이블을 포함한다. 태그는 번호로 구분한다.

    • DT_NEEDED 태그는 바이너리와 의존성 관계를 가진 정보를 동적 링커에게 알려준다.
    • DT_VERNEEDDT_VERNEEDNUM 태그는 버전 의존성 테이블(version dependency table)의 시작 주소와 엔트리 수를 지정한다.
    • 동적 링커의 수행에 필요한 중요한 정보들을 가리키는 역할을 하기도 한다. 예를 들어 동적 문자열 테이블(DT_STRTAB), 동적 심벌 테이블(DT_SYMTAB), .got.plt 섹션(DT_PLTGOT), 동적 재배치 섹션(DT_RELA) 등.
  • .shstrtab 섹션은 섹션의 이름을 포함하는 문자열 배열이다. 각 이름들을 숫자로 인덱스가 매겨져 있다.

  • .symtab 섹션에는 Elf64_Sym 구조체의 테이블인 심벌 테이블이 포함되어 있다. 각 심벌 테이블은 심벌명을 함수나 변수와 같이 코드나 데이터와 연관시킨다.

  • .strtab 섹션에는 심벌 이름을 포함한 실제 문자열들이 위치한다. 이 문자열들은 Elf64_Sym 테이블과 연결된다. 스트립 된 바이너리에는 .symtab.strtab 테이블은 전부 삭제된다.

  • .dynsym 섹션과 .dynstr 섹션은 동적 링킹에 필요한 심벌과 문자열 정보를 담고 있다는 점을 제외하면 .symtab이나 .strtab와 유사하다. 정적 심벌 테이블은 섹션 타입이 SHT_SYMTAB이고 동적 심벌 테이블은 SHT_DYNSYM 타입이다.


참고