之前的计划就是想重新学习一边编译链接的过程,这次是安排上了,但是还是感觉太年轻了,以上一篇的问题,去准备这一篇的时候,发现学习的东西还是很多的,应该是很多很多,惆怅。
但是flag已经发出去了,就硬着头皮去搞把,希望能坚持下来。加油。。。
2.1 目标文件
我们按照上一节的步骤,我们分步编译出来了,预处理文件.i,还有汇编文件.s,还有目标文件.o。
预处理文件上一篇就讲过了,汇编文件的话里面都是一些汇编语句,这个以后再讲,这一节我们的目标就是.o文件。
我有在之前的hello_world的基础上,添加了一些变量,和一个函数调用,这样分析起来会更全面。
1 | c复制代码#include <stdio.h> |
在这里hello_world中,我们定义了全局变量,局部变量,函数调用。
就按上一节的操作,我们直接汇编成.o文件。
我们可以使用file命令查看这个文件的格式:
1 | shell复制代码root@ubuntu:~/c_test/02# file hello_world.o |
很明显hello_world.o文件也是一个ELF文件,不过不是可执行文件,是可重定位的文件,等待链接器把多个.o文件链接起来,怎么链接呢?下节再讲。
2.2 objdump命令
linux下,已经准备了两个命令给我们,就是给我们这些不安分的,一个是objdump反汇编的命令,一个是readelf解析elf的命令,这样我们就不用一个字节一个字节去对比elf文件,如果有兴趣的话,也可以去一个字节一个字节对比,不过我这样就不对比了,后来的任务巨重。
下面简单介绍一个objdump常用的参数:
1 | shell复制代码objdump -f hello_world.o # 显示文件的头信息 |
2.3 readelf命令
接着看看readelf常用的参数:
1 | shell复制代码readelf -h hello_world.o # 显示elf格式的头信息 |
其实也不是很熟这些命令,以后有常用在添加。
2.4 hello_world.o分析
来到了这一章的重点,我们先分析hello_world.o,下次分析hello_world,以后看安排要不要把.a .so的也分析一波。
2.4.1 头信息
查看头信息,两个命令都可以查看到:
2.4.1.1 objdump -f
1 | shell复制代码root@ubuntu:~/c_test/02# objdump -f hello_world.o |
可以查看到编译文件的系统架构,文件的格式,后面还有文件的标记:
1 | arduino复制代码/* BFD contains relocation entries. */ |
2.4.1.2 readelf -h
1 | shell复制代码root@ubuntu:~/c_test/02# readelf -h hello_world.o |
这个readelf命令重点是根据elf的二进制编码,然后把属于头部分的信息,转码出来,所以我们看的就是这个样子,又回想起当年,动不动就分析二进制的时候,哎,现在有一个工具多爽,直接输入一个命令就出来了,就不用去对这二进制数据分析了。
2.4.2 段信息
段信息是一个很重要的知识点,段信息也是可以使用两个命令来读取的。
这里readelf -S这个我们留着下节去分析hello_world,这一节我们用objdump -h。
先来总览一下:
1 | shell复制代码root@ubuntu:~/c_test/02# size hello_world.o |
我们可以用size来简单查看各个段大小,text段、data段、bss段。
1 | shell复制代码root@ubuntu:~/c_test/02# objdump -h hello_world.o |
重点出来了,接下来的目标,我们就重点分析这几个段。
2.4.2.1 text段
这一段是专门存放代码的,我们就来看看代码最后是怎么存的?想看不?
看的话还是需要objdump这个老工具人
1 | shell复制代码root@ubuntu:~/c_test/02# objdump -s hello_world.o |
-s 是把所有段以十六进制的格式输出,这也是代码中存储的格式,直接看十六进制看不懂,是不是,所以我们还需要把机器指令段反汇编出来,一对比就明白了很多。
1 | shell复制代码root@ubuntu:~/c_test/02# objdump -d hello_world.o |
左侧第一列,是这条语句的偏移,通过这个就能明白x86的机器命令是变长的。
左侧第二列,就是十六进制的内容,可以看成机器码。
右侧,就是对应的汇编指令了。
通过我们对比两个的十六进制,是不是完全符合,所以说代码存储在文件中,就是这样存储的。
2.4.2.2 .data段
程序中初始化的数据,就是不为0的全局变量,不为0的局部静态变量。
我们看上面的这个段是8个字节,怎么是8个字节呢?
这两个数据是存储在data段中的,不信是不是?
我们用反汇编来验证一波:
1 | shell复制代码Contents of section .data: |
0x54000000 = 84,这里面就是存了两个84。
为什么最低位在前面,这个就是linux系统一般都是小端模式。
顺序的话,应该是按变量的加载顺序,取值。
2.4.2.3 BSS段
程序中为0的全局变量,为0的静态变量,或者是不赋值,不赋值也会默认为0。
为什么会分出一个.bss段,是因为这样可以节省一点空间,等程序运行的时候在搞上来,所以这个段是需要大小的,统计程序中符合这个段的变量大小,留下空间,但是因为值为0,所以不要存储。
CONTENTS表示是这个.o程序包含CONTENTS这个字段,因为bss没有,所以没包含。
2.4.2.4 rodata
只读数据段,一般是放的是常量(const),比如字符串常量。
为什么会出现这样的只读数据段,这是在编译器中把这些只读的数据存储到一起,如果这段数据被修改,就会出现报错,编译器是这样检测只读数据的。
不过好像可以用一个指针指向这个地址,然后就可以直接修改了,因为linux系统,存放只读数据也是放在RAM上,理论上也是可以绕过编译器修改的。
像51单片机,因为RAM太少,只读数据是直接存储到flash中的,flash是一个不能改的存储介质,这个用指针指向也是不能修改的。
其实我很疑惑的一定就是这个代码中没有只读数据啊,怎么会有呢?
这就是我们c语言功底不够用,其实printf函数中的字符串,就是存储成字符串常量的,就是存在这个只读数据段的。不信?我们反汇编看看:
1 | shell复制代码Contents of section .rodata: |
是不是安排的明明白白,清清楚楚的。
2.4.2.5 comment
包含编译器的信息段
1 | shell复制代码Contents of section .comment: |
2.4.2.6 .note.GNU-stack
堆栈提示段,表示不是很明白,
2.4.2.7 .eh_frame
保存c++异常处理的内容
1 | shell复制代码Contents of section .eh_frame: |
这一段也不懂,以后再补补,不懂的地方还有很多。
2.4.2.8 自定义段
1 | shell复制代码__attribute__((section("FOO"))) int global = 42; |
当初分析uboot代码的时候,就很多这种自定义段,uboot启动后,会把同一个初始化的函数放到同一个段,然后好像是依次编译这个段的内容。
本文转载自: 掘金