工欲善其事,必先利其器

了解计算机程序的运行原理和底层细节,对于程序员来说十分重要。毕竟根基不稳,大厦不牢。

而我们在学习这些内容时,如果有得心应手的工具帮我们披荆斩棘,那么将会使我们事半功倍。

其中 readelf 和 objdump 就是两把有力的瑞士军刀。

img

体验

我们通过一个小实例,来实际体验一下 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选项载入。
  • 通过 r9 传递 rdx
  • 通过 r8 传递 __libc_start_main
  • 通过 rcx 传递 __libc_csu_init
  • 通过 rdi 传递 main
  • 通过 rsi 传递 argc
  • 通过 rdx 传递 argv
  • 通过 栈 传递 rsp 的值

地址为 0x1081 开始的指令,为 lea 指令,将立即数 0x00c1 赋值给寄存器 rip;

地址为 0x1088 开始的指令,为 callq 指令,只有一个参数 rip = 0xc1,意思是调用相对于下一条指令的偏移量为 0xc1 的指令。其中下一条的指令地址为

参考

readelf nm objdump 命令详解

X86-64和ARM64用户栈的结构 (3) —_start到__libc_start_main

X86-64和ARM64用户栈的结构 (3) —_start到__libc_start_main_大企鹅的技术博客_51CTO博客 ★★★