最近发现以前同事发的blog,对于刚刚进入这个领域进行学习的我帮助很大,决定一点点学习并且记录下来。
原文地址:http://blog.csdn.net/yayong/article/details/170842
1. 编译环境
OS: Solaris 12 X86 (uname -a)
Compiler: gcc 3.4.3 (gcc -v gcc安装:设置好publisher后就可以直接pkg install gcc-3)
Linker: Solaris Link Editors 5.x (ld -V)
Debug Tool: mdb (Note: mdb是Solaris提供的kernel debug工具,这里用它做反汇编和汇编语言调试工具。)
Editor: vi/vim
2. C代码分析
# vi test1.c
int main()
{
return 0;
}
编译该程序,产生二进制文件:
# gcc test1.c -o test1
# file test1
test1: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped
test1是一个ELF格式32位小端(Little Endian)的可执行文件,动态链接并且符号表没有去除。
这正是Unix/Linux平台典型的可执行文件格式。
Note:
(1)
Linux ELF ELF = Executable and Linkable Format,可执行连接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发 和发布的。扩展名为elf。工具接口标准委员会(TIS)选择了正在发展中的ELF标准作为工作在32位INTEL体系上不同操作系统之间可移植的二进制文件格式。假定开发者定义了 一个二进制接口集合,ELF标准用它来支持流线型的软件发展。应该减少不同执行接口的数量。因此可以减少重新编程重新编译的代码。 编辑本段文件格式 Linking View Elf header Program header table optional section1 …… section n section header table Exection View Elf header Program header table segment 1 section 2 …… segment header table 一个ELF头在文件的开始,保存了路线图(road map),描述了该文件的组织情况。sections保存着object 文件的信息,从连接角度看:包括指令,数据,符号表,重定位信息等 等。特别sections的描述会出项在以后的第一部分。第二部分讨论了段和从程序的执行角度看文件。 假如一个程序头表(program header table)存在,那么它告诉系统如何 来创建一个进程的内存映象。被用来建立进程映象(执行一个程序)的文件必须要有一个程序头表(program header table);可重定位文件不需要这个头表。一个section头表 (section header table)包含了描述文件sections的信息。每个section在这个表中有一个入口;每个入口给出了该section的名字,大小,等等信息。在联接过程中的文件必 须有一个section头表;其他object文件可要可不要这个section头表。 注意: 虽然图显示出程序头表立刻出现在一个ELF头后,section头表跟着其他section部分出现,事实 是的文件是可以不同的。此外,sections和段(segments)没有特别的顺序。只有ELF头(elf header)是在文件的固定位置。
(2)
LSB: Least Significant Bit,最低有效位(LSB)是给这些单元值的一个二进制整数位位置,就是,决定是否这个数字是偶数或奇数。LSB有时候是指最右边的位,因为写较不重 要的数字到右边位置符号的协定。它类似于一个十进制整数的最不重要的数字,它是在一个(最右边)位置的数字。 LSB(Least Significant Bit),意思为最低有效位; MSB(Most Significant Bit),即最高有效位,若MSB=1,则表示数据为负值,若MSB=0,则表示数据为正。 MSB是Most Significant Bit的缩写,最高有效位。在二进制数中,MSB是最高加权位。与十进制数字中最左边的一位类似。通常,MSB位于二进制数的最左侧,LSB位于二进制数 的最右侧。
# mdb test1
Loading modules: [ libc.so.1 ]
> main::dis ; 反汇编main函数,mdb的命令一般格式为 <地址>::dis
main: pushl %ebp ; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址
main+1: movl %esp,%ebp ; esp值赋给ebp,设置main函数的栈基址
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: movl $0,%eax
main+0xe: subl %eax,%esp
main+0x10: movl $0,%eax ; 设置函数返回值0
main+0x15: leave ; 将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址
main+0x16: ret ; main函数返回,回到上级调用
>
注:这里得到的汇编语言语法格式与Intel的手册有很大不同,Unix/Linux采用AT&T汇编格式作为汇编语言的语法格式
如果想了解AT&T汇编可以参考文章:Linux AT&T 汇编语言开发指南
(1) EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。
pushl %ebp ; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址
movl %esp,%ebp ; esp值赋给ebp, 设置 main函数的栈基址
........... ; 以上两条指令相当于 enter 0,0
...........
leave ; 将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址
ret ; main函数返回,回到上级调用
ENTER是建立当前函数的栈框架,即相当于以下两条指令:
pushl %ebp
movl %esp,%ebp
LEAVE是释放当前函数或者过程的栈框架,即相当于以下两条指令:
movl ebp esp
popl ebp
(3) RET指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行
(CALL指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。)
(4) 为什么用EAX寄存器保存函数返回值?
实际上IA32并没有规定用哪个寄存器来保存返回值。但如果反汇编Solaris/Linux的二进制文件,就会发现,都用EAX保存函数返回值。
这不是偶然现象,是操作系统的ABI(Application Binary Interface)来决定的。
Solaris/Linux操作系统的ABI就是Sytem V ABI。
在C语言的层面来看,main函数是一个程序的起始入口点,而实际上,ELF可执行文件的入口点并不是main而是_start。
mdb也可以反汇编_start:
> _start::dis ;从_start 的地址开始反汇编
_start: pushl $0
_start+2: pushl $0
_start+4: movl %esp,%ebp
_start+6: pushl %edx
_start+7: movl $0x80504b0,%eax
_start+0xc: testl %eax,%eax
_start+0xe: je +0xf <_start+0x1d>
_start+0x10: pushl $0x80504b0
_start+0x15: call -0x75 <atexit>
_start+0x1a: addl $4,%esp
_start+0x1d: movl $0x8060710,%eax
_start+0x22: testl %eax,%eax
_start+0x24: je +7 <_start+0x2b>
_start+0x26: call -0x86 <atexit>
_start+0x2b: pushl $0x80506cd
_start+0x30: call -0x90 <atexit>
_start+0x35: movl +8(%ebp),%eax
_start+0x38: leal +0x10(%ebp,%eax,4),%edx
_start+0x3c: movl %edx,0x8060804
_start+0x42: andl $0xf0,%esp
_start+0x45: subl $4,%esp
_start+0x48: pushl %edx
_start+0x49: leal +0xc(%ebp),%edx
_start+0x4c: pushl %edx
_start+0x4d: pushl %eax
_start+0x4e: call +0x152 <_init>
_start+0x53: call -0xa3 <__fpstart>
_start+0x58: call +0xfb <main> ;在这里调用了main函数
_start+0x5d: addl $0xc,%esp
_start+0x60: pushl %eax
_start+0x61: call -0xa1 <exit>
_start+0x66: pushl $0
_start+0x68: movl $1,%eax
_start+0x6d: lcall $7,$0
_start+0x74: hlt
>