背景:
最近在看Linux手册,记录一下刚刚学习的一些GNU binutils工具包里面的一些有用的命令,例如有objdump、nm、readelf等,以备后续要用到却忘记如何使用了,所以记录一下。
一、源码文件
首先看一下源码文件,非常的简单。
a.c
b.c
main.c
Makefile
执行命令:
# make clean
# make
二、基础复习
1. 大小端
我们查看一下b.o里面保存的内容
-s 查看目标文件的所有段的全部内容
100的十六进制是64, 200的十六进制是C8,那为什么.data里面存储的是 C8000000和64000000,这是大小端(Big-endian和Little-endian)的问题了,先总结一下这是小端,小值放在前面是小端,大值放在前面是大端。 小值是指0x64和0xC8,前面是指这串数字的最左侧,也就是低位的地址,大值在这里面是0x00。
MSB被保存到低地址,LSB被保存到高地址,在传输的时候 先传输MSB后传输LSB,这是大端
MSB被保存到高地址,LSB被保存到低地址,在传输的时候 先传输LSB后传输MSB,这是小端。
我们经常使用的x86和ARM为小端模式,TCP/IP和Java虚拟机的字节为大端模式。
我们在网络传输的时候,先读取低地址MSB的0x12,再依次传输0x34,...,最后传输0x78,这是大端序,务必注意字节序。
同上,我们的int类型是32位,4个字节,是i386小端模式
100也就是 0x 0000 0064
200也就是 0x 0000 00c8
0x64是一个字节,0xc8是一个字节,所以如上图所示需要先传LSB放到低地址,然后再写高地址, 6400 0000 和 c800 0000
我们再次做一次试验,编写了一个C语言的源代码,x是十进制表示的0x12345678,而y是十六进制,我们进行编译并使用objdump去查看:
例如:0x12345678 ,一个十六进制的数字,MSB是最重要的位或字节,也就是0x12,即所说的大值; LSB是最不重要的位或字节,也就是0x78,即所说的小值
我们发现他表现的确实是我们之前所推断的,都是小端,从低地址到高地址依次传78563412,这x和y这两个变量是一样的。
2. 进制转换
默认都是十进制,ibase代表输入的进制,obase代表输出的进制,然后是数值
将十进制的100转为2进制
echo "obase=2;100" | bc
echo "obase=16; ibase=2; 11111111" | bc
将十六进制值转换为二进制:
将十六进制值转换为十进制:
将十进制值305419896 转换为十六进制:
还有一种进制转换的方式是通过gdb:
(gdb)p/x 100 // 表示将100转换为十六进制并打印出来,p代表print,x代表十六进制
(gdb)p/x 200 // 同上
(gdb)p/t 0x12345678 // 将十六进制转换为二进制,t代表二进制
(gdb)p 0x12345678 // 默认是将十六进制转换为十进制
3. 目标文件的分析
问题:什么是目标文件? 什么是可执行文件? 他们的存储格式和文件有什么区别?
一般对 源代码通过编译器进行编译后生成的文件叫做目标文件,且未进行链接的那些中间文件(Windows的.obj和Linux的.o),而可执行文件格式(Executable)主要是Windows下的PE(portable executable)和Linux的ELF(Executable Linkable Format),他们都是COFF(Common file format)格式的变种。
目标文件和可执行文件的文件格式都采用一种格式存储,从广义上看,目标文件和可执行文件的格式几乎是一样的,是同一类文件,只是目标文件没经过链接有些符号或地址没有被调整。而动态链接库(.dll和.so)、静态链接库(.lib和.a)和核心转储文件(Core Dump File)也都是按照可执行文件进行存储。
这种文件的分析主要由两个工具,一个是objdump,另一个是readelf。 一般使用readelf查看头部和各个段的相关信息,而使用objdump查看各个段的反编译信息。
三、readelf命令(重要)
readelf常用的六个选项(-h,-S,-l,-a,-x,-p)
3.1 readelf -h 查看ELF Header
查看elf格式的目标文件的header的基本信息
关于EFL header的详细信息可参考 我记不住的ELF文件组成和分析。
3.2 readelf -S --wide 查看段表
段表(Section Header Tabel)保存了这些段的基本属性的结构,是除了文件头以外最重要的结果,它描述了ELF各个段的信息,比如每个段的段名、长度、偏移、读写权限及段的属性。
段表是一个数组,数组中的每一个元素是一个Elf32_Shdr/Elf64_Shdr的结构体,又称为“段描述符”,用于描述每个段的信息。
gcc在选项-g 会增加很多调试的段一般以debug开头,会占用文件大量的空间,如果是发布程序的时候可以直接将-g去掉重新编译 或 把调试信息去掉可以使用strip命令进行去掉。
使用选项-h显示出的Start of section headers表示 段表在文件中的偏移,也就是段表从文件的第多少个字节开始。可以使用 --wide 将各列依次展开。
段表是一个数组,每一个元素是一个Elf32_Shdr/Elf64_Shdr的结构体,又称为“段描述符”,用于描述每个段的信息,下面是 段描述符的32/64位的数据结构:
.strtab 是字符串表(string table),它保存的是普通的字符串,比如符号的名字。
.shstrtab是 段表字符串表(section header string table),它保存的是段表中用到的字符串,最常见的就是段名.
3.3 readelf -l 查看程序头部表或段
也可以使用 --wide 选项将列展开显示,这样效果更好一些。
这个选项 显示了程序头表PHT的相关信息以及从section到segment的映射关系。
3.4 readelf -a 查看ELF文件的全部内容
$ readelf -a a.out
显示所有全部的相关信息,一般比较长,可以使用less进行查看
即 readelf -a a.out | less
这里省略了相关图片,不再赘述。
3.5 readelf -x/-p 以十六进制或字符串的格式进行查看内容
我们先看一下section相关信息,如下图:
用法:readelf -x <section_name|section_index> <elf_binary>
12 代表上面的序号,也可以写 .strtab
用法:readelf -p <section_name|section_index> <elf_binary>
四、objdump命令(重要)
4.1 objdump -h a.o 汇总信息
-h 显示目标文件各段的汇总信息,这个选项无法看到ELF文件header,这个选项只是把ELF文件中的关键的段显示出来,而省略了其他的辅助性的段,例如符号表、字符串表、段名字符串表、重定位表等,可以使用readelf。 可以使用 --wide来进行显示这样更清晰
objdump -h --wide a.o
CONTENTS表示该段在文件中存在,即.text/.data/.comment/.note.GNU-stack/.eh_frame存在于这个a.o文件中。
.text 代码段 :程序源代码编译后的机器指令被放在这个段里,里面保存了机器代码;
.data 数据段 :全局变量和局部静态变量数据被放在这个段里,如上图所示,说明a.o本身没有全局变量和局部静态变量size=0, 但是CONTENTS表示这个段在文件中是存在的;
.bss(block Started by Symbol) : 未初始化的全局变量和局部静态变量被放在这个段里,未初始化的全局变量和局部静态变量都为0,这个段存在的意义在于 只是为未初始化的全局变量和局部静态变量预留位置而已,它本身没有内容也不占据文件的空间,如上所所示,第2个bss, size为0, 也没有CONTENTS表示不存在;
.note.GNU-stack堆栈提示段 :虽然有CONTENTS但是它的长度是0.
objdump -h main.o
.rodata(read only data) 只读数据段,它存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量,例如: printf("%d + %d = %d\n",i); 里面的"%d + %d=%d\n"则是一个字符串常量。有时候不同的编译器也会把这些字符串常量放在.data段。
也可以使用size命令查看各个段的大小:
dec代表十进制的大小,hex代表十六进制的大小
4.2 objdump -s a.o 查看十六进制的内容
-s 将所有段的内容以十六进制打印出来,这个选项用于查看内容
我们可以数一下,最左侧是偏移量,第二列 8d0437c3是十六进制表示,它占用了4个字节,和上一个指令objdump -h的各段的size相对应,例如 .comment段,16+15=31个字节,与objdump -h显示size为0x1f是一样的。最后一列是 各段的ASCII码,而.text的ASCII码是没有意义的,我们需要使用-d来反编译这个段。
4.3 objdump -d a.o 反编译
使用-d或-s -d将所有包含指令.text段进行反编译,如果要对其他的section进行反编译请使用-D
正好可以看到 foo是这个函数的标号, 8d 04 37 占据了3个字节,里面写的是lea (%rdi,%rsi,1),%eax , c3占据是一个字节,里面是ret
对 b.c中的变量进行反编译,因为b.c的变量被放入了.data段,不存在.text段,所以需要使用-D来进行反编译:
objdump -D b.o 显示所有的段sections的汇编文件的内容,一般输出的信息较多,我们可以使用-j来选择某个section进行反编译。
也可以指定某个section进行反编译,例如:
objdump -j .text -d a.o // 一般对.text里面的机器码进行反编译,因为这个段有代码,而对一些像comment字段进行反编译一般没有什么用
4.4 objdump -x 显示所有的头的信息包括符号表和重定向入口
显示可用的头信息,包括符号表和重定向的入口,等价于 -a -f -h -p -r -t
objdump -x main.o
4.5 objdump -f 显示文件头的信息
4.6 objdump -r main.o查看重定向表
或是使用 readelf -a main.o查看所有的信息。
这个重定向表 同时也是ELF的一个段
.rela.text 对 .text进行重定向的段,这个段里面保存的就是对.text中各个变量进行重定向的表
.rela.data 对.data进行重定向的段,这个段里面保存的就是对.data中各个变量进行重定向的表
objdump -r a.o
这个命令用于查看a.o里面要重定位的地方,即a.o所有引用到外部符号的地址,每个要被重定位的地方叫一个重定位入口(Relocation Entry),重定位入口的偏移(offset)表示该入口在要被重定位的段中的位置,
我们可以看到main.c中有7个地方需要重定向,也就是需要进行修改,所需要修改的位置在重定向的表中,第一个是0x06,第二个是0x0c,第三个是0x11,第四个是0x17,第五个是0x1d,第六个是0x22,第七个是0x2f ,分别对应上面的红框的偏移。
而重定向的表最后一列的value
也就是重定向表 本质上是一个map,它的key是在哪里进行替换,value是所替换的值是什么。
这里的<y>的地址是 0x4a50d0 ,nextPC为0x40151a,偏移量为0xa3bb6
也就是在0x401510+0x06 = 0x401516这个位置,长度为4个字节,写入的值为 0xa3bb6
我们objdump -d a.out 查看一下
0x4a50d0 - 0x40151a = 0xa3bb6
0x4a50d4 - 0x401520 = 0xa3bb4
0x401680 - 0x401525 = 0x015b
0x4a50d0 - 0x40152b = 0xa3ba5
0x4a50d4 - 0x401531 = 0xa3ba3
0x409ed0 - 0x401543 = 0x898d
4.7 objdump -t 显示符号表
五、nm命令
nm是names的简写,功能与objdump -t相似,即打印出来一个文件的符号表,所谓的符号其实就是函数和变量,他们的名称称为符号名,相应的列表称为符号表。但是objdump和nm的输出格式不一样。
nm命令,从目标文件中列出各个符号的相关信息,可以列出当前的地址和所在段segment
#include <stdio.h> /* printf */
int g_initialized1 = 10; /* DATA segment */
int g_initialized2 = 12; /* DATA segment */
int g_initialized3 = 14; /* DATA segment */
int g_uninitialized1; /* BSS segment */
int g_uninitialized2; /* BSS segment */
int g_uninitialized3; /* BSS segment */
int main(void) /* CODE segment */
{
int local_variable = 5; /* STACK segment */
static int local_uninit_static; /* BSS segment */
static int local_init_static = 10; /* DATA segment */
printf("Uninitialized1 global data is in the BSS segment = %p\n", &g_uninitialized1);
printf("Uninitialized2 global data is in the BSS segment = %p\n", &g_uninitialized2);
printf("Uninitialized3 global data is in the BSS segment = %p\n", &g_uninitialized3);
printf(" Initialized1 global data is in the DATA segment = %p\n", &g_initialized1);
printf(" Initialized2 global data is in the DATA segment = %p\n", &g_initialized2);
printf(" Initialized3 global data is in the DATA segment = %p\n", &g_initialized3);
printf(" Code for main is in the CODE segment = %p\n", &main);
printf(" Code for printf is in the CODE segment = %p\n", &printf);
printf(" Code for scanf is in the CODE segment = %p\n", &scanf);
printf(" Local non-static data is in the STACK segment = %p\n", &local_variable);
printf(" Local uninit static data is in the BSS segment = %p\n", &local_uninit_static);
printf(" Local init static data is in the DATA segment = %p\n", &local_init_static);
return 0;
}
左侧一列代表符号所在的地址
中间一列:
B/b - BSS段
D/d - Initialized Data Segment
R/r - 只读数据段Read-only Data Segment
T/t - 代码段Text Segment
U - Undefined symbol
右侧一列代表符号的名称
六、strings命令
strings 按顺序打印文件中的可打印的字符串,例如:函数名、打印的字符串、变量名等等。
七、objcopy命令
略
总结: objdump主要用于反编译也就是-d和-D对二进制机器码进行底层的分析,而readelf主要用于查看相关的头或段表的信息。
参考文献:
1. GCC and Make - A Tutorial on how to compile, link and build C/C++ applications
2. objdump(1) - Linux manual page
3. readelf(1) - Linux manual page
4. 程序员的自我修养:链接、装载与库
文章评论