[toc]
在如何编译目标文件中我们了解到汇编文件经过编译器之后生成了可重定位文件,并且在可重定位文件详解中我们以一个最简单的'hello, wolrd'
程序为例,分析了可重定位文件的详细内容。这篇文章中我们主要分析一下汇编文件和可重定位文件代码段的关系,以及从汇编文件生成可重定位文件的详细过程。
我们还是以如何编译目标文件中的hello.c
来研究。
一、 由汇编器生成的汇编代码
首先我们先查看一下经过汇编之后转换成的汇编代码:
.file "hello.c"
.section .rodata
.LC0:
.string "hello, world"
.text
.globl main
.type main, @function
main:
.LFB0:
pushq %rbp
movq %rsp, %rbp
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
ret
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
为了方便阅读代码中我们省略了CFI信息,关于CFI详细信息,请参考参考阅读[1]
二、 查看可重定位文件的内容
2.1 可重定位文件的实际内容
由于可重定位文件属于二进制文件。在linux机器上,可以使用hexdump
命令来查看二进制文件的内容。
$ hexdump -C hello.o
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 01 00 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 |..>.............|
00000020 00 00 00 00 00 00 00 00 a0 02 00 00 00 00 00 00 |................|
00000030 00 00 00 00 40 00 00 00 00 00 40 00 0d 00 0a 00 |....@.....@.....|
00000040 55 48 89 e5 bf 00 00 00 00 e8 00 00 00 00 b8 00 |UH..............|
00000050 00 00 00 5d c3 68 65 6c 6c 6f 2c 20 77 6f 72 6c |...].hello, worl|
00000060 64 00 00 47 43 43 3a 20 28 55 62 75 6e 74 75 20 |d..GCC: (Ubuntu |
00000070 35 2e 34 2e 30 2d 36 75 62 75 6e 74 75 31 7e 31 |5.4.0-6ubuntu1~1|
00000080 36 2e 30 34 2e 31 30 29 20 35 2e 34 2e 30 20 32 |6.04.10) 5.4.0 2|
00000090 30 31 36 30 36 30 39 00 14 00 00 00 00 00 00 00 |0160609.........|
... snip ...
2.2 反汇编可重定位的代码段
hello.o
是一个二进制文件,显然,代码段也被编译成了一系列的二进制数字,而且这些数字就被包含于上面hexdump
输出的数字里,那么如何找到他们呢?
我们知道ELF文件中,Section Header Table
指定了各个section
的起始地址,长度等信息,那么,我们查看hello.o
的信息得到以下内容:
$ readelf -S hello.o
There are 13 section headers, starting at offset 0x2a0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000015 0000000000000000 AX 0 0 1
... snip ...
上述信息我们知道.text
(代码段)起始于ELF文件首地址偏移00000040
处,长度为0x15
个字节。也即0x40 ~ 0x54
之间都为代码段部分,结合hexdump
输出的信息,即以下内容:
00000040 55 48 89 e5 bf 00 00 00 00 e8 00 00 00 00 b8 00 |UH..............|
00000050 00 00 00 5d c3 |...]
这些二进制数据显然不那么的友好,我们可以使用objdump
命令来反编译一下。objdump
工具用来显示二进制文件的信息,就是以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。-d
参数表示反汇编ELF文件中可执行的代码(代码段)部分。
$ objdump -d hello.o
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e <main+0xe>
e: b8 00 00 00 00 mov $0x0,%eax
13: 5d pop %rbp
14: c3 retq
输出内容中最左侧依然是字节序列的序号,中间部分表示ELF文件内保存的字节序列,最右侧是反汇编出来的汇编代码。我们注意到,反编译出的字节序列正好就是根据hexdump
命令和section header table
推算出来的字节序列。
仔细观察发现,objdump反汇编出来的代码中除了省略掉了.cfi_*
相关的信息,和原来的汇编文件的代码部分唯一的差异有两条指令。摘录如下:
# 汇编器生成的汇编代码:
movl $.LC0, %edi
call puts
# 反编译出来的汇编代码:
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e <main+0xe>
首先,我们分析第二条语句,结合C代码这行汇编代码对应的是printf("hello, world\n");
。这里我们不用在意为什么C代码中调用的是printf函数,而到汇编语言中却变成了puts函数(实际上是编译器做了优化)。我们主要讨论为什么由汇编器生成的call指令和反编译出来的call指令会有区别。
实际上,由汇编器生成的call指令,仅仅是将函数名替代进来了而已。而反编译出来的call指令后面的值(本例中'callq e'
中的'e'
)其实只是个占位符,并无实际意义。当一个模块(本例中的hello.o
)调用一个外部的函数(本例中printf
,printf
并未在hello.c
中定义)时,那么编译器会生成一条call
指令,指令的操作数是一个占位符,而与此同时生成一条重定位条目(relocation entry
)放在.rel.text
section中,类似的,当模块调用一个外部变量时,编译器会生成一条重定位条目放在.rel.data
section中。我们可以在可重定位文件详解 2.2.2的第9条中看到put
对应的重定位条目。
其次,我们再看第一条语句,在.s文件中这条语句的意思是将.LC0的地址复制到%edi中去。结合代码,我们知道.LC0保存的是”hello, world\n”字符串的地址,将其复制到edi中去。根据x86_64体系结构,在调用某个函数时,其第一个参数、第二个参数、第三个参数、第四个参数应该依次保存到%edi、%esi、%edx、%ecx中。 那么这条语句在编译成.o文件反汇编出来后,对应参数地址为什么变成0了呢?
实际上,同上面call指令一样,0在此处也是一个占位符。同时,在.rel.text
中也生成了一条重定位条目。
三、总结
我们再回过头来总结一下,从汇编语言的.s
文件到二进制的.o
文件,是怎么生成的。
.s
文件中包含外部函数的汇编指令,是用操作符加函数名来表示的,经过编译之后,生成的.o
文件中,对应指令中的函数名被占位符所代替;-
.s
文件中包含全局变量的汇编指令,是用操作符加变量名来表示的,经过编译之后,生成的.o
文件中,对应指令中的变量名被转换成了由RIP寄存器表示的位置无关码,其中,加在RIP上的偏移量也是一个占位符; -
.s
文件中包含常量的汇编指令,是用操作符加变量名来表示的,经过编译之后,生成的.o
文件中,对应指令中的常量名被转换成了占位符; -
.s
文件中其他指令被保留下来生成了.o
文件的.text
段中; -
编译器为上述的每个外部函数生成了对应的重定位条目,放到了
.o
文件的.rel.text
中,为上述的每个全局变量生成了对应的重定位条目,放到了.o
文件的.rel.data
中; -
.s
文件中已初始化的全局变量和静态变量被保存在了.o
文件的.data
段中; -
原来
c文件
中的未初始化的全局变量和静态变量,初始化为0的全局变量和静态变量被保存到了.o
文件的.bss
段中; -
.s
文件中的只读数据生成了.o
文件的.rodata
段; -
编译器信息以
ASCII
字符的形式保存到了.o
文件的.comment
段中; -
.s
文件中所有的CFI
相关的信息被保存到了.eh_frame
段中,并且对.eh_frame
段生成了对应的重定位信息,保存到了.rela.eh_frame
中; -
.s
文件中文件名,全局变量和外部函数的名字被保存到了.strtab
段中; -
在由
.s
文件生成.o
文件时,生成的各个段的名字被保存到了.shstrtab
中。
综上所述,我们用一个更直观的gif图片来反映从汇编到.o
文件的过程:
点击此处查看原图