撬开程序底层的两把瑞士军刀
Posted Li-Yongjun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了撬开程序底层的两把瑞士军刀相关的知识,希望对你有一定的参考价值。
工欲善其事,必先利其器
了解计算机程序的运行原理和底层细节,对于程序员来说十分重要。毕竟根基不稳,大厦不牢。
我们在学习这些内容的时候,如果有得心应手的工具帮我们披荆斩棘,那么将会事半功倍。
readelf 和 objdump 就是其中两把有力的瑞士军刀。
体验
我们通过一个小实例,来实际体验一下 readelf、objdump 所能够达到的效果。
源码
main.c
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hello world!\\n");
return 0;
}
编译、运行
$ gcc main.c -o main.out
$ ./main.out
hello world!
readelf
上面是一个最简单的 hello world 程序,最终运行输出 hello world。我们来看一下这个程序为什么能够输出 hello world。
这时候就需要用到 readelf 命令
$ readelf -hs main.out
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x1060
程序头起点: 64 (bytes into file)
Start of section headers: 14712 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
...
Symbol table '.symtab' contains 65 entries:
...
__libc_start_main@@GLIBC_
53: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start
54: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
55: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
56: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used
57: 0000000000001170 101 FUNC GLOBAL DEFAULT 16 __libc_csu_init
58: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 _end
59: 0000000000001060 47 FUNC GLOBAL DEFAULT 16 _start
60: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
61: 0000000000001149 38 FUNC GLOBAL DEFAULT 16 main
62: 0000000000004010 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
63: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
64: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
-h
--file-header 显示 elf 文件头信息
-s
--syms
--symbols 显示符号表中的项
可以看到,ELF 头中描述到,程序入口点地址为 0x1060。我们在符号表中找到地址 0x1060 对应的符号为 _start 函数。这里简单介绍下 _start,后面我有计划详细讲解。
Linux 系统下,一般程序的入口是 _start,这个函数是 Linux 系统库(Glibc)的一部分。当我们的程序与 Glibc 库链接在一起形成最终可执行文件以后,这个函数就是程序的初始化部分的入口,程序初始化部分完成一系列初始化过程之后,会调用 main 函数来执行程序的主体。在 main 函数执行完成以后,返回到初始化部分,它进行一些清理工作,然后结束进程。
——《程序员的自我修养——链接、装载与库》
我们知道了 _start 会调 main 函数,这只是从书上看到的理论知识,我们自己怎么去求证呢?这个时候 objdump 命令该登场了。
objdump
$ objdump -Sd main.out
main.out: 文件格式 elf64-x86-64
...
Disassembly of section .text:
0000000000001060 <_start>:
1060: f3 0f 1e fa endbr64
1064: 31 ed xor %ebp,%ebp
1066: 49 89 d1 mov %rdx,%r9
1069: 5e pop %rsi
106a: 48 89 e2 mov %rsp,%rdx
106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1071: 50 push %rax
1072: 54 push %rsp
1073: 4c 8d 05 66 01 00 00 lea 0x166(%rip),%r8 # 11e0 <__libc_csu_fini>
107a: 48 8d 0d ef 00 00 00 lea 0xef(%rip),%rcx # 1170 <__libc_csu_init>
1081: 48 8d 3d c1 00 00 00 lea 0xc1(%rip),%rdi # 1149 <main>
1088: ff 15 52 2f 00 00 callq *0x2f52(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5>
108e: f4 hlt
108f: 90 nop
0000000000001090 <deregister_tm_clones>:
1090: 48 8d 3d 79 2f 00 00 lea 0x2f79(%rip),%rdi # 4010 <__TMC_END__>
1097: 48 8d 05 72 2f 00 00 lea 0x2f72(%rip),%rax # 4010 <__TMC_END__>
109e: 48 39 f8 cmp %rdi,%rax
10a1: 74 15 je 10b8 <deregister_tm_clones+0x28>
10a3: 48 8b 05 2e 2f 00 00 mov 0x2f2e(%rip),%rax # 3fd8 <_ITM_deregisterTMCloneTable>
10aa: 48 85 c0 test %rax,%rax
10ad: 74 09 je 10b8 <deregister_tm_clones+0x28>
10af: ff e0 jmpq *%rax
10b1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
10b8: c3 retq
10b9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
...
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 83 ec 10 sub $0x10,%rsp
1155: 89 7d fc mov %edi,-0x4(%rbp)
1158: 48 89 75 f0 mov %rsi,-0x10(%rbp)
115c: 48 8d 3d a1 0e 00 00 lea 0xea1(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1163: e8 e8 fe ff ff callq 1050 <puts@plt>
1168: b8 00 00 00 00 mov $0x0,%eax
116d: c9 leaveq
116e: c3 retq
116f: 90 nop
...
-S 尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。
-d 查看每个段的汇编
我们可以看到:
_start 函数其实也就做了一件事情,就是调用 __libc_start_main 函数,并向其传递参数:main、argc、argv 等。
__libc_start_main 最终调用 main 函数。
收益
在上述实例的分析中,我们用到了 readelf、objdump 命令,并简单介绍了几个参数,有了这些命令和参数的帮助,我们能够顺风顺水地分析程序底层的原理,从而能够更加深刻地理解程序编译、运行的过程,可执行文件的组成等底层原理,这对我们学习计算机很有帮助,甚至能够进一步影响到我们的上层代码。
详细参数
下面我们再列举一下它们的详细参数。
readelf
用于显示 elf 格式文件的信息。
这个程序和 objdump 提供的功能类似,但是它显示的信息更为具体
-a
--all 显示全部信息,等价于 -h -l -S -s -r -d -V -A -I.
-h
--file-header 显示elf文件开始的文件头信息.
-l
--program-headers
--segments 显示程序头(段头)信息(如果有的话)。
-S
--section-headers
--sections 显示节头信息(如果有的话)。
-g
--section-groups 显示节组信息(如果有的话)。
-t
--section-details 显示节的详细信息(-S的)。
-s
--syms
--symbols 显示符号表段中的项(如果有的话)。
-e
--headers 显示全部头信息,等价于: -h -l -S
-n
--notes 显示note段(内核注释)的信息。
-r
--relocs 显示可重定位段的信息。
-u
--unwind 显示unwind段信息。当前只支持IA64 ELF的unwind段信息。
-d
--dynamic 显示动态段的信息。
-V
--version-info 显示版本段的信息。
-A
--arch-specific 显示CPU构架信息。
-D
--use-dynamic 使用动态段中的符号表显示符号,而不是使用符号段。
-x <number or name>
--hex-dump=<number or name> 以16进制方式显示指定段内内容。number指定段表中段的索引,或字符串指定文件中的段名。
-w[liaprmfFsoR] or
--debug-dump[=line,=info,=abbrev,=pubnames,=aranges,=macro,=frames,=frames-interp,=str,=loc,=Ranges] 显示调试段中指定的内容。
-I
--histogram 显示符号的时候,显示bucket list长度的柱状图。
-v
--version 显示readelf的版本信息。
-H
--help 显示readelf所支持的命令行选项。
-W
--wide 宽行输出。
@file 可以将选项集中到一个文件中,然后使用这个@file选项载入。
objdump
显示二进制文件信息
objdump命令是用来查看目标文件或者可执行的目标文件的构成的 gcc 工具。
--archive-headers
-a
显示档案库的成员信息,类似ls -l将lib*.a的信息列出。
-b bfdname
--target=bfdname
指定目标码格式。这不是必须的,objdump能自动识别许多格式,比如:
objdump -b oasys -m vax -h fu.o
显示fu.o的头部摘要信息,明确指出该文件是Vax系统下用Oasys编译器生成的目标文件。objdump -i将给出这里可以指定的目标码格式列表。
-C
--demangle
将底层的符号名解码成用户级名字,除了去掉所开头的下划线之外,还使得C++函数名以可理解的方式显示出来。
--debugging
-g
显示调试信息。企图解析保存在文件中的调试信息并以C语言的语法显示出来。仅仅支持某些类型的调试信息。有些其他的格式被readelf -w支持。
-e
--debugging-tags
类似-g选项,但是生成的信息是和ctags工具相兼容的格式。
--disassemble
-d
从objfile中反汇编那些特定指令机器码的section。
-D
--disassemble-all
与 -d 类似,但反汇编所有section.
--prefix-addresses
反汇编的时候,显示每一行的完整地址。这是一种比较老的反汇编格式。
-EB
-EL
--endian={big|little}
指定目标文件的小端。这个项将影响反汇编出来的指令。在反汇编的文件没描述小端信息的时候用。例如S-records.
-f
--file-headers
显示objfile中每个文件的整体头部摘要信息。
-h
--section-headers
--headers
显示目标文件各个section的头部摘要信息。
-H
--help
简短的帮助信息。
-i
--info
显示对于 -b 或者 -m 选项可用的架构和目标格式列表。
-j name
--section=name
仅仅显示指定名称为name的section的信息
-l
--line-numbers
用文件名和行号标注相应的目标代码,仅仅和-d、-D或者-r一起使用使用-ld和使用-d的区别不是很大,在源码级调试的时候有用,要求编译时使用了-g之类的调试编译选项。
-m machine
--architecture=machine
指定反汇编目标文件时使用的架构,当待反汇编文件本身没描述架构信息的时候(比如S-records),这个选项很有用。可以用-i选项列出这里能够指定的架构.
--reloc
-r
显示文件的重定位入口。如果和-d或者-D一起使用,重定位部分以反汇编后的格式显示出来。
--dynamic-reloc
-R
显示文件的动态重定位入口,仅仅对于动态目标文件意义,比如某些共享库。
-s
--full-contents
显示指定section的完整内容。默认所有的非空section都会被显示。
-S
--source
尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。
--show-raw-insn
反汇编的时候,显示每条汇编指令对应的机器码,如不指定--prefix-addresses,这将是缺省选项。
--no-show-raw-insn
反汇编时,不显示汇编指令的机器码,如不指定--prefix-addresses,这将是缺省选项。
--start-address=address
从指定地址开始显示数据,该选项影响-d、-r和-s选项的输出。
--stop-address=address
显示数据直到指定地址为止,该项影响-d、-r和-s选项的输出。
-t
--syms
显示文件的符号表入口。类似于nm -s提供的信息
-T
--dynamic-syms
显示文件的动态符号表入口,仅仅对动态目标文件意义,比如某些共享库。它显示的信息类似于 nm -D|--dynamic 显示的信息。
-V
--version
版本信息
--all-headers
-x
显示所可用的头信息,包括符号表、重定位入口。-x 等价于-a -f -h -r -t 同时指定。
-z
--disassemble-zeroes
一般反汇编输出将省略大块的零,该选项使得这些零块也被反汇编。
@file 可以将选项集中到一个文件中,然后使用这个@file选项载入。
结束语
底子打得牢,就算一只脚也能站得稳
底子打得不牢,呐
以上是关于撬开程序底层的两把瑞士军刀的主要内容,如果未能解决你的问题,请参考以下文章