《编译系统-自底向上研究方法》ELF头部 - 弦外之音

/ 0评 / 0

接着上篇文章分析,main.csum.cmain 3个文件,下载地址:百度网盘,提取码:cat1 。用 xelfviewer 打开 文件 main ,如下图:

上图我圈出来几个重点,上面的 Offset 是指这个字段在 main 文件的位置,例如 0x12 ~ 0x14 这两个字节是 e_mechine 的内容。

然后开头的 4个 字节 7f 45 4c 46 ,7f 是 DEL控制符的ASII码,后面3 个字节是 ELF 的ASII码。这四个字节被叫做 魔数,操作系统通过魔数决定如何加载运行。

windows 的exe文件,前面两个字节是 0x4d 0x5a 。

ELF 的头部一共是 64 个字节,我数了一下,一共有 14 个字段,其实上面这些字段,是在一个结构体里面定义的,在 /usr/include/elf.h 文件里面,如下图:

实际上,这 14 个字段,只有一些是重点,我着重讲一下。

1,e_ident,16 个字节, 魔数 ,23位还是64,大端还是小端,ABI标准,等一些信息。

2,e_type,2个字节,代表文件的类型,因为ELF格式不是只有 可运行文件才有,.o 这种文件也是 ELF 格式的。

3,e_entry,8个字节,值是 4f0 ,这个是重点,术语是 ELF 程序的入口虚拟地址,这样说有点绕口,实际上,可以理解为第一条执行的指令就是从这个位置开始执行的。如下图:

上图这些都是二进制 机器码指令,可以丢给CPU运行的。但是机器码不太好阅读,所以需要反汇编一下,这时候就需要用到 objdump 这个工具了。命令如下:

objdump -d main

这样看,就是一些 汇编指令,可以看到 操作系统 运行 main 文件,不是从 main() 函数开始的,而是从一个 _start() 函数开始,可以看到 _start() 里面调了一个 call 指令,也就是 从 _start() 里面调 main()

_start() 做的事情比较简单,就是把命令行参数 压进去栈,给 main() 函数用 ,main() 函数可以接受 argcargv 变量的。

这里还有一个需要研究的重点是,callq 的参数 0x200ac6,其实这是一个相对地址,也就是运行到 callq 0x200ac6 指令的时候,要做两件事情。

1,callq 会把 下条指令的地址值压到栈顶上,方便 里面用 ret 跳回来。

2,第二个是重点,callq 实际上是一个 jmp ,会进行跳转,跳转地址是 寄存器 rip 的值 加上 0x200ac6,这个跳转地址实际上会跳转到 main() 函数的代码。main() 函数代码的位置如下图,还是用 objdump 打印出来的。

注意看左边的 5fa,这是文件的偏移位置。在用 notepad++ 查看二进制内容,如下:

所以 main 函数的代码,确实是在 5fa 的位置,但是运行的时候,callq 的参数是 rip 加上 0x200ac6,rip 是运行的时候才知道,编译系统是怎么这么聪明能正好

rip 加上 0x200ac6 就能跳到 5fa 的位置的呢?这个 0x200ac6 是怎么计算出来的?这个问题可以说是本文的重点。


gdb 调试,在 _start 函数的入口打个断点。

b *_start

断点调试之后发现,callq 0x200ac6 并不是跳转 到 main() 函数,而是还有一层 __libc_start_main,如下图

__libc_start_main 这个函数应该是 libc 动态库的一个函数。估计是初始化环境变量什么的,所以这个 0x200ac6 暂时不管他。隔了太多层,不好分析。

不过 callq 的参数,还是非常值得研究的,我们 main.c 里面调用 sum.c 的函数 sum,这也是一个 callq ,咱们来分析这个,如下图:

可以看到,这里的 callq 的参数是 0x8,这个 8 是怎么计算出来的呢?实际上就是 61b 减去 60e 得到的。也就是说,无论操作系统运行时 把这个 60e 的位置映射到内存的哪个位置,指令执行到 call 08 00 00 00 的时候,寄存器 rip 的值就可以看做是 60e ,指令映射到内存也是连续的,所以偏移 8 个字节,自然就会跳到 sum 函数的开始位置 61b。理解这机制有助于我们 后面往 main 加一些自己的机器码指令。不会弄乱他的偏移位。


回到之前的主题,分析ELF 头部的其他字段。

4,e_shoff ,2个字节,值是 0x1948,这个字段也是重点,这个是段表的偏移位置,也就是从文件的 1948 字节开始就是段表的内容。什么是段表,一个 ELF 文件,指令跟数据是分开不同的段放的。可以把段表理解为一个容器,管理不同的东西,让操作系统知道怎么加载不同的东西,段表的内容分析请看后续文章《编译系统-自底向上研究方法》ELF段表

5,e_shensize,2个字节,值是 0x40,这个字段是说段表里面的一个段有多大,段表里面可以有多个段的。

6,e_shnum,2字节,值是 0x1c,这个字段是说段表里面有多少个段,在main里面有 28 个段。

5,e_ehsize,ELF头文件的大小,这里是 64字节,这个字段感觉有点多余,直接 sizeof(Elf64_Ehdr) 也能知道是 64 字节。

其他字段的一些介绍,可以看《程序员的自我修养》第 3.4 章节。


由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1,QQ:2338195090。

发表回复

您的电子邮箱地址不会被公开。