ELF入門
C言語が書ける、アセンブラが吐き出すアセンブリコードが分かる、でもHello, worldするバイナリは何が書かれているか分からない…。というか、バイナリの実行って何?っていう疑問を解決したいバイナリ初心者のメモです。
今回はreadelfコマンドは知っていても、どんな意味がある出力なのかがいまいち分かっていなかったので、車輪の再開発で理解していこうという試みです。とりあえずreadelfに-h、-l、-Sオプションを指定した時と同じような出力ができるプログラムの作成を目指します。
まず、ELFファイルとはなんぞやという所からですが、Windowsでいうexeみたいなもんだと思います(乱暴)。ELFはExecutable and Linking Formatの略であり、例えばC言語で書かれたソースコードをコンパイルして出来た実行可能ファイルはELFファイルです。
実行可能ファイルだけでなく、共有ライブラリや再配置可能オブジェクト、コアファイルもELFに従っています。これらの拡張子は.soファイルであったり.oファイルであったりします。コアファイルについては以前書いたこちらを参照してください。
1年生に送るデバッガ(gdb)の使い方(入門編) - 情弱ログ
このELFファイルですが、バイナリファイルであるため通常は内容を確認することはできません。逸般人の方はバイナリエディタを使って見ることができるらしいですが、通常はreadelfコマンドを用いて確認することになります。しかし、このreadelfコマンドは「分かっている人」向けの出力であり、初心者には少し出力が難解です。
そこで、まずはELFファイルの形式について勉強するため、簡単なELFファイルをダンプするプログラムを書いていきます。
コードの全容はこちら
github.com
何はともあれ、ELFファイルの構造が分かっていないとどうしようもないので、wikipediaあたりを見てみます。すると以下のような図があったので、以降はこの図を参考にしていきます。余談ですがman elfは滅茶苦茶丁寧に書かれているので一見の価値ありです。
wikipediaより
図から、ELFファイルの最初にELF header、続いてProgram header table、ファイルの最後にSection header tableがあることが分かります。次に、/usr/include以下にelf.hという、いかにもなヘッダファイルがあるので中身を確認していきます。
#include <stdint.h> /* Type for a 16-bit quantity. */ typedef uint16_t Elf64_Half; /* Types for signed and unsigned 32-bit quantities. */ typedef uint32_t Elf64_Word; typedef int32_t Elf64_Sword; /* Types for signed and unsigned 64-bit quantities. */ typedef uint64_t Elf64_Xword; typedef int64_t Elf64_Sxword; /* Type of addresses. */ typedef uint64_t Elf64_Addr; /* Type of file offsets. */ typedef uint64_t Elf64_Off; /* Type for section indices, which are 16-bit quantities. */ typedef uint16_t Elf64_Section; /* Type for version symbol information. */ typedef Elf64_Half Elf64_Versym; /* The ELF file header. This appears at the start of every ELF file. */ #define EI_NIDENT (16) 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; /* Section header. */ 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; /* Program segment header. */ typedef struct { Elf64_Word p_type; /* Segment type */ Elf64_Word p_flags; /* Segment flags */ Elf64_Off p_offset; /* Segment file offset */ Elf64_Addr p_vaddr; /* Segment virtual address */ Elf64_Addr p_paddr; /* Segment physical address */ Elf64_Xword p_filesz; /* Segment size in file */ Elf64_Xword p_memsz; /* Segment size in memory */ Elf64_Xword p_align; /* Segment alignment */ } Elf64_Phdr;
途中をかなり省略したり、64bit向けの構造体のみを抜き出したりしています。図のELF headerがElf64_Ehdrに、Program header tableがElf64_Phdrに、Section header tableがElf64_Shdrに対応しています。これらの情報を元に、まずELF (file) headerをダンプするプログラムを書いていきましょう。
#include <stdio.h> #include <stdint.h> #include <sys/stat.h> #include <fcntl.h> #include <assert.h> #include <elf.h> #define DUMP(x) do {printf(" " #x " = %u(0x%x)\n", (uint32_t)x, (uint32_t)x);} while(0); void dump_ehdr(Elf64_Ehdr *ehdr){ int i; printf(" ehdr->e_ident = "); for (i = 0; i < EI_NIDENT; i++) { printf("%02x ", ehdr->e_ident[i]); } printf("\n"); DUMP(ehdr->e_type); DUMP(ehdr->e_machine); DUMP(ehdr->e_version); DUMP(ehdr->e_entry); DUMP(ehdr->e_phoff); DUMP(ehdr->e_shoff); DUMP(ehdr->e_flags); DUMP(ehdr->e_ehsize); DUMP(ehdr->e_phentsize); DUMP(ehdr->e_phnum); DUMP(ehdr->e_shentsize); DUMP(ehdr->e_shnum); DUMP(ehdr->e_shstrndx); printf("\n"); } int main(void){ Elf64_Ehdr *ehdr; int fd; FILE *fp; struct stat stbuf; fd = open("hello", O_RDONLY); assert(fd); fp = fdopen(fd, "rb"); assert(fp); fstat(fd, &stbuf); unsigned char buf[stbuf.st_size]; assert(fread(buf, 1, sizeof(buf), fp) == (unsigned long)stbuf.st_size); fclose(fp); printf("Elf file header(equivalent as readelf -h)\n"); ehdr = (Elf64_Ehdr *)buf; dump_ehdr(ehdr); }
適当にhello worldするhelloという実行可能ファイルを生成し、ダンププログラムを実行します。すると以下のような実行結果が得られました。
$ dump Elf file header(equivalent as readelf -h) ehdr->e_ident = 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 ehdr->e_type = 2(0x2) ehdr->e_machine = 62(0x3e) ehdr->e_version = 1(0x1) ehdr->e_entry = 4195456(0x400480) ehdr->e_phoff = 64(0x40) ehdr->e_shoff = 6056(0x17a8) ehdr->e_flags = 0(0x0) ehdr->e_ehsize = 64(0x40) ehdr->e_phentsize = 56(0x38) ehdr->e_phnum = 10(0xa) ehdr->e_shentsize = 64(0x40) ehdr->e_shnum = 29(0x1d) ehdr->e_shstrndx = 26(0x1a)
readelfコマンドの実行結果と見比べてみます。readelfコマンドによるELFヘッダの出力は--file-header(-h)オプションでできます。
$ readelf -h hello 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: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400480 Start of program headers: 64 (bytes into file) Start of section headers: 6056 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 10 Size of section headers: 64 (bytes) Number of section headers: 29 Section header string table index: 26
見比べてみると、大体同じような出力が得られていますが、自作のダンププログラムではClass、Data、Version、OS/ABI、ABI Versionが取得できていません。実はこれらは上のMagicに格納されており、マジックナンバーである0x7f 0x45 0x4c 0x64以降の情報がそれらに当たります。ちなみにELFファイルのマジックナンバーの0x45 0x4c 0x64はasciiコードでELFになります。0x7fは知りません。もう一度elf.hを見てみると、コメントからマジックナンバー(e_ident)に格納される値は以下のようになっていることが分かります。
/* Conglomeration of the identification bytes, for easy testing as a word. */ #define ELFMAG "\177ELF" #define SELFMAG 4 #define EI_CLASS 4 /* File class byte index */ #define ELFCLASSNONE 0 /* Invalid class */ #define ELFCLASS32 1 /* 32-bit objects */ #define ELFCLASS64 2 /* 64-bit objects */ #define ELFCLASSNUM 3 #define EI_DATA 5 /* Data encoding byte index */ #define ELFDATANONE 0 /* Invalid data encoding */ #define ELFDATA2LSB 1 /* 2's complement, little endian */ #define ELFDATA2MSB 2 /* 2's complement, big endian */ #define ELFDATANUM 3 #define EI_VERSION 6 /* File version byte index */ /* Value must be EV_CURRENT */ #define EI_OSABI 7 /* OS ABI identification */ #define ELFOSABI_NONE 0 /* UNIX System V ABI */ #define ELFOSABI_SYSV 0 /* Alias. */ #define ELFOSABI_HPUX 1 /* HP-UX */ #define ELFOSABI_NETBSD 2 /* NetBSD. */ #define ELFOSABI_GNU 3 /* Object uses GNU ELF extensions. */ #define ELFOSABI_LINUX ELFOSABI_GNU /* Compatibility alias. */ #define ELFOSABI_SOLARIS 6 /* Sun Solaris. */ #define ELFOSABI_AIX 7 /* IBM AIX. */ #define ELFOSABI_IRIX 8 /* SGI Irix. */ #define ELFOSABI_FREEBSD 9 /* FreeBSD. */ #define ELFOSABI_TRU64 10 /* Compaq TRU64 UNIX. */ #define ELFOSABI_MODESTO 11 /* Novell Modesto. */ #define ELFOSABI_OPENBSD 12 /* OpenBSD. */ #define ELFOSABI_ARM_AEABI 64 /* ARM EABI */ #define ELFOSABI_ARM 97 /* ARM */ #define ELFOSABI_STANDALONE 255 /* Standalone (embedded) application */ #define EI_ABIVERSION 8 /* ABI version */ #define EI_PAD 9 /* Byte index of padding bytes */ /* Legal values for e_version (version). */ #define EV_NONE 0 /* Invalid ELF version */ #define EV_CURRENT 1 /* Current version */ #define EV_NUM 2
ClassはELFCLASS64=2、DataはELFDATA2LSB=1、VersionはEV_CURRENT=1、OS/ABIはELFOSABI_LINUX…ではなくELFOSABI_NONE=0、ABI Versionは…分からないです。とりあえず0でした。
仕様書を確認してみたんですが、そもそも仕様書のバージョン1.2の時点ではEI_VERSION以降は未定義なので独自拡張なのかな…。
ちなみに仕様書はこちらにあります。
https://refspecs.linuxfoundation.org/elf/elf.pdf
とりあえずreadelf -hが何をしているかが分かりました。このコマンドはELFファイルの頭から64バイトを読み込んで、各値を読みやすいように表示してくれているようです。
では、次にsection header tableとprogram header tableを見ていきます。ELFファイルヘッダのダンプ出力から、program header tableの開始位置はe_phoff=0x64、テーブルの要素数はe_phnum=10個であることが分かります。同様にsection header tableの開始位置はe_shoff=0x17a8、要素数はe_shnum=29個であることが分かります。これらを元に、先ほどのダンププログラムを拡張していきましょう。
#include <stdio.h> #include <stdint.h> #include <sys/stat.h> #include <fcntl.h> #include <assert.h> #include <elf.h> #define DUMP(x) do {printf(" " #x " = %u(0x%x)\n", (uint32_t)x, (uint32_t)x);} while(0); void dump_ehdr(Elf64_Ehdr *ehdr){ int i; printf(" ehdr->e_ident = "); for (i = 0; i < EI_NIDENT; i++) { printf("%02x ", ehdr->e_ident[i]); } printf("\n"); DUMP(ehdr->e_type); DUMP(ehdr->e_machine); DUMP(ehdr->e_version); DUMP(ehdr->e_entry); DUMP(ehdr->e_phoff); DUMP(ehdr->e_shoff); DUMP(ehdr->e_flags); DUMP(ehdr->e_ehsize); DUMP(ehdr->e_phentsize); DUMP(ehdr->e_phnum); DUMP(ehdr->e_shentsize); DUMP(ehdr->e_shnum); DUMP(ehdr->e_shstrndx); printf("\n"); } void dump_phdr(Elf64_Phdr *phdr, int e_phnum){ int i; for (i = 0; i < e_phnum; i++, phdr++) { DUMP(phdr->p_type); DUMP(phdr->p_flags); DUMP(phdr->p_offset); DUMP(phdr->p_vaddr); DUMP(phdr->p_paddr); DUMP(phdr->p_filesz); DUMP(phdr->p_memsz); DUMP(phdr->p_align); printf("\n"); } printf("\n"); } void dump_shdr(Elf64_Shdr *shdr, int e_shnum){ int i; for (i = 0; i < e_shnum; i++, shdr++) { DUMP(shdr->sh_name); DUMP(shdr->sh_type); DUMP(shdr->sh_flags); DUMP(shdr->sh_addr); DUMP(shdr->sh_offset); DUMP(shdr->sh_size); DUMP(shdr->sh_link); DUMP(shdr->sh_info); DUMP(shdr->sh_addralign); DUMP(shdr->sh_entsize); printf("\n"); } printf("\n"); } int main(void){ Elf64_Ehdr *ehdr; Elf64_Phdr *phdr; Elf64_Shdr *shdr; int fd; FILE *fp; struct stat stbuf; fd = open("hello", O_RDONLY); assert(fd); fp = fdopen(fd, "rb"); assert(fp); fstat(fd, &stbuf); unsigned char buf[stbuf.st_size]; assert(fread(buf, 1, sizeof(buf), fp) == (unsigned long)stbuf.st_size); fclose(fp); printf("Elf file header(equivalent as readelf -h)\n"); ehdr = (Elf64_Ehdr *)buf; dump_ehdr(ehdr); printf("Program header(equivalent as readelf -l)\n"); phdr = (Elf64_Phdr *)(&buf[ehdr->e_phoff]); dump_phdr(phdr, ehdr->e_phnum); printf("Section header(equivalent as readelf -S)\n"); shdr = (Elf64_Shdr *)(&buf[ehdr->e_shoff]); dump_shdr(shdr, ehdr->e_shnum); return 0; }
実行結果が長くなってしまったのでgistに上げています。
ELFファイルのダンプ結果 · GitHub
readelfコマンドの結果と見比べていきます。program header tableはreadelf -l、section header tableはreadelf -Sで見ることができます。
$ readelf -l hello Elf file type is EXEC (Executable file) Entry point 0x400480 There are 10 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000230 0x0000000000000230 R E 8 INTERP 0x0000000000000270 0x0000000000400270 0x0000000000400270 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000724 0x0000000000000724 R E 200000 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x0000000000000230 0x0000000000000238 RW 200000 DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28 0x00000000000001d0 0x00000000000001d0 RW 8 NOTE 0x000000000000028c 0x000000000040028c 0x000000000040028c 0x0000000000000020 0x0000000000000020 R 4 GNU_EH_FRAME 0x0000000000000604 0x0000000000400604 0x0000000000400604 0x0000000000000034 0x0000000000000034 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x00000000000001f0 0x00000000000001f0 R 1 PAX_FLAGS 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 8 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got 09
$ readelf -S hello There are 29 section headers, starting at offset 0x17a8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400270 00000270 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 000000000040028c 0000028c 0000000000000020 0000000000000000 A 0 0 4 [ 3] .gnu.hash GNU_HASH 00000000004002b0 000002b0 000000000000001c 0000000000000000 A 4 0 8 [ 4] .dynsym DYNSYM 00000000004002d0 000002d0 0000000000000060 0000000000000018 A 5 1 8 [ 5] .dynstr STRTAB 0000000000400330 00000330 000000000000003d 0000000000000000 A 0 0 1 [ 6] .gnu.version VERSYM 000000000040036e 0000036e 0000000000000008 0000000000000002 A 4 0 2 [ 7] .gnu.version_r VERNEED 0000000000400378 00000378 0000000000000020 0000000000000000 A 5 1 8 [ 8] .rela.dyn RELA 0000000000400398 00000398 0000000000000018 0000000000000018 A 4 0 8 [ 9] .rela.plt RELA 00000000004003b0 000003b0 0000000000000048 0000000000000018 AI 4 11 8 [10] .init PROGBITS 00000000004003f8 000003f8 000000000000001a 0000000000000000 AX 0 0 4 [11] .plt PROGBITS 0000000000400420 00000420 0000000000000040 0000000000000010 AX 0 0 16 [12] .text PROGBITS 0000000000400460 00000460 0000000000000181 0000000000000000 AX 0 0 16 [13] .fini PROGBITS 00000000004005e4 000005e4 0000000000000009 0000000000000000 AX 0 0 4 [14] .rodata PROGBITS 00000000004005f0 000005f0 0000000000000012 0000000000000000 A 0 0 4 [15] .eh_frame_hdr PROGBITS 0000000000400604 00000604 0000000000000034 0000000000000000 A 0 0 4 [16] .eh_frame PROGBITS 0000000000400638 00000638 00000000000000ec 0000000000000000 A 0 0 8 [17] .init_array INIT_ARRAY 0000000000600e10 00000e10 0000000000000008 0000000000000000 WA 0 0 8 [18] .fini_array FINI_ARRAY 0000000000600e18 00000e18 0000000000000008 0000000000000000 WA 0 0 8 [19] .jcr PROGBITS 0000000000600e20 00000e20 0000000000000008 0000000000000000 WA 0 0 8 [20] .dynamic DYNAMIC 0000000000600e28 00000e28 00000000000001d0 0000000000000010 WA 5 0 8 [21] .got PROGBITS 0000000000600ff8 00000ff8 0000000000000008 0000000000000008 WA 0 0 8 [22] .got.plt PROGBITS 0000000000601000 00001000 0000000000000030 0000000000000008 WA 0 0 8 [23] .data PROGBITS 0000000000601030 00001030 0000000000000010 0000000000000000 WA 0 0 8 [24] .bss NOBITS 0000000000601040 00001040 0000000000000008 0000000000000000 WA 0 0 1 [25] .comment PROGBITS 0000000000000000 00001040 000000000000002a 0000000000000001 MS 0 0 1 [26] .shstrtab STRTAB 0000000000000000 0000106a 00000000000000f5 0000000000000000 0 0 1 [27] .symtab SYMTAB 0000000000000000 00001160 00000000000004e0 0000000000000018 28 32 8 [28] .strtab STRTAB 0000000000000000 00001640 0000000000000166 0000000000000000 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)
中々見比べるのが大変ですね。まずはprogram header tableから見ていきます。ちょっと量が多いので、一番上の出力だけを見て行きましょう。
phdr->p_type = 6(0x6) phdr->p_flags = 5(0x5) phdr->p_offset = 64(0x40) phdr->p_vaddr = 4194368(0x400040) phdr->p_paddr = 4194368(0x400040) phdr->p_filesz = 560(0x230) phdr->p_memsz = 560(0x230) phdr->p_align = 8(0x8)
以下はreadelf -l helloの結果から対応してそうな値を抜粋したものです。
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000230 0x0000000000000230 R E 8
先述したように、program header tableに対応する構造体はElf64_Phdrになります。Elf64_Phdrのメンバのうち、p_typeとp_flagsは以下の値で定義されます。
/* Legal values for p_type (segment type). */ #define PT_NULL 0 /* Program header table entry unused */ #define PT_LOAD 1 /* Loadable program segment */ #define PT_DYNAMIC 2 /* Dynamic linking information */ #define PT_INTERP 3 /* Program interpreter */ #define PT_NOTE 4 /* Auxiliary information */ #define PT_SHLIB 5 /* Reserved */ #define PT_PHDR 6 /* Entry for header table itself */ #define PT_TLS 7 /* Thread-local storage segment */ #define PT_NUM 8 /* Number of defined types */ #define PT_LOOS 0x60000000 /* Start of OS-specific */ #define PT_GNU_EH_FRAME 0x6474e550 /* GCC .eh_frame_hdr segment */ #define PT_GNU_STACK 0x6474e551 /* Indicates stack executability */ #define PT_GNU_RELRO 0x6474e552 /* Read-only after relocation */ #define PT_LOSUNW 0x6ffffffa #define PT_SUNWBSS 0x6ffffffa /* Sun Specific segment */ #define PT_SUNWSTACK 0x6ffffffb /* Stack segment */ #define PT_HISUNW 0x6fffffff #define PT_HIOS 0x6fffffff /* End of OS-specific */ #define PT_LOPROC 0x70000000 /* Start of processor-specific */ #define PT_HIPROC 0x7fffffff /* End of processor-specific */ /* Legal values for p_flags (segment flags). */ #define PF_X (1 << 0) /* Segment is executable */ #define PF_W (1 << 1) /* Segment is writable */ #define PF_R (1 << 2) /* Segment is readable */ #define PF_MASKOS 0x0ff00000 /* OS-specific */ #define PF_MASKPROC 0xf0000000 /* Processor-specific */
(これもまた仕様書に載ってない値がちらほら…)
p_type==6ということはPT_PHDRですね。また、p_flags == ( (1<<0) | (1<<2) )であるため、executableかつreadableであることが分かります。readelfコマンドの出力結果を見るとFlagsのところにR Eと書いてあるので、きっとReadableとExecutableでしょう。その他の値に関しても同じ出力が得られています。
この出力結果から、program header tableに格納されている種類や値が分かりました。tableというからには、ファイルが参照する先には何か値が入っているのでしょう。(少しメタりますがこのPT_PHDRでは面白くないので、次の値のPT_INTERPを見てみます)。
phdr->p_type = 3(0x3) phdr->p_flags = 4(0x4) phdr->p_offset = 624(0x270) phdr->p_vaddr = 4194928(0x400270) phdr->p_paddr = 4194928(0x400270) phdr->p_filesz = 28(0x1c) phdr->p_memsz = 28(0x1c) phdr->p_align = 1(0x1)
readelf -l helloの出力の該当しそうな値は以下の結果です。(一部抜粋)
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align INTERP 0x0000000000000270 0x0000000000400270 0x0000000000400270 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
readelfコマンドの方では[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]という出力が得られましたが、そんな値はElf64_Phdrには格納されていません。どういうことでしょうか。
とりあえずp_offset=0x270という点と、p_filesz=0x1cという点に注目して、helloファイル上の値を確認してみます。odコマンドを使い、16進数とascii表示でダンプしていきます。
$ od -Ax -tx1c -j 0x270 -N 0x1c hello 000270 2f 6c 69 62 36 34 2f 6c 64 2d 6c 69 6e 75 78 2d / l i b 6 4 / l d - l i n u x - 000280 78 38 36 2d 36 34 2e 73 6f 2e 32 00 x 8 6 - 6 4 . s o . 2 \0 00028c
readelfで得られた出力と同じ文字列が格納されていることが分かります。このことから、readelfコマンドは自動的にp_typeがPT_INTERPであるときはテーブルの参照先を確認して、その文字列を表示してくれることが分かりました。
ちなみにPT_INTERPの詳細については分からないので飛ばします。動的リンクを処理するインタプリタが入っているとのことです。
次に、section header tableを見てみます。program header tableと同様に一番目の値だけを比較して…と思ったんですが、全て0が格納されていて見比べようが無いので二番目の値を見ていきます。
shdr->sh_name = 27(0x1b) shdr->sh_type = 1(0x1) shdr->sh_flags = 2(0x2) shdr->sh_addr = 4194928(0x400270) shdr->sh_offset = 624(0x270) shdr->sh_size = 28(0x1c) shdr->sh_link = 0(0x0) shdr->sh_info = 0(0x0) shdr->sh_addralign = 1(0x1) shdr->sh_entsize = 0(0x0)
$ readelf -S hello [ 1] .interp PROGBITS 0000000000400270 00000270 000000000000001c 0000000000000000 A 0 0 1
まずsh_nameですが、elf.hのコメントを見るとSection name (string tbl index)と書いてあり、string tableという新しいテーブルが関係してくるようです。これは、ELF headerのe_shstrndxが指し示す位置に格納されています。尚、e_shstrndxが指し示す位置というのはファイルの先頭からのオフセットではなく、section header tableのインデックスに対応しています。早速、ehdr->e_shstrndx = 26(0x1a)という情報を元に26番目のsection header tableを見てみましょう。
shdr->sh_name = 17(0x11) shdr->sh_type = 3(0x3) shdr->sh_flags = 0(0x0) shdr->sh_addr = 0(0x0) shdr->sh_offset = 4202(0x106a) shdr->sh_size = 245(0xf5) shdr->sh_link = 0(0x0) shdr->sh_info = 0(0x0) shdr->sh_addralign = 1(0x1) shdr->sh_entsize = 0(0x0)
sh_offsetは0x106a、sh_sizeは0xf5ということが分かったのでodでダンプします。
$ od -Ax -c -j 0x106a -N 0xf5 hello 00106a \0 . s y m t a b \0 . s t r t a b 00107a \0 . s h s t r t a b \0 . i n t e 00108a r p \0 . n o t e . A B I - t a g 00109a \0 . g n u . h a s h \0 . d y n s 0010aa y m \0 . d y n s t r \0 . g n u . 0010ba v e r s i o n \0 . g n u . v e r 0010ca s i o n _ r \0 . r e l a . d y n 0010da \0 . r e l a . p l t \0 . i n i t 0010ea \0 . t e x t \0 . f i n i \0 . r o 0010fa d a t a \0 . e h _ f r a m e _ h 00110a d r \0 . e h _ f r a m e \0 . i n 00111a i t _ a r r a y \0 . f i n i _ a 00112a r r a y \0 . j c r \0 . d y n a m 00113a i c \0 . g o t \0 . g o t . p l t 00114a \0 . d a t a \0 . b s s \0 . c o m 00115a m e n t \0 00115f
NULL文字で区切って要素名が書かれていることが分かります。これを元にプログラムを拡張しましょう。ちなみにsh_nameはstring tableの先頭からのオフセットを意味します。
void dump_stringtbl(unsigned char *str, Elf64_Shdr *shdr){ unsigned char *tbl_head = &str[shdr->sh_offset]; unsigned long total_len = 0; while(total_len < shdr->sh_size){ printf(" %03lu: %s\n", (&tbl_head[total_len] - tbl_head), &tbl_head[total_len]); total_len += strlen((char *)&tbl_head[total_len]) + 1; } printf("\n"); } int main(void){ Elf64_Ehdr *ehdr; Elf64_Phdr *phdr; Elf64_Shdr *shdr; int fd; FILE *fp; struct stat stbuf; fd = open("hello", O_RDONLY); assert(fd); fp = fdopen(fd, "rb"); assert(fp); fstat(fd, &stbuf); unsigned char buf[stbuf.st_size]; assert(fread(buf, 1, sizeof(buf), fp) == (unsigned long)stbuf.st_size); fclose(fp); // 〜(中略)〜 ehdr = (Elf64_Ehdr *)buf; shdr = (Elf64_Shdr *)(&buf[ehdr->e_shoff]); printf("String table\n"); dump_stringtbl(buf, &shdr[ehdr->e_shstrndx]); return 0; }
出力結果を見てみましょう。
$ ./dump (中略) String table 000: 001: .symtab 009: .strtab 017: .shstrtab 027: .interp 035: .note.ABI-tag 049: .gnu.hash 059: .dynsym 067: .dynstr 075: .gnu.version 088: .gnu.version_r 103: .rela.dyn 113: .rela.plt 123: .init 129: .text 135: .fini 141: .rodata 149: .eh_frame_hdr 163: .eh_frame 173: .init_array 185: .fini_array 197: .jcr 202: .dynamic 211: .got 216: .got.plt 225: .data 231: .bss 236: .comment
section header tableの二番目の値を再掲します。
shdr->sh_name = 27(0x1b) shdr->sh_type = 1(0x1) shdr->sh_flags = 2(0x2) shdr->sh_addr = 4194928(0x400270) shdr->sh_offset = 624(0x270) shdr->sh_size = 28(0x1c) shdr->sh_link = 0(0x0) shdr->sh_info = 0(0x0) shdr->sh_addralign = 1(0x1) shdr->sh_entsize = 0(0x0)
sh_nameに27が格納されていることから、この値の名前は.interpであることが確認できました。これでreadelf -S helloと同等の結果が得られていることが分かりました。
次に、sh_typeとsh_flagsはelf.hに以下のように記述されています。
/* Legal values for sh_type (section type). */ #define SHT_NULL 0 /* Section header table entry unused */ #define SHT_PROGBITS 1 /* Program data */ #define SHT_SYMTAB 2 /* Symbol table */ #define SHT_STRTAB 3 /* String table */ #define SHT_RELA 4 /* Relocation entries with addends */ #define SHT_HASH 5 /* Symbol hash table */ #define SHT_DYNAMIC 6 /* Dynamic linking information */ #define SHT_NOTE 7 /* Notes */ #define SHT_NOBITS 8 /* Program space with no data (bss) */ #define SHT_REL 9 /* Relocation entries, no addends */ #define SHT_SHLIB 10 /* Reserved */ #define SHT_DYNSYM 11 /* Dynamic linker symbol table */ #define SHT_INIT_ARRAY 14 /* Array of constructors */ #define SHT_FINI_ARRAY 15 /* Array of destructors */ #define SHT_PREINIT_ARRAY 16 /* Array of pre-constructors */ #define SHT_GROUP 17 /* Section group */ #define SHT_SYMTAB_SHNDX 18 /* Extended section indeces */ #define SHT_NUM 19 /* Number of defined types. */ #define SHT_LOOS 0x60000000 /* Start OS-specific. */ #define SHT_GNU_ATTRIBUTES 0x6ffffff5 /* Object attributes. */ #define SHT_GNU_HASH 0x6ffffff6 /* GNU-style hash table. */ #define SHT_GNU_LIBLIST 0x6ffffff7 /* Prelink library list */ #define SHT_CHECKSUM 0x6ffffff8 /* Checksum for DSO content. */ #define SHT_LOSUNW 0x6ffffffa /* Sun-specific low bound. */ #define SHT_SUNW_move 0x6ffffffa #define SHT_SUNW_COMDAT 0x6ffffffb #define SHT_SUNW_syminfo 0x6ffffffc #define SHT_GNU_verdef 0x6ffffffd /* Version definition section. */ #define SHT_GNU_verneed 0x6ffffffe /* Version needs section. */ #define SHT_GNU_versym 0x6fffffff /* Version symbol table. */ #define SHT_HISUNW 0x6fffffff /* Sun-specific high bound. */ #define SHT_HIOS 0x6fffffff /* End OS-specific type */ #define SHT_LOPROC 0x70000000 /* Start of processor-specific */ #define SHT_HIPROC 0x7fffffff /* End of processor-specific */ #define SHT_LOUSER 0x80000000 /* Start of application-specific */ #define SHT_HIUSER 0x8fffffff /* End of application-specific */ /* Legal values for sh_flags (section flags). */ #define SHF_WRITE (1 << 0) /* Writable */ #define SHF_ALLOC (1 << 1) /* Occupies memory during execution */ #define SHF_EXECINSTR (1 << 2) /* Executable */ #define SHF_MERGE (1 << 4) /* Might be merged */ #define SHF_STRINGS (1 << 5) /* Contains nul-terminated strings */ #define SHF_INFO_LINK (1 << 6) /* `sh_info' contains SHT index */ #define SHF_LINK_ORDER (1 << 7) /* Preserve order after combining */ #define SHF_OS_NONCONFORMING (1 << 8) /* Non-standard OS specific handling required */ #define SHF_GROUP (1 << 9) /* Section is member of a group. */ #define SHF_TLS (1 << 10) /* Section hold thread-local data. */ #define SHF_COMPRESSED (1 << 11) /* Section with compressed data. */ #define SHF_MASKOS 0x0ff00000 /* OS-specific. */ #define SHF_MASKPROC 0xf0000000 /* Processor-specific */ #define SHF_ORDERED (1 << 30) /* Special ordering requirement (Solaris). */ #define SHF_EXCLUDE (1U << 31) /* Section is excluded unless referenced or allocated (Solaris).*/
sh_typeはSHT_PROGBITS=1ですし、sh_flagsは0x1なのでSHF_ALLOCと同値です。readelf -S helloの出力を見てもAllocateのものと思われるAが書いてあるので、これらに関しても同等の結果が得られています。
次に、sh_infoとsh_linkですが、これらの値に関してはelf.hのコメントを読んでも該当する値が見当たりませんでした。仕様書を確認した所、sh_typeによって異なる値が設定されるらしく、今回ではsh_type=0であり、コメントにはSection header table entry unusedと書かれているので、sh_infoとsh_linkに関しても使用していないものだと思われます。
次にsh_sizeとsh_entsizeという2つの値ですが、sh_sizeは今までも使ってきたようにセクション自体のサイズをバイト単位で表したものです。sh_entsizeはシンボルテーブルのような固定長のエントリを持つテーブルが、各エントリのサイズを表すために使っているそうです。仕様書をそのまま訳しただけなので、次回以降に実際にシンボルテーブルを見てみるときに確認しましょう。
最初に想定していた5倍くらいの文量になりましたが、以上でreadelf -h、-l、-Sと同じ出力をするダンププログラムの作成と解説を終わります。以下まとめです。
readelfコマンドはELFファイルの各フィールドを確認して、人間が見やすいように整形してくれるコマンドです(今更)。readelf -hではELFファイルの先頭64バイトを元に整形結果を出力します。readelf -lではまずreadelf -hと同様にELFファイルの先頭64バイトを確認し、ELFファイルの先頭からe_phoffsetバイト目をプログラムヘッダテーブルとして整形して出力します。readelf -Sも同様にELFファイルの先頭64バイトを確認し、ELFファイルの先頭からe_shoffsetバイト目をセクションヘッダテーブルとして整形して出力します。この時、セクションヘッダテーブルのe_shstrndx番目にあるストリングテーブルを確認して、各セクションに設定されたsh_nameに該当する名前を整形して表示しています。
まだまだ分からないことがたくさんあるので、おいおい見ていきたいと思います。