一、实验题目
• Task1:分析 Object 文件
• Task2:对比 Object 文件链接后生成的 out 文件与源文件的变化
• Task3:调用不同 C 文件中的子函数
二、实验目的
|
• |
掌握编译和反汇编方法。 |
• |
学会分析 Object 文件。 |
|
|
• |
学会对比 Object 文件链接后生成的 out 文件与源文件的变化。 |
三、实验设计
3.1. 实验步骤
• 按照TA在实验指导中所给的指令在Ubuntu环境下完成3个Task;
• 分析每个Task的实验结果;
• 回答问题,完成实验报告。
3.2. 实验原理
3.2.1 程序的汇编、链接、运行过程
以“ 求一组数的最大值的汇编程序”max.o 为例讨论目标文件和可执行文件的格式。解释一下这个 程序的汇编、链接、运行过程:
1). 写一个汇编程序保存成文本文件max.s。
2). 汇编器读取这个文本文件转换成目标文件max.o,目标文件由若干个Section组成,我们在汇编 程序中声明的.section会成为目标文件中的Section,此外汇编器还会自动添加一些Section(比如 符号表)。
3). 然后链接器把目标文件中的Section合并成几个Segment,生成可执行文件max。 4). 最后加载器(Loader)根据可执行文件中的Segment信息加载运行这个程序。
3.2.2 对象文件
3.2.2.1 对象文件(Object files)有三个种类:
a) 可重定位的对象文件(Relocatable file)
这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者 一个可被共享的对象文件(Shared object file)。
b) 可执行的对象文件(Executable file) 文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable objectfile。在我们的 Linux 系统里面,存在两种可执行的东西。除了这里说的 Executable object file,另外一种就是可执行的脚本(如shell脚本)。注意这些脚本不是 Executable object file, 它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。
c) 可被共享的对象文件(Shared object file)
这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那 每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内 存。如果将静态库换成动态库,那么这些问题都不会出现。
要看一个文件是属于这三种类型ELF对象文件中的哪一种,可以用file命令查看。
左边是从链接器的视角来看ELF文件,开头的ELF Header描述了体系结构和操作系统等基本信息,并 指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在链接过程中用不到,所以是可有可无的,Section Header Table中保存了所有Section的描述信息, 通过Section Header Table可以找到每个Section在文件中的位置。
3.2.3 查看ELF对象文件的内容: ELF文件头被固定地放在不同类对象文件的最前面。至于它里面的内容,除了file命令所显示出来 的那些之外,更重要的是包含另外一些数据,用于描述ELF文件中ELF文件头之外的内容。我们可 以使用其中的 readelf 工具来读出整个ELF文件头的内容。
3.2.4 分析ELF对象文件
用readelf工具读出目标文件max.o的ELF Header和Section Header Table,然后我们逐段 分析。
a) ELF Header
ELF Header中描述了操作系统是UNIX,体系结构是80386。Section Header Table中有8个Section Header,从文件地址200(0xc8)开始,每个Section Header占40字节,共320字节,到文件地址 0x207结束。这个目标文件没有Program Header。文件地址是这样定义的:文件开头第一个字节的 地址是0,然后每个字节占一个地址。
b) Section Header
从Section Header中读出各Section的描述信息,其中.text和.data是我们在汇编程序中声明的 Section,而其它Section是汇编器自动添加的。Addr是这些段加载到内存中的地址(我们讲过程序 中的地址都是虚拟地址),加载地址要在链接时填写,现在空缺,所以是全0。Off和Size列指出了 各Section的起始文件地址和长度。比如.text段从文件地址0x34开始,一共98个字节,.data段从 文件地址0xcc开始,大小为0个字节,此段为空,.bss也同样为空。根据以上信息可以描绘出整个 目标文件的布局。
c) 符号表
.symtab是符号表。Ndx列是每个符号所在的Section编号,例如符号data_items在第3个Section里(也 就是.data段),各Section的编号见Section Header Table。Value列是每个符号所代表的地址,在 目标文件中,符号地址都是相对于该符号所在Section的相对地址,比如data_items位于.data段的 开头,所以地址是0,_start位于.text段的开头,所以地址也是0,但是start_loop和loop_exit相 对于.text段的地址就不是0了。从Bind这一列可以看出_start这个符号是GLOBAL的,而其它符号是 LOCAL的,GLOBAL符号是在汇编程序中用.globl指示声明过的符号。
² 各段解释总汇:
基本: 代码段, 数据段和BSS段
代码段 .text (code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。 这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读.
数据段 .data 通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态 内存分配。
BSS段 .bss 未初始化的全局变量和局部静态变量,一般在初始化时bss 段部分将会清零(bss 段属于静态内存分配,即程序一开始就将其清零了)BSS段属于静态内存分配。初 始化为0的全局变量也会被放在.bss因为被优化掉了,放在.bss可以节省磁盘空间
(.bss不占磁盘空间)。
其余段:
段的参数:段长( Size ),段所在位置 (File Offset)可以在每段的CONTENTS, ALLOC中找到各段属性, CONTENT表示该段在文件中存在,有专门的命令”size”可以用来查看ELF文件的代码段,数据段和 BSS段的长度。
3.2.5 连接前后生成文件变化
使用 arm-elf-ld.real -o max2.out max.o对源文件进行链接。
四、实验结果
4.1 Task1 生成 Object 文件,并分析
4.1.1 生成 object 文件,用 readelf 查看 ELF 对象文件的内容:
4.1.2 分析 ELF 对象文件
以上图为例,上面是使用 readelf 命令读取 max.o 的信息后重定向到文件 max_elfinfo 文件,
4.1.2.1 ELF 头
ELF Header 中描述了(为什么这里没有描述系统),体系结构是 ARM。
Section Header Table 中有 8 个 Section Header,从文件地址 252 开始,每个 Section Header 占 40 字节,含 8 个 sections,共 320 字节,目标到文件地址 519 (0x207)结束。这个目标文件 没有 Program Header。
a) Type 一项表明:这个对象文件属于 REL 可重定位文件。
b) 入口点地址一项表明:这个 max.o 的进入点是 0x0(e_entry),这表明 Relocatable objects 不会有程序进入点。所谓程序进入点是指当程序真正执行起来的时候,其第一条要运 行的指令的运行时地址。因为 Relocatable objects file 只是供再链接而已,所以它不存在 进入点。
c) 这个 max.o 文件包含有 8 个 sections,但却没有 segments(Number of program headers 为 0)。
4.1.2.2 节头
从 Section Header 中读出各 Section 的描述信息:
a) Addr 是这些段加载到内存中的地址(我们讲过程序中的地址都是虚拟地址),加载地址要在链接时填 写,现在空缺,所以是全 0。
b) Off 和 Size 列指出了各 Section 的起始文件地址和长度。比如.text 段从文件地址 0x34 开始,一共 98 个字节。
c) .data 段从文件地址 0xcc 开始,大小为 0 个字节,此段为空,.bss 也同样为空。 用 hexdump 工具把目标文件的字节全部打印出来看。
hexdump -C max.o > max_hex,直接打印出 Object 文件的全部字节,保存在 max_hex 文件中。
对于.bss 段为空的解释:
我们知道,C 语言的全局变量如果在代码中没有初始化,就会在程序加载时用 0 初始化。这种数据属 于.bss 段,在加载时它和.data 段一样都是可读可写的数据,但是在 ELF 文件中.data 段需要占用一 分空间保存初始值,而.bss 段则不需要。也就是说,.bss 段在文件中只占一个 Section Header 而 没有对应的 Section,程序加载时.bss 段占多大内存空间在 Section Header 中描述。在我们这个例 子中没有用到.bss 段,max.c 没有加载。
最后一部分:符号表
.symtab 是符号表。
a) Ndx 列是每个符号所在的 Section 编号,例如符号 data_items 在第 3 个 Section 里(也就是.data 段),各 Section 的编号见 Section Header Table。
b) Value 列是每个符号所代表的地址,在目标文件中,符号地址都是相对于该符号所在 Section 的相对 地址
c) GLOBAL 符号,在汇编程序中用.globl 指示声明过的符号。
4.2 Task2 分析链接前后的变化
4.2.1 Object 文件链接
第一步:链接 max.o 文件 arm-elf-ld.real -o max2.out max.o 链接指定 Object 文件,max2.out 为生成的文件,max.o 为源文件;
4.2.2 用 readelf 命令分析新生成的 max2.out,它和之前生成的 max.o 之间有有什么区别
4.2.2.1 ELF 头
可以发现,在 ELF Header 中:
a) Type 改成了 EXEC,由目标文件变成可执行文件了;
b) Entry point address 改成了 0x8000(这是_start 符号的地址);
c) 多了 1 个 Program Header,多了 1 个 Section Header;
4.2.2.2 节头
在 Section Header Table 中,和连接前的节头不同 是:
a) .text,.data,.bss 段的加载地址分别改成了 0x8000,0x8198, 0x8198;
b) .rel.text 段就是用于链接过程的,做完链接就没用了,所以删掉了。.bss 段没有用到,但是也没 被删掉,因为它的加载地址信息改变了;
c) 多了 2 个 section: .stack,加载地址为 0x8000;.sbss,加载地址为 0x8198;两段都为空;
d) .shstrtab, .symtab, .strtab 的偏移量 off 和大小 size 都改变了;
4.2.2.3 程序头
多出来的 Program Header Table 描述了 1 个 Segment: LOAD 的信息。
a) .text 段和前面的 ELF Header、Program Header Table 一起组成一个 Segment;
b) FileSiz 指出这个 segment 总长度是 0x00198;
c) .data 段为空,并没有组成 Segment;
d) VirtAddr 列指出该 Segment 加载到虚拟地址 0x008000; e) Flg 列指出这个 Segment 的访问权限是可读可写可执行;
f) 最后一列 Align 的值 0x8000(32K)是 ARM 平台的内存页面大小。在加载时文件也要按内存页面 大小分成若干页,文件中的一页对应内存中的一页。
4.2.2.4 重定位节
4.2.2.5 Symtab 节
4.3 Task3: 调用不同 C 文件中的子函数
4.3.1 新建一个头文件 my_hfile.h,在其中声明你将要用到的子函数名, 如要调用一个新的子函数 encrypt_char.c,则声明:
uchar encrypt_char( uchar c );
4.3.2 编写一个 main.c 函数,并在函数中用#include 命令包含 my_hfile.h 注意,由于 GCC 的规则,主函数名应该记作_main 或 gccmain,如下所示:
4.3.3 按照上述步骤,将 main.c 编译生成 Object 文件,再将其反汇编并保存。
arm-elf-gcc –c main.c
arm-elf-gcc –S main.o > main.s
4.3.4 确保要调用的子函数 encrypt_char.c 已编译为 Object 文件,并在同一目录下;确保添加的头 文件也在同一目录下(系统自带的头文件除外)
4.3.5
arm-elf-ld.real -o main2.out main.o encrypt_char.o -lc
main2.out 是链接生成的文件,main.o encrypt_char.o 是参与链接的源文件,
-lc 允许链接器搜索默认的路径,当你使用了系统自带的头文件或子函数时,需要添加该选项 以便链接器能顺利链接到对应库
4.3.6
arm-elf-objdump -S main2.out > main2_1.s 反汇编链接后的.out 文件,和链接前的.o 文件反汇编结果进行比较
链接前 main.s:
链接后 main2_1.s:
指令中的相对地址都改成了绝对地址,但是对于每一条指令,比如 4.3.6.1 算术运算指令:
链接前的
与链接后的
都没有改变,只是反汇编的结果不同了,但是指令的机器码没有变化;
4.3.6.2 跳转指令:
为什么不用改指令就能跳转到新的地址呢?因为跳转指令中指定的是相 对于当前指令向前或向后跳多 少字节,而不是指定一个完整的内存地址,内存地址有 32 位,这些跳转指令只有 16 位,显然也不可能
指定一个完整的内存地址,这称为相对跳转。这种相对跳转指令只有 16 位,只能在当前指令前后的一个 小范围内跳转,不可能跳得太远,也有的跳转指令指定一个完整的内存地址,可以跳到任何地方,这称 绝对跳转。
4.3.6.3 内存访问指令
五、 实验感想
5.1Q&A
5.1.1 Suppose an application to be run on an embedded microprocessor system based on an ARM CPU and RAM, and has (flash) ROM memory chips. Indicate for each of the sections listed in Table 1, in which memory type, the corresponding content is to be created:
Ans:
问题分析:(来源于 http://www.cnblogs.com/hongzg1982/articles/2205093.html)
text 和 data 段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执 行文件中加载。而 bss 段不在可执行文件中,由系统初始化。
bss 段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。
data(已手动初始化的数据)段则为数据分配空间,数据保存在目标文件中。
一个 ARM 程序包含 3 部分:RO,RW 和 ZI RO 是程序中的指令和常量
RW 是程序中的已初始化变量 ZI 是程序中的未初始化的变量 在 ARM 的集成开发环境中,
1、只读的代码段称为 Code 段,即上述的.text 段。 2、只读的常量数据段,被称作 RO Data 段,即上述的.constdata 段。 以上两个段统称为 RO 段(Read Only),放在 ROM 或 FLASH 等非易失性器件中.
3、可读可写的初始化了的全局变量和静态变量段,被称作 RW Data 段(ReadWrite),即上述 的.bss 段。
4、可读可写的未初始化的全局变量和静态变量段,被称作 ZI Data 段(Zero Init),即上述的.data
段。因为这个段里的变量要被初始化为零,所以叫 ZI 段。
以上两个段统称为 RW 段,而在运行时,它必须重新装载到可读可写的 RAM 中。
5.1.3 Declare in encrypt_char.c two global integer variables a, b, and a global integer constant cons. Initialize these data as follows: a = 7, b = 0, cons = 33. Compile the program again as described above and examine the generated assembler directives. In which sections of the object file, the assembler sets a, b, and cons are stored? What sense does the differential treatment of a and b? What sense does the differential treatment of a and ccons?
Ans: 说明:变量 c 名字我改为 cons,不与函数 encrypt_char 的 c 重名
.s 文件
a ,b,cons 都放在.data,但是《程序员的自我修养—链接、装载与库》里面说的是 初始化为 0 的全局变量会被放在.bss,因为会被优化,放在.bss 会减少磁盘空间。怀疑会不会 是 a 和 b 的初始化之间用逗号隔开而非分号的缘故,于是我改了定义的格式:
发现结果还是一样的,b 被放在.data,在目标文件的信息中,看到.data 这个 section 的 size 也 是 8,意味着 b 确实在编译的时候是没有被优化的。看一下如果 b 没有初始化的话:
结果是:
全局变量 b 直接在汇编文件中被删掉了, 但是不是说 b 这个变量就相当于完全没存在了呢? 其实不是,《程序员的自我修养—链接、装载与库》里面关于 BSS 段是这么解释的,书里面的 程序声明了一个未初始化的全局变量 global_uninit_var 和一个静态变量 static_var2,
于是查看目标文件的符号表,b 初始化为 0 的如下: 节头表的话依然是.data 8 个字节( a, b ),.rodata 为 4 个字节(变量 cons)
符号表
显然符号表表明了 b 和 a 都是放在第[3]段也就是.data 段,也印证了刚才所说的 b 虽然是初始 化为 0 的全局变量,但是在我的机器上并没有被优化。
b 不初始化的情况:
节头表
.data 的大小变成了 4 个字节。 再看符号表
果然,b 没有被放在任何段,只有一个未定义的 COM 符号,而变量 a 被表明是放在第[3]段, 也就是.data 段,cons 被放在第[5]段也就是.rodata 段。
还是很模糊,于是又分别把 main.o 和 b 初始化和没初始化两个版本的 encrypt_char.c 生成的.o 文件链接起来,结果如下:
b 初始化为 0 -》 编译生成 encrypt_char.o ,与 main.o 链接 –》 readelf 查看 main.out 信 息到 main4_elfinfo
.data 依然是 8 个字节,说明链接后 b 还是放在了.data 段,也就是链接器和编译器都没有把这 个初始化为 0 的全局变量优化;
再看符号表
确确实实是在.data 段。
b 不初始化 -》 编译生成 encrypt_char.o ,与 main.o 链接 –》 readelf 查看 main.out 信息到main5_elfinfo
如猜测一样,.data 变成 4 个字节,而.bss 为 4 个字节,b 被放在了.bss 符号表:
第 5 段表明是在.bss 段,也就是之前编译的结果显示的只用一个 COM 打发的(在.bss 段也没 有)b 经链接后还是放进了.bss 段。 这个问题探究到此为止吧,至于为什么编译器没有把初始化为 0 的全局变量 b 优化到.bss 段的 原因,查看了《程序员修炼之道》一书中“强符号与弱符号”,COMMON 符号这部分,我认 为有可能是编译器在编译 把全局变量 b 初始化为 0 这一语句的时候,在“强符号和弱符号” 之间选择了强符号,认为 b 是强符号而非弱符号,由于.bss 段放的是弱符号或者静态变量,因 此强符号 b 是不会被放在.bss 段的,除非给 b 加上 static。 最后,我的结论是对于全局变量来说,如果初始化了不为 0 的值,那么该全局变量则被保存在 data 段,如果初始化的值为 0,那么将其保存在 bss 段,如果没有初始化,则将其保存在 common 段,等到链接时再将其放入到 BSS 段。关于第三点不同编译器行为会不同,有的编译器会把 没有初始化的全局变量直接放到 BSS 段。最后一点就是初始化为 0 的全局变量也有可能被放 在.data 段而非.bss 段。
5.1.4 Declare the main function a local integer variable d and initialize it with any value. Compile the program again as described above and examine the generated assembly code. Why d is not created in a data section of the object file?
Ans:
编译生成的.s 文件只有.text 代码段
很简单,.data 数据段是用于静态内存分配的,通常放的是已初始化的全局变量,而局部变量 是在代码段中声明、初始化、修改的,应该放在动态分配的内存段,而 stack 和 heap 就是这样 的内存段。在我的代码中变量 b 应该被放在栈中:栈又称堆栈,是用户存放程序临时创建的局 部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括 static 声明的变量,static 意味 着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈 特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临 时数据的内存区。
声明局部变量前后的变化:
节头的开始增加了 8 个字节,是因为.text 段增加了相应的大小,局部变量声明在代码段中,占 用.text 的空间。
.s 文件中的 frame 也是增加了 4 个字节的。
5.2 感想总结
5.2.1 了解了程序的汇编、链接、运行过程,掌握了一些学术概念;
5.2.2 学会了分析目标文件,初步掌握了 ELF 文件的格式,包括 ELF 头,程序头部表,节区,节 区头部表,一些基本的 section 里面装载的是什么内容,一些 Object 分析工具,ELF 格式分 析工具。
5.2.3 编译这块的知识储备实在太缺乏,花了好长时间查阅资料,然而效果依然不够好,以后先 要多看这方面的书。