欢迎访问我的个人博客: luomuxiaoxiao.com
在 如何编译目标文件中我们了解到汇编文件经过编译器之后生成了可重定位文件,并且在 可重定位文件详解中我们以一个最简单的
'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指令会有区别。
想第一时间查看我的文章吗?请关注我的微信公众号号,搜索“落木萧萧技术论坛”或登陆我的个人博客:www.luomuxiaoxiao.com,更多精彩文章等你。
![qrcode](https://i-blog.csdnimg.cn/blog_migrate/18761891c55b2bfa97548506179f7601.jpeg)