二进制文件通常是与平台相关的,因此这里选择的Linux,ELF 二进制。
示例C代码:
#include <stdio.h>
int main() {
printf("Penguin!\n");
}
编译 gcc -o hello hello.c
得到了hello的可执行文件。
-rwxr-xr-x 1 root root 16424 2月 4 14:11 hello
-rw-r--r-- 1 root root 61 2月 4 14:10 hello.c
原来的C文件只有61字节,编译后的可执行文件变成了16K左右,变大了很多。为了弄明白这个二进制文件的结构,我们需要补充一些相关知识, symbols, sections, and segments。
symbols are like function names, and are used to answer “If I call printf and it’s defined somewhere else, how do I find it?”
symbols are organized into sections – code lives in one section (.text), and data in another (.data, .rodata)
sections are organized into segments
---
译文
符号表像函数名,当调用其它地方定义的函数(如上面的printf)时,需要直到可以怎么定位到它。
符号表是按区组织的,代码位于一个区(.text), 数据位于另外的区(.data, .rodata)。
区是按段组织的。
直接使用文本编辑器打开
直接用文本编辑器打开,除了很少一部分可以看得出的文本字符,其它尽是乱码。其中的ELF是文件格式的名称。
cat hello | head -c 3000
ELF>`@@▒8@8
@@@@@@h▒▒@▒@@@PP@@▒▒ @ @@@.>@>@ (.(>@(>@▒▒▒@▒@DDP▒td @ @44Q▒tdR▒td.>@>@▒▒/lib64/ld-linux-x86-64.so.2GNUס▒&▒▒6y▒J▒U▒▒:GNU
. libc.so.6puts__libc_start_mainGLIBC_2.2.5__gmon_start__ui "▒?@@@ @@(@@
使用readelf来看符号表
readelf --symbols hello
Num: Value Size Type Bind Vis Ndx Name
48: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
59: 0000000000400410 0 FUNC GLOBAL DEFAULT 13 _start
61: 00000000004004f4 16 FUNC GLOBAL DEFAULT 13 main
在这里,我们看到了三个符号:main是main()函数的地址。puts看起来是对printf函数的引用,猜想应该是编译器为了优化将其更改为puts?。_start非常重要。当程序开始运行时,你可能认为它从main开始执行。但实际上它会转到_start。_start会执行一些非常重要的操作,其中包括调用main函数。
什么是符号
当你编写程序时,你可能会编写一个名为hello的函数。当你编译程序时,该函数的二进制代码将被标记为一个名为hello的符号。如果我从库中调用一个函数(比如printf),我们需要一种查找该函数代码的方法!从库中查找函数的过程称为链接。它可以在我们编译程序后进行(“静态链接”),也可以在我们运行程序时进行(“动态链接”)。
所以符号是使链接工作的关键!让我们来找一下printf的符号!它应该在libc库中,那里存放着所有的C标准库函数。
如果我在我的libc副本上运行nm命令,它会告诉我“没有符号”。但是互联网告诉我可以使用objdump -tT命令!这个命令有效!objdump -tT /lib/libc-2.17.so给我输出了如下内容。
objdump -tT /lib/libc-2.17.so | head -n 20
/lib/libc-2.17.so: 文件格式 elf32-i386
SYMBOL TABLE:
00000174 l d .note.gnu.build-id 00000000 .note.gnu.build-id
00000198 l d .note.ABI-tag 00000000 .note.ABI-tag
000001b8 l d .gnu.hash 00000000 .gnu.hash
00003f10 l d .dynsym 00000000 .dynsym
0000d590 l d .dynstr 00000000 .dynstr
0001347a l d .gnu.version 00000000 .gnu.version
0001474c l d .gnu.version_d 00000000 .gnu.version_d
00014bb4 l d .gnu.version_r 00000000 .gnu.version_r
00014bf4 l d .rel.dyn 00000000 .rel.dyn
00017634 l d .rel.plt 00000000 .rel.plt
00017680 l d .plt 00000000 .plt
00017720 l d .plt.got 00000000 .plt.got
00017730 l d .text 00000000 .text
00167260 l d __libc_freeres_fn 00000000 __libc_freeres_fn
00168f90 l d __libc_thread_freeres_fn 00000000 __libc_thread_freeres_fn
001692a0 l d .rodata 00000000 .rodata
......
如果你仔细观察,你会看到sprintf、strlen、fork、exec等你可能期望libc具有的函数。从这里,我们可以开始想象动态链接是如何工作的 - 我们看到hello调用了puts,然后我们可以在libc的符号表中查找puts的位置。
使用objdump查看二进制文件,并了解分区
objdump -s hello
Contents of section .text:
400410 31ed4989 d15e4889 e24883e4 f0505449 1.I..^H..H...PTI
400420 c7c0a005 400048c7 c1100540 0048c7c7 ....@.H....@.H..
400430 f4044000 e8c7ffff fff49090 4883ec08 ..@.........H...
Contents of section .interp:
400238 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
400248 7838362d 36342e73 6f2e3200 x86-64.so.2.
Contents of section .rodata:
4005f8 01000200 50656e67 75696e21 00 ....Penguin!.
.text is the program’s actual code (the assembly). _start and main are both part of the .text section.
.rodata is where some read-only data is stored (in this case, our string “Penguin!”)
.interp is the filename of the dynamic linker!
分区和段之间的主要区别在于部分在链接时(由ld)使用,而段在执行时使用。objdump向我们显示了部分的内容,这很好,但是没有给我们足够关于部分的元数据,这让我感到遗憾。让我们尝试使用readelf代替:
readelf --sections hello
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[13] .text PROGBITS 0000000000400410 00000410
00000000000001d8 0000000000000000 AX 0 0 16
[15] .rodata PROGBITS 00000000004005f8 000005f8
000000000000000b 0000000000000000 A 0 0 4
[24] .data PROGBITS 0000000000601010 00001010
0000000000000010 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000601020 00001020
0000000000000010 0000000000000000 WA 0 0 8
[26] .comment PROGBITS 0000000000000000 00001020
000000000000002a 0000000000000001 MS 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)
Neat! We can see .text is executable and read-only, .rodata (“read only data”) is read-only, and .data is read-write.
查看下汇编代码
在上面的 .text 区,我们看到的二进制的数字,比如下面的十六进制表示,这些表示什么?下面我们反编译看下汇编代码,
Contents of section .text:
400410 31ed4989 d15e4889 e24883e4 f0505449 1.I..^H..H...PTI
400420 c7c0a005 400048c7 c1100540 0048c7c7 ....@.H....@.H..
400430 f4044000 e8c7ffff fff49090 4883ec08 ..@.........H...
objdump -d ./hello
Disassembly of section .text:
0000000000400410 <_start>:
400410: 31 ed xor %ebp,%ebp
400412: 49 89 d1 mov %rdx,%r9
400415: 5e pop %rsi
400416: 48 89 e2 mov %rsp,%rdx
400419: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
So we see that 31 ed is xoring two things. Neat! That’s all the assembly we’ll do for now.
段
最后,一个程序组织入段或者程序头,下面使用 readelf --segments hello
来看看我们程序的段信息。
Program Headers:
[... removed ...]
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000006d4 0x00000000000006d4 R E 200000
LOAD 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001f8 0x0000000000000208 RW 200000
[... removed ...]
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym
.dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt
.text .fini .rodata .eh_frame_hdr .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .ctors .dtors .jcr .dynamic .got
段是用来指定如何将程序的各个部分如何放入到内存中,第一个LOAD段被标注为可读可执行,第二个为可读写。 .text is in the first segment (we want to read it but never write to it), and .data, .bss are in the second (we need to write to them, but not execute them).
可执行文件并不是什么魔法. ELF 像其它文件格式一样! 你可以使用 readelf
, nm
, 和 objdump
等工具来对linux二进制文件一探究竟
本文主要参考并译自 Julia Evans 的博文。
How is a binary executable organized? Let’s explore it!