本文名字虽然是汇编语言学习手记, 但实际论述了业界真正的C编译器和链接器生成汇编语言的惯例, 这些知识点是熟悉汇编语言后使用汇编和C协同真正进行工作的基础.
在第三小节给出了一个ELF文件segment/section dump实例;
当然随着编译器的发展, ELF sections也有些变动, 如.rel.got=>.rel.dyn/.rel.plt; 在.got外,增加了.got.plt等. 当然不大部分还是可以做为概略描述参考的.
很久以前保存的专题文章, 因为工作需要温习ELF时, 查阅此文; 找到原文链接http://blog.csdn.net/yayong/article/category/42846, 转载在此.
X86汇编语言学习手记(1-3)
原文出处:http://blog.csdn.net/yayong
作者: Badcoffee
Email: blog.oliver@gmail.com
版权所有:转载时请务必以超链接形式标明文章原始出处、作者信息及本声明
这是作者在学习X86汇编过程中的学习笔记,难免有错误和疏漏之处,欢迎指正。
作者将随时修改错误并将新的版本发布在自己的Blog站点上。
严格说来,本篇文档更侧重于C语言和C编译器方面的知识,如果涉及到具体汇编语言的内容,可以参考相关文档。
X86汇编语言学习手记(1)
2004年10月
1. 编译环境
OS: Solaris 9 X86
Compiler: gcc 3.3.2
Linker: Solaris Link Editors 5.x
Debug Tool: mdb
Editor: vi
注:关于编译环境的安装和设置,可以参考文章:Solaris 上的开发环境安装及设置。
mdb是Solaris提供的kernel debug工具,这里用它做反汇编和汇编语言调试工具。
如果在Linux平台可以用gdb进行反汇编和调试。
2. 最简C代码分析
为简化问题,来分析一下最简的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平台典型的可执行文件格式。
用mdb反汇编可以观察生成的汇编代码:
# 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 汇编语言开发指南
问题:谁调用了 main函数?
在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
>
问题:为什么用EAX寄存器保存函数返回值?
实际上IA32并没有规定用哪个寄存器来保存返回值。但如果反汇编Solaris/Linux的二进制文件,就会发现,都用EAX保存函数返回值。
这不是偶然现象,是操作系统的ABI(Application Binary Interface)来决定的。
Solaris/Linux操作系统的ABI就是Sytem V ABI。
概念:SFP (Stack Frame Pointer)栈框架指针
正确理解SFP必须了解:
IA32 的栈的概念
CPU 中32位寄存器ESP/EBP的作用
PUSH/POP 指令是如何影响栈的
CALL/RET/LEAVE 等指令是如何影响栈的
如我们所知:
1)IA32的栈是用来存放临时数据,而且是LIFO,即后进先出的。栈的增长方向是从高地址向低地址增长,按字节为单位编址。
2) EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。
3) PUSH一个long型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。
4) POP一个long型数据,过程与PUSH相反,依次将ESP-4、ESP-3、ESP-2、ESP-1从栈内弹出,放入一个32位寄存器。
5) CALL指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。
6) RET指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行
7) ENTER是建立当前函数的栈框架,即相当于以下两条指令:
pushl %ebp
movl %esp,%ebp
8) LEAVE是释放当前函数或者过程的栈框架,即相当于以下两条指令:
movl ebp esp
popl ebp
如果反汇编一个函数,很多时候会在函数进入和返回处,发现有类似如下形式的汇编语句:
pushl %ebp ; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址
movl %esp,%ebp ; esp值赋给ebp,设置 main函数的栈基址
........... ; 以上两条指令相当于 enter 0,0
...........
leave ; 将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址
ret ; main函数返回,回到上级调用
这些语句就是用来创建和释放一个函数或者过程的栈框架的。
原来编译器会自动在函数入口和出口处插入创建和释放栈框架的语句。
函数被调用时:
1) EIP/EBP成为新函数栈的边界
函数被调用时,返回时的EIP首先被压入堆栈;创建栈框架时,上级函数栈的EBP被压入堆栈,与EIP一道行成新函数栈框架的边界
2) EBP成为栈框架指针SFP,用来指示新函数栈的边界
栈框架建立后,EBP指向的栈的内容就是上一级函数栈的EBP,可以想象,通过EBP就可以把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现 backtrace功能的
3) ESP总是作为栈指针指向栈顶,用来分配栈空间
栈分配空间给函数局部变量时的语句通常就是给ESP减去一个常数值,例如,分配一个整型数据就是 ESP-4
4) 函数的参数传递和局部变量访问可以通过SFP即EBP来实现
由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:
+8+xx(%ebp) ; 函数入口参数的的访问
-xx(%ebp) ;函数局部变量访问
假如函数A调用函数B,函数B调用函数C,则函数栈框架及调用关系如下图所示:
+-------------------------+----> 高地址
| EIP (上级函数返回地址) |
+-------------------------+
+--> | EBP (上级函数的EBP) | --+ <------当前函数A的EBP (即SFP框架指针)
| +-------------------------+ +-->偏移量A
| | Local Variables | |
| | .......... | --+ <------ESP指向函数A新分配的局部变量,局部变量可以通过A的ebp-偏移量A访问
| f +-------------------------+
| r | Arg n(函数B的第n个参数) |
| a +-------------------------+
| m | Arg .(函数B的第.个参数) |
| e +-------------------------+
| | Arg 1(函数B的第1个参数) |
| o +-------------------------+
| f | Arg 0(函数B的第0个参数) | --+ <------ B函数的参数可以由B的ebp+偏移量B访问
| +-------------------------+ +--> 偏移量B
| A | EIP (A函数的返回地址) | |
| +-------------------------+ --+
+--- | EBP (A函数的EBP) |<--+ <------ 当前函数B的EBP (即SFP框架指针)
+-------------------------+ |
| Local Variables | |
| .......... | | <------ ESP指向函数B新分配的局部变量
+-------------------------+ |
| Arg n(函数C的第n个参数) | |
+-------------------------+ |
| Arg .(函数C的第.个参数) | |
+-------------------------+ +--> frame of B
| Arg 1(函数C的第1个参数) | |
+-------------------------+ |
| Arg 0(函数C的第0个参数) | |
+-------------------------+ |
| EIP (B函数的返回地址) | |
+-------------------------+ |
+--> | EBP (B函数的EBP) | --+ <------ 当前函数C的EBP (即SFP框架指针)
| +-------------------------+
| | Local Variables |
| | .......... | <------ ESP指向函数C新分配的局部变量
| +-------------------------+----> 低地址
frame of C
图 1-1
再分析test1反汇编结果中剩余部分语句的含义:
# mdb test1
Loading modules: [ libc.so.1 ]
> main::dis ; 反汇编main函数
main: pushl %ebp
main+1: movl %esp,%ebp ; 创建Stack Frame(栈框架)
main+3: subl $8,%esp ;通过ESP-8来分配8字节堆栈空间
main+6: andl $0xf0,%esp ;使栈地址16字节对齐
main+9: movl $0,%eax ;无意义
main+0xe: subl %eax,%esp ;无意义
main+0x10: movl $0,%eax ; 设置main函数返回值
main+0x15: leave ; 撤销Stack Frame(栈框架)
main+0x16: ret ; main 函数返回
>
以下两句似乎是没有意义的,果真是这样吗?
movl $0,%eax
subl %eax,%esp
用gcc的O2级优化来重新编译test1.c:
# gcc -O2 test1.c -o test1
# mdb test1
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: xorl %eax,%eax ; 设置main返回值,使用xorl异或指令来使eax为0
main+0xb: leave
main+0xc: ret
>
新的反汇编结果比最初的结果要简洁一些,果然之前被认为无用的语句被优化掉了,进一步验证了之前的猜测。
提示:编译器产生的某些语句可能在程序实际语义上没有用处,可以用优化选项去掉这些语句。
问题:为什么用xorl来设置eax的值?
注意到优化后的代码中,eax返回值的设置由movl $0,%eax 变为 xorl %eax,%eax,这是因为IA32指令中,xorl比movl有更高的运行速度。
概念:Stack aligned栈对齐
那么,以下语句到底是和作用呢?
subl $8,%esp
andl $0xf0,%esp ; 通过andl使低4位为0,保证栈地址16字节对齐
表面来看,这条语句最直接的后果是使ESP的地址后4位为0,即16字节对齐,那么为什么这么做呢?
原来,IA32系列CPU的一些指令分别在4、8、16字节对齐时会有更快的运行速度,因此gcc编译器为提高生成代码在IA32上的运行速度,默认对产生的代码进行16字节对齐
andl $0xf0,%esp的意义很明显,那么 subl $8,%esp 呢,是必须的吗?
这里假设在进入main函数之前,栈是16字节对齐的话,那么,进入main函数后,EIP和EBP被压入堆栈后,栈地址最末4位二进制位必定是1000,esp -8则恰好使后4位地址二进制位为0000。看来,这也是为保证栈16字节对齐的。
如果查一下gcc的手册,就会发现关于栈对齐的参数设置:
-mpreferred-stack-boundary=n ; 希望栈按照2的n次的字节边界对齐, n的取值范围是2-12
默认情况下,n是等于4的,也就是说,默认情况下,gcc是16字节对齐,以适应IA32大多数指令的要求。
让我们利用-mpreferred-stack-boundary=2来去除栈对齐指令:
# gcc -mpreferred-stack-boundary=2 test1.c -o test1
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: movl $0,%eax
main+8: leave
main+9: ret
>
可以看到,栈对齐指令没有了,因为,IA32的栈本身就是4字节对齐的,不需要用额外指令进行对齐。
那么,栈框架指针SFP是不是必须的呢?
# gcc -mpreferred-stack-boundary=2 -fomit-frame-pointer test1.c -o test
> main::dis
main: movl $0,%eax
main+5: ret
>
由此可知,-fomit-frame-pointer可以去除SFP。
问题:去除SFP后有什么缺点呢?
1)增加调式难度
由于SFP在调试器backtrace的指令中被使用到,因此没有SFP该调试指令就无法使用。
2)降低汇编代码可读性
函数参数和局部变量的访问,在没有ebp的情况下,都只能通过+xx(esp)的方式访问,而很难区分两种方式,降低了程序的可读性。
问题:去除SFP有什么优点呢?
1)节省栈空间
2)减少建立和撤销栈框架的指令后,简化了代码
3)使ebp空闲出来,使之作为通用寄存器使用,增加通用寄存器的数量
4)以上3点使得程序运行速度更快
概念:Calling Convention 调用约定和 ABI (Application Binary Interface)应用程序二进制接口
函数如何找到它的参数?
函数如何返回结果?
函数在哪里存放局部变量?
那一个硬件寄存器是起始空间?
那一个硬件寄存器必须预先保留?
Calling Convention 调用约定对以上问题作出了规定。Calling Convention也是ABI的一部分。
因此,遵守相同ABI规范的操作系统,使其相互间实现二进制代码的互操作成为了可能。
例如:由于Solaris、Linux都遵守System V的ABI,Solaris 10就提供了直接运行Linux二进制程序的功能。
详见文章:关注: Solaris 10的10大新变化
3. 小结
本文通过最简的C程序,引入以下概念:
SFP 栈框架指针
Stack aligned 栈对齐
Calling Convention 调用约定和 ABI (Application Binary Interface)应用程序二进制接口
今后,将通过进一步的实验,来深入了解这些概念。通过掌握这些概念,使在汇编级调试程序产生的core dump、掌握C语言高级调试技巧成为了可能。
X86汇编语言学习手记(2)
2004年11月
自X86 汇编语言学习手记(1)在作者的Blog上发布以来,得到了很多网友的肯定和鼓励,并且还有热心网友指出了其中的错误,作者已经将文档中已发现的错误修正后更新在Blog上。
上一篇文章通过分析一个最简的C程序,引出了以下概念:
Stack Frame 栈框架和 SFP栈框架指针
Stack aligned 栈对齐
Calling Convention 调用约定和 ABI (Application Binary Interface)应用程序二进制接口
本章中,将通过进一步的实验,来深入了解这些概念。如果还不了解这些概念,可以参考X86汇编语言学习手记(1)。
1. 局部变量的栈分配
上篇文章已经分析过一个最简的C程序,
下面我们分析一下C编译器如何处理局部变量的分配,为此先给出如下程序:
#vi test2.c
int main()
{
int i;
int j=2;
i=3;
i=++i;
return i+j;
}
编译该程序,产生二进制文件,并利用mdb来观察程序运行中的stack的状态:
#gcc test2.c -o test2
#mdb test2
Loading modules: [ libc.so.1 ]
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp ; main至main+1,创建Stack Frame
main+3: subl $8,%esp ; 为局部变量i,j分配栈空间,并保证栈16字节对齐
main+6: andl $0xf0,%esp
main+9: movl $0,%eax
main+0xe: subl %eax,%esp ; main+6至main+0xe,再次保证栈16字节对齐
main+0x10: movl $2,-8(%ebp) ; 初始化局部变量j的值为2
main+0x17: movl $3,-4(%ebp) ; 给局部变量i赋值为3
main+0x1e: leal -4(%ebp),%eax ; 将局部变量i的地址装入到EAX寄存器中
main+0x21: incl (%eax) ; i++
main+0x23: movl -8(%ebp),%eax ; 将j的值装入EAX
main+0x26: addl -4(%ebp),%eax ; i+j并将结果存入EAX,作为返回值
main+0x29: leave ; 撤销Stack Frame
main+0x2a: ret ; main函数返回
>
> main+0x10:b ; 在地址 main+0x10处设置断点
> main+0x1e:b ; 在main+0x1e设置断点
> main+0x29:b ; 在main+0x1e设置断点
> main+0x2a:b ; 在main+0x1e设置断点
下面的mdb的4个命令在一行输入,中间用分号间隔开,命令的含义在注释中给出:
> :r;<esp,10/nap;<ebp=X;<eax=X ; 运行程序(:r命令)
mdb: stop at main+0x10;以ESP寄存器为起始地址,指定格式输出16字节的栈内容(<esp,10/nap命令)
mdb: target stopped at: ; 在最后输出EBP和EAX寄存器的值(<ebp=X命令和<eax=X命令)
main+0x10: movl $2,-8(%ebp) ; 在main +0x10处指令执行前中断,此时栈分配后还未初始化
0x8047db0:
0x8047db0: 0xddbebca0;这是变量j,4字节,未初始化,此处为栈顶,ESP的值就是0x8047db0
0x8047db4: 0xddbe137f ; 这是变量i, 4字节,未初始化
0x8047db8: 0x8047dd8 ;这是_start的SFP(_start的EBP),4字节,由main的SFP指向它
0x8047dbc: _start+0x5d;这是_start调用main之前压栈的下条指令地址,main返回后将恢复EIP
0x8047dc0: 1
0x8047dc4: 0x8047de4
0x8047dc8: 0x8047dec
0x8047dcc: _start+0x35
0x8047dd0: _fini
0x8047dd4: ld.so.1`atexit_fini
0x8047dd8: 0 ; _start的SFP指向的内容为0,证明_start是程序的入口
0x8047ddc: 0
0x8047de0: 1
0x8047de4: 0x8047eb4
0x8047de8: 0
0x8047dec: 0x8047eba
8047db8 ; 这是main当前EBP寄存器的值,即main的SFP
0 ; EAX的值,当前为0
> :c;<esp,10/nap;<ebp=X;<eax=X ; 继续运行程序(:c命令),其余3命令同上,打印16字节栈和EBP,EAX内容
mdb: stop at main+0x1e
mdb: target stopped at:
main+0x1e: leal -4(%ebp),%eax ;程序运行到断点main+0x1e处停止,此时局部变量i,j赋值已完成
0x8047db0:
0x8047db0: 2 ; 这是变量j,4字节,值为2,此处为栈顶,ESP的值就是0x8047db0
0x8047db4: 3 ; 这是变量i,4字节,值为3
0x8047db8: 0x8047dd8 ; 这是_start的SFP,4字节
0x8047dbc: _start+0x5d ; 这是返回_start后的EIP
0x8047dc0: 1
0x8047dc4: 0x8047de4
0x8047dc8: 0x8047dec
0x8047dcc: _start+0x35
0x8047dd0: _fini
0x8047dd4: ld.so.1`atexit_fini
0x8047dd8: 0
0x8047ddc: 0
0x8047de0: 1
0x8047de4: 0x8047eb4
0x8047de8: 0
0x8047dec: 0x8047eba
8047db8 ; 这是main当前EBP寄存器的值,即main的SFP
0 ; EAX的值,当前为0
> :c;<esp,10/nap;<ebp=X;<eax=X ; 继续运行程序,打印16字节栈和EBP,EAX内容
mdb: stop at main+0x29
mdb: target stopped at:
main+0x29: leave ; 运行到断点main+0x29处停止,计算已经完成,即将撤销Stack Frame
0x8047db0:
0x8047db0: 2 ; 这是变量j,4字节,值为2,此处为栈顶,ESP的值就是0x8047db0
0x8047db4: 4 ; 这是i++以后的变量i,4字节,值为3
0x8047db8: 0x8047dd8 ; 这是_start的SFP,4字节
0x8047dbc: _start+0x5d ; 这是返回_start后的EIP
0x8047dc0: 1
0x8047dc4: 0x8047de4
0x8047dc8: 0x8047dec
0x8047dcc: _start+0x35
0x8047dd0: _fini
0x8047dd4: ld.so.1`atexit_fini
0x8047dd8: 0
0x8047ddc: 0
0x8047de0: 1
0x8047de4: 0x8047eb4
0x8047de8: 0
0x8047dec: 0x8047eba
8047db8 ; 这是main当前EBP寄存器的值,即main的SFP
6 ; EAX的值,即函数的返回值,当前为6
> :c;<esp,10/nap;<ebp=X;<eax=X ; 继续运行程序,打印16字节栈和EBP,EAX内容
mdb: stop at main+0x2a
mdb: target stopped at:
main+0x2a: ret ; 运行到断点main+0x2a处停止,Stack Frame已被撤销,main即将返回
0x8047dbc:
0x8047dbc: _start+0x5d ;Stack Frame已经被撤销,栈顶是返回_start后的EIP,main的栈已释放
0x8047dc0: 1
0x8047dc4: 0x8047de4
0x8047dc8: 0x8047dec
0x8047dcc: _start+0x35
0x8047dd0: _fini
0x8047dd4: ld.so.1`atexit_fini
0x8047dd8: 0
0x8047ddc: 0
0x8047de0: 1
0x8047de4: 0x8047eb4
0x8047de8: 0
0x8047dec: 0x8047eba
0x8047df0: 0x8047ed6
0x8047df4: 0x8047edd
0x8047df8: 0x8047ee4
8047dd8 ;_start的SFP,之前存储在地址0x8047db8处,mainStack Frame撤销时恢复
6 ; EAX的值,即函数的返回值,当前为6
> :s;<esp,10/nap;<ebp=X;<eax=X ; 单步执行下条指令(:s命令),打印16字节栈和EBP,EAX内容
mdb: target stopped at:
_start+0x5d: addl $0xc,%esp ; 此时main已经返回,_start+0x5d曾经存储在地址0x8047dbc处
0x8047dc0:
0x8047dc0: 1 ; main已经返回,_start +0x5d已经被弹出
0x8047dc4: 0x8047de4
0x8047dc8: 0x8047dec
0x8047dcc: _start+0x35
0x8047dd0: _fini
0x8047dd4: ld.so.1`atexit_fini
0x8047dd8: 0 ; _start的SFP指向的内容为0,证明_start是程序的入口
0x8047ddc: 0
0x8047de0: 1
0x8047de4: 0x8047eb4
0x8047de8: 0
0x8047dec: 0x8047eba
0x8047df0: 0x8047ed6
0x8047df4: 0x8047edd
0x8047df8: 0x8047ee4
0x8047dfc: 0x8047ef3
8047dd8 ; _start的SFP,之前存储在地址0x8047db8处,main的Stack Frame撤销时恢复
6 ; EAX的值为6,还是main函数的返回值
>
通过mdb对程序运行时的寄存器和栈的观察和分析,可以得出局部变量在栈中的访问和分配及释放方式:
1.局部变量的分配,可以通过esp减去所需字节数
subl $8,%esp
2.局部变量的释放,可以通过leave指令
leave
3.局部变量的访问,可以通过ebp减去偏移量
movl -8(%ebp),%eax
addl -4(%ebp),%eax
问题:当存在2个以上的局部变量时,如何进行栈对齐?
在上篇文章中,提到subl $8,%esp语句除了分配栈空间外,还有一个作用就是栈对齐。那么本例中,由于i和j正好是8字节,那么如果存在2个以上的局部变量时,如何同时满足空间分配和栈对齐呢?
2. 两个以上的局部变量的栈分配
在之前的C程序中,增加局部变量定义k,程序如下:
# vi test3.c
int main()
{
int i, j=2, k=4;
i=3;
i=++i;
k=i+j+k;
return k;
}
编译该程序后,用mdb反汇编得出如下结果:
# gcc test3.c -o test3
# mdb test3
Loading modules: [ libc.so.1 ]
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp ; main至main+1,创建Stack Frame
main+3: subl $0x18,%esp ; 为局部变量i,j,k分配栈空间,并保证栈16字节对齐
main+6: andl $0xf0,%esp
main+9: movl $0,%eax
main+0xe: subl %eax,%esp ; main+6至main+0xe,再次保证栈16字节对齐
main+0x10: movl $2,-8(%ebp) ; j=2
main+0x17: movl $4,-0xc(%ebp) ; k=4
main+0x1e: movl $3,-4(%ebp) ; i=3
main+0x25: leal -4(%ebp),%eax ; 将i的地址装入到EAX
main+0x28: incl (%eax) ; i++
main+0x2a: movl -8(%ebp),%eax ; 将j的值装入到 EAX
main+0x2d: movl -4(%ebp),%edx ; 将i的值装入到 EDX
main+0x30: addl %eax,%edx ; j+i,结果存入EDX
main+0x32: leal -0xc(%ebp),%eax ; 将k的地址装入到EAX
main+0x35: addl %edx,(%eax) ; i+j+k,结果存入地址ebp-0xc即k中
main+0x37: movl -0xc(%ebp),%eax ; 将k的值装入EAX,作为返回值
main+0x3a: leave ; 撤销Stack Frame
main+0x3b: ret ; main函数返回
>
问题:为什么3个变量分配了0x18字节的栈空间?
在2个变量的时候,分配栈空间的指令是:subl $8,%esp
而在3个局部变量的时候,分配栈空间的指令是:subl $0x18,%esp
3个整型变量只需要0xc字节,为何实际上分配了0x18字节呢?
答案就是:保持16字节栈对齐。
在X86 汇编语言学习手记(1)里,已经说明过gcc默认的编译是要16字节栈对齐的,subl $8,%esp会使栈16字节对齐,而8字节空间只能满足2个局部变量,如果再分配4字节满足第3个局部变量的话,那栈地址就不再16字节对齐的,而同时满足空间需要而且保持16字节栈对齐的最接近的就是0x18。
如果,各定义一个50字节和100字节的字符数组,在这种情况下,实际分配多少栈空间呢?答案是0x8+0x40+0x70,即184字节。
下面动手验证一下:
# vi test4.c
int main()
{
char str1[50];
char str2[100];
return 0;
}
# mdb test4
Loading modules: [ libc.so.1 ]
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $0xb8,%esp ; 为两个字符数组分配栈空间,同时保证16字节对齐
main+9: andl $0xf0,%esp
main+0xc: movl $0,%eax
main+0x11: subl %eax,%esp
main+0x13: movl $0,%eax
main+0x18: leave
main+0x19: ret
> 0xb8=D ; 16进制换算10进制184
> 0x40+0x70+0x8=X ; 表达式计算,结果指定为16进制b8
>
问题:定义了多个局部变量时,栈分配顺序是怎样的?
局部变量栈分配的顺序是按照变量声明先后的顺序,同一行声明的变量是按照从左到右的顺序入栈的,在test2.c中,变量声明如下:
int i, j=2, k=4;
而反汇编的结果中:
movl $2,-8(%ebp) ; j=2
movl $4,-0xc(%ebp) ; k=4
movl $3,-4(%ebp) ; i=3
其中不难看出,i,j,k的栈中的位置如下图:
+----------------------------+------>高地址
| EIP (_start函数的返回地址) |
+----------------------------+
| EBP (_start函数的EBP) | <------ main函数的EBP指针(即SFP框架指针)
+----------------------------+
| i (EBP-4) |
+----------------------------+
| j (EBP-8) |
+----------------------------+
| k (EBP-0xc) |
+----------------------------+------> 低地址
图 2-1
3. 小结
这次通过几个试验程序,进一步了解了局部变量在栈中的分配和释放以及位置,并再次回顾了上篇文章中涉及到的以下概念:
SFP 栈框架指针
Stack aligned 栈对齐
并且,利用Solaris提供的mdb工具,直观的观察到了栈在程序运行中的动态变化,以及Stack Frame的创建和撤销,根据给出的图例的内容(图 2-1和图 1-1),可以更清晰的了解IA32架构中栈在内存中的布局(Stack Layer)。
X86汇编语言学习手记(3)
2004年12月
在X86汇编语言学习手记(1)(2)中,可以看到栈(Stack)作为进程执行过程中数据的临时存储区域,通常包含如下几类数据:
局部变量
函数调用的返回地址
函数调用的入口参数
SFP 栈框架指针 (可以通过编译器优化选项去除)
本章中,将继续通过实验,了解全局变量和静态变量在进程中是如何存储和分配的。
注:不同的Calling Convention对入口参数的规定是有一定差别的,函数调用入口参数也有可能通过寄存器来传递。
例如IBM的Power PC和AMD的Opteron,函数的入口参数全部或部分就是通过寄存器来传递的。
1. 全局变量和全局常量的实验
延续之前的方式,给出一个简单的C程序,其中声明的全局变量分为3种:
初始化过的全局变量
未初始化的全局变量
全局常量
#vi test5.c
int i=1;
int j=2;
int k=3;
int l,m;
int n;
const int o=7;
const int p=8;
const int q=9;
int main()
{
l=4;
m=5;
n=6;
return i+j+k+l+m+n+o+p+q;
}
# gcc test5.c -o test5
# mdb test5
Loading modules: [ libc.so.1 ]
> main::dis
main: pushl %ebp ; main至main+1,创建Stack Frame
main+1: movl %esp,%ebp
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: movl $0,%eax
main+0xe: subl %eax,%esp ; main+(3-0xe),为局部变量预留栈空间,并保证栈16字节对齐
main+0x10: movl $4,0x8060948 ; l=4
main+0x1a: movl $5,0x806094c ; m=5
main+0x24: movl $6,0x8060950 ; n=6
main+0x2e: movl 0x8060908,%eax
main+0x33: addl 0x8060904,%eax
main+0x39: addl 0x806090c,%eax
main+0x3f: addl 0x8060948,%eax
main+0x45: addl 0x806094c,%eax
main+0x4b: addl 0x8060950,%eax
main+0x51: addl 0x8050808,%eax
main+0x57: addl 0x805080c,%eax
main+0x5d: addl 0x8050810,%eax ; main+0x2e至main+0x5d,i+j+k+l+m+n+o+p+q
main+0x63: leave ; 撤销Stack Frame
main+0x64: ret ; main函数返回
现在,让我们在全局变量初始化后的地方设置断点,观察一下这几个全局变量的值:
> main+0x2e:b ; 设置断点
> :r ; 运行程序
mdb: stop at main+0x2e
mdb: target stopped at:
main+0x2e: movl 0x8060908,%eax
> 0x8060904,03/nap ; 察看全局变量 i,j,k的值
test5`i:
test5`i:
test5`i: 1
test5`j: 2
test5`k: 3
> 0x8060948,03/nap ; 察看全局变量l,m,n的值
test5`l:
test5`l:
test5`l: 4
test5`m: 5
test5`n: 6
> 0x8050808,03/nap ; 察看全局变量o,p,q的值
o:
o:
o: 7
p: 8
q: 9
>
概念:进程地址空间 Process Address Space
+----------------------+ ----> 0xFFFFFFFF (4GB)
| |
| Kernel Space |
| |
+----------------------+ ----> _kernel_base (0xE0000000)
| |
| Other Library |
: :
: :
| |
+----------------------+
| data section |
| Lib C Library |
| text section |
: :
: :
+----------------------+
| |
| |
: :
: grow up :
: :
| User Heap |
| |
+----------------------+
| bss |
| |
| User Data |
| |
+----------------------+
| |
| User Text |
| |
| |
+----------------------+ ----> 0x08050000
| |
| User Stack |
| |
: grow down :
: :
: :
| |
| |
+----------------------+ ----> 0
图 3-1 Solaris在IA32上的进程地址空间
如图3-1所示,Solaris在IA32上的进程地址空间和Linux是相似的,在用户进程的4GB地址空间内:
Kernel总是映射到用户地址空间的最高端,从宏定义_kernel_base至0xFFFFFFFF的区域
用户进程所依赖的各个共享库紧接着Kernel映射在用户地址空间的高端
最后是用户进程地址空间在地址空间的低端
各共享库的代码段,存放着二进制可执行的机器指令,是由kernel把该库ELF文件的代码段map到虚存空间,属性是read/exec/share
各共享库的数据段,存放着程序执行所需的全局变量,是由kernel把ELF文件的数据段map到虚存空间,属性为read/write/private
用户代码段,存放着二进制形式的可执行的机器指令,是由kernel把ELF文件的代码段map到虚存空间,属性为read/exec
用户代码段之上是数据段,存放着程序执行所需的全局变量,是由kernel把ELF文件的数据段map到虚存空间,属性为 read/write/private
用户代码段之下是栈(stack),作为进程的临时数据区,是由kernel把匿名内存map到虚存空间,属性为read/write/exec
用户数据段之上是堆(heap),当且仅当malloc调用时存在,是由kernel把匿名内存map到虚存空间,属性为read/write/exec
注意Stack和Heap的区别和联系:
相同点:
1. 都是来自于kernel分配的匿名内存,和磁盘上的ELF文件无关
2. 属性均为read/write/exec
不同点:
1.栈的分配在C语言层面一般是通过声明局部变量,调用函数引起的;堆的分配则是通过显式的调用(malloc)引起的
2.栈的释放在C语言层面是对用户透明的,用户不需要关心,由C编译器产生的相应的指令代劳;堆则需显式的调用(free)来释放
3.栈空间的增长方向是从高地址到低地址;堆空间的增长方向是由低地址到高地址
4.栈存在于任何进程的地址空间;堆则在程序中没有调用malloc的情况下不存在
用户地址空间的布局随着CPU和OS的不同,略有差异,以上都是基于X86 CPU在Solaris OS上的情况的讨论。
使用pmap命令,可以观察到系统中的指定进程的地址空间分布情况,下面就是用pmap观察bash进程的一个例子:
# pmap 1030
1030: -bash
08045000 12K rw--- [ stack ] ; bash的栈
08050000 444K r-x-- /usr/bin/bash ; bash文本段
080CE000 72K rwx-- /usr/bin/bash ; bash的数据段
080E0000 156K rwx-- [ heap ] ; bash的堆
DD8C0000 8K r-x-- /usr/lib/locale/zh_CN.GB18030/methods_zh_CN.GB18030.so.2; so的文本段
DD8D1000 4K rwx-- /usr/lib/locale/zh_CN.GB18030/methods_zh_CN.GB18030.so.2 ; so的数据段
DD8E0000 324K r-x-- /usr/lib/locale/zh_CN.GB18030/zh_CN.GB18030.so.2
DD940000 8K rwx-- /usr/lib/locale/zh_CN.GB18030/zh_CN.GB18030.so.2
DD950000 4K rwx-- [ anon ] ; 匿名内存,由映射/dev/zero设备来创建的
DD960000 12K r-x-- /usr/lib/libmp.so.2
DD973000 4K rwx-- /usr/lib/libmp.so.2
DD980000 628K r-x-- /usr/lib/libc.so.1
DDA2D000 24K rwx-- /usr/lib/libc.so.1
DDA33000 4K rwx-- /usr/lib/libc.so.1
DDA50000 4K rwx-- [ anon ]
DDA60000 548K r-x-- /usr/lib/libnsl.so.1
DDAF9000 20K rwx-- /usr/lib/libnsl.so.1
DDAFE000 32K rwx-- /usr/lib/libnsl.so.1
DDB10000 44K r-x-- /usr/lib/libsocket.so.1
DDB2B000 4K rwx-- /usr/lib/libsocket.so.1
DDB30000 152K r-x-- /usr/lib/libcurses.so.1
DDB66000 28K rwx-- /usr/lib/libcurses.so.1
DDB6D000 8K rwx-- /usr/lib/libcurses.so.1
DDB80000 4K r-x-- /usr/lib/libdl.so.1
DDB90000 292K r-x-- /usr/lib/ld.so.1
DDBE9000 16K rwx-- /usr/lib/ld.so.1
DDBED000 8K rwx-- /usr/lib/ld.so.1
total 2864K
问题:全局变量和全局常量在进程地址空间的位置?
显然,根据前面的叙述,全局变量在用户的数据段,那么全局常量呢,是数据段吗?
同样的,可以利用mdb将test5进程挂起,然后用pmap命令求证一下:
# mdb test5
Loading modules: [ libc.so.1 ]
> ::sysbp _exit ; 在系统调用_exit处设置断点
> :r ; 运行程序
mdb: stop on entry to _exit
mdb: target stopped at:
libc.so.1`exit+0x2b: jae +0x15 <libc.so.1`exit+0x40>
>
此时,程序运行后在_exit处挂起,可以利用pmap在另一个终端内查看test5进程的地址空间了:
# ps -ef | grep test5
root 1387 1386 0 02:23:53 pts/1 0:00 test5
root 1399 1390 0 02:25:03 pts/3 0:00 grep test5
root 1386 1338 0 02:23:41 pts/1 0:00 mdb test5
# pmap -F 1387 ; 用pmap强制查看
1387: test5
08044000 16K rwx-- [ stack ] ; test5的stack
08050000 4K r-x-- /export/home/asm/L3/test5 ; test5的代码段,起始地址为0x08050000
08060000 4K rwx-- /export/home/asm/L3/test5 ; test5的数据段,起始地址为0x08060000
DDAC0000 628K r-x-- /usr/lib/libc.so.1
DDB6D000 24K rwx-- /usr/lib/libc.so.1
DDB73000 4K rwx-- /usr/lib/libc.so.1
DDB80000 4K r-x-- /usr/lib/libdl.so.1
DDB90000 292K r-x-- /usr/lib/ld.so.1
DDBE9000 16K rwx-- /usr/lib/ld.so.1
DDBED000 8K rwx-- /usr/lib/ld.so.1
total 1000K
可以看到,由于test5程序没有使用malloc来申请内存,所以没有heap的映射
前面用mdb观察过这些全局变量和常量的初始化值,它们的地址分别是:
全局变量i,j,k:
0x8060904起始的12字节
全局变量l,m,n:
0x8060948起始的12字节
全局常量o,p,q:
0x8050808起始的12字节
显然,根据这些变量的地址,我们可以初步判断出这些变量属于哪个段:
由于test5数据段起始地址为0x08060000,我们得出结论:全局变量i,j,k,l,m,n属于数据段
而test5代码段的起始地址为0x08050000,我们得出结论:全局常量o,p,q属于代码段
得出这个结论的确有点让人意外:全局常量竟然在代码段。
却又似乎在情理之中:数据段内存映射后的属性是r/w/x,而常量要求是只读属性,所以在代码段(r-x)就合情合理了。
问题:为什么这些全局变量地址不是连续的?
很容易注意到,全局变量i,j,k和l,m,n以及全局常量o,p,q是连续声明的,但地址实际上并不连续,而是在3段连续12字节的地址上。
当然,全局常量属于代码段,所以地址和全局变量是分开的;那么,为什么全局变量也并非连续呢?
前面谈到数据段实际上是从ELF格式的二进制文件映射到进程的地址空间的,就通过分析ELF文件格式来寻找答案吧:
# file test5
test5: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped
# elfdump test5
ELF Header ; ELF头信息共52(0x34)字节,具体意义可以参考ELF format的相关文档
ei_magic: { 0x7f, E, L, F } ; ELF的幻数
ei_class: ELFCLASS32 ei_data: ELFDATA2LSB ; 32位的ELF文件,小端(LSB)编码
e_machine: EM_386 e_version: EV_CURRENT ; Intel 80386,版本1
e_type: ET_EXEC ; 可执行文件
e_flags: 0
e_entry: 0x8050600 e_ehsize: 52 e_shstrndx: 27 ; 程序入口点_start的地址0x8050600
e_shoff: 0x1584 e_shentsize: 40 e_shnum: 29 ; Section header table的大小是29*40
e_phoff: 0x34 e_phentsize: 32 e_phnum: 5
Program Header[0]: ; 描述Program header table本身在内存中如何映射
p_vaddr: 0x8050034 p_flags: [ PF_X PF_R ]
p_paddr: 0 p_type: [ PT_PHDR ]
p_filesz: 0xa0 p_memsz: 0xa0
p_offset: 0x34 p_align: 0
Program Header[1]: ; 描述程序装载器的路径名(.interp section)存放在文件的位置
p_vaddr: 0 p_flags: [ PF_R ]
p_paddr: 0 p_type: [ PT_INTERP ]
p_filesz: 0x11 p_memsz: 0
p_offset: 0xd4 p_align: 0
Program Header[2]: ; 描述代码段在内存中如何映射,起始地址0x8050000,大小为 0x814
p_vaddr: 0x8050000 p_flags: [ PF_X PF_R ]
p_paddr: 0 p_type: [ PT_LOAD ]
p_filesz: 0x814 p_memsz: 0x814
p_offset: 0 p_align: 0x10000
Program Header[3]: ; 描述数据段在内存中如何映射,起始地址0x8060814,大小为0x144
p_vaddr: 0x8060814 p_flags: [ PF_X PF_W PF_R ]
p_paddr: 0 p_type: [ PT_LOAD ]
p_filesz: 0x118 p_memsz: 0x144
p_offset: 0x814 p_align: 0x10000
Program Header[4]: ; 描述动态链接信息(.dynamic section)在内存中如何映射
p_vaddr: 0x8060848 p_flags: [ PF_X PF_W PF_R ]
p_paddr: 0 p_type: [ PT_DYNAMIC ]
p_filesz: 0xb8 p_memsz: 0
p_offset: 0x848 p_align: 0
Section Header[1]: sh_name: .interp ; 该section保存了程序的解释程序(interpreter)的路径
sh_addr: 0x80500d4 sh_flags: [ SHF_ALLOC ]
sh_size: 0x11 sh_type: [ SHT_PROGBITS ]
sh_offset: 0xd4 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header[2]: sh_name: .hash ; 该section保存着一个符号的哈希表
sh_addr: 0x80500e8 sh_flags: [ SHF_ALLOC ]
sh_size: 0x104 sh_type: [ SHT_HASH ]
sh_offset: 0xe8 sh_entsize: 0x4
sh_link: 3 sh_info: 0
sh_addralign: 0x4
Section Header[3]: sh_name: .dynsym ; 该section保存着动态符号表
sh_addr: 0x80501ec sh_flags: [ SHF_ALLOC ]
sh_size: 0x200 sh_type: [ SHT_DYNSYM ]
sh_offset: 0x1ec sh_entsize: 0x10
sh_link: 4 sh_info: 1
sh_addralign: 0x4
Section Header[4]: sh_name: .dynstr ; 该section保存着动态连接时需要的字符串
sh_addr: 0x80503ec sh_flags: [ SHF_ALLOC SHF_STRINGS ]
sh_size: 0x11a sh_type: [ SHT_STRTAB ]
sh_offset: 0x3ec sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header[5]: sh_name: .SUNW_version ; 该section是SUN扩展的,保存版本信息
sh_addr: 0x8050508 sh_flags: [ SHF_ALLOC ]
sh_size: 0x20 sh_type: [ SHT_SUNW_verneed ]
sh_offset: 0x508 sh_entsize: 0
sh_link: 4 sh_info: 1
sh_addralign: 0x4
Section Header[6]: sh_name: .rel.got ; 该section保存着.got section中部分符号的重定位信息
sh_addr: 0x8050528 sh_flags: [ SHF_ALLOC SHF_INFO_LINK ]
sh_size: 0x18 sh_type: [ SHT_REL ]
sh_offset: 0x528 sh_entsize: 0x8
sh_link: 3 sh_info: 14
sh_addralign: 0x4
Section Header[7]: sh_name: .rel.bss ; 该section保存着.bss section中部分符号的重定位信息
sh_addr: 0x8050540 sh_flags: [ SHF_ALLOC SHF_INFO_LINK ]
sh_size: 0x8 sh_type: [ SHT_REL ]
sh_offset: 0x540 sh_entsize: 0x8
sh_link: 3 sh_info: 22
sh_addralign: 0x4
Section Header[8]: sh_name: .rel.plt ; 该section保存着.plt section中部分符号的重定位信息
sh_addr: 0x8050548 sh_flags: [ SHF_ALLOC SHF_INFO_LINK ]
sh_size: 0x38 sh_type: [ SHT_REL ]
sh_offset: 0x548 sh_entsize: 0x8
sh_link: 3 sh_info: 9
sh_addralign: 0x4
Section Header[9]: sh_name: .plt ; 该section保存着过程连接表(Procedure Linkage Table)
sh_addr: 0x8050580 sh_flags: [ SHF_ALLOC SHF_EXECINSTR ]
sh_size: 0x80 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x580 sh_entsize: 0x10
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[10]: sh_name: .text ; 该section保存着程序的正文部分,即可执行指令
sh_addr: 0x8050600 sh_flags: [ SHF_ALLOC SHF_EXECINSTR ]
sh_size: 0x1ec sh_type: [ SHT_PROGBITS ]
sh_offset: 0x600 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[11]: sh_name: .init ; 该section保存着可执行指令,它构成了进程的初始化代码
sh_addr: 0x80507ec sh_flags: [ SHF_ALLOC SHF_EXECINSTR ]
sh_size: 0xd sh_type: [ SHT_PROGBITS ]
sh_offset: 0x7ec sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header[12]: sh_name: .fini ; 该section保存着可执行指令,它构成了进程的终止代码
sh_addr: 0x80507f9 sh_flags: [ SHF_ALLOC SHF_EXECINSTR ]
sh_size: 0x8 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x7f9 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header[13]: sh_name: .rodata ; 该section保存着只读数据
sh_addr: 0x8050804 sh_flags: [ SHF_ALLOC ]
sh_size: 0x10 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x804 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[14]: sh_name: .got ; 该section保存着全局的偏移量表
sh_addr: 0x8060814 sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x34 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x814 sh_entsize: 0x4
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[15]: sh_name: .dynamic ; 该section保存着动态连接的信息
sh_addr: 0x8060848 sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0xb8 sh_type: [ SHT_DYNAMIC ]
sh_offset: 0x848 sh_entsize: 0x8
sh_link: 4 sh_info: 0
sh_addralign: 0x4
Section Header[16]: sh_name: .data ; 该sections保存着初始化了的数据
sh_addr: 0x8060900 sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x10 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x900 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[17]: sh_name: .ctors
sh_addr: 0x8060910 sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x8 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x910 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[18]: sh_name: .dtors
sh_addr: 0x8060918 sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x8 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x918 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[19]: sh_name: .eh_frame
sh_addr: 0x8060920 sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x4 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x920 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[20]: sh_name: .jcr
sh_addr: 0x8060924 sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x4 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x924 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[21]: sh_name: .data.rel.local
sh_addr: 0x8060928 sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x4 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x928 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[22]: sh_name: .bss ; 该sectiopn保存着未初始化的数据
sh_addr: 0x806092c sh_flags: [ SHF_WRITE SHF_ALLOC ]
sh_size: 0x2c sh_type: [ SHT_NOBITS ] ; 指示不占据ELF空间sh_size是内存大小
sh_offset: 0x92c sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[23]: sh_name: .symtab ; 该section保存着一个符号表
sh_addr: 0 sh_flags: 0
sh_size: 0x540 sh_type: [ SHT_SYMTAB ]
sh_offset: 0x92c sh_entsize: 0x10
sh_link: 24 sh_info: 53
sh_addralign: 0x4
Section Header[24]: sh_name: .strtab ; 该section保存着字符串表
sh_addr: 0 sh_flags: [ SHF_STRINGS ]
sh_size: 0x20b sh_type: [ SHT_STRTAB ]
sh_offset: 0xe6c sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header[25]: sh_name: .comment ; 该section保存着版本控制信息
sh_addr: 0 sh_flags: 0
sh_size: 0x24d sh_type: [ SHT_PROGBITS ]
sh_offset: 0x1077 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header[26]: sh_name: .stab.index
sh_addr: 0 sh_flags: 0
sh_size: 0x24 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x12c4 sh_entsize: 0xc
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header[27]: sh_name: .shstrtab ; 该section保存着section名称
sh_addr: 0 sh_flags: [ SHF_STRINGS ]
sh_size: 0xdc sh_type: [ SHT_STRTAB ]
sh_offset: 0x12e8 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header[28]: sh_name: .stab.indexstr
sh_addr: 0 sh_flags: 0
sh_size: 0x1c0 sh_type: [ SHT_STRTAB ]
sh_offset: 0x13c4 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Interpreter:
/usr/lib/ld.so.1
Version Needed Section: .SUNW_version
file version
libc.so.1 SYSVABI_1.3
Symbol Table: .dynsym ; 动态解析和链接所需的符号表
index value size type bind oth ver shndx name
[0] 0x00000000 0x00000000 NOTY LOCL D 0 UNDEF
[1] 0x080507ec 0x0000000d FUNC GLOB D 0 .init _init
[2] 0x08050804 0x00000004 OBJT GLOB D 0 .rodata _lib_version
[3] 0x08050580 0x00000000 OBJT GLOB D 0 .plt _PROCEDURE_LINKAGE_TABLE_
[4] 0x08050600 0x00000075 FUNC GLOB D 0 .text _start
[5] 0x08060900 0x00000000 OBJT GLOB D 0 .data __dso_handle
[6] 0x08060848 0x00000000 OBJT GLOB D 0 .dynamic _DYNAMIC
[7] 0x00000000 0x00000000 NOTY WEAK D 0 UNDEF __deregister_frame_info_bases
[8] 0x08050814 0x00000000 OBJT GLOB D 0 .rodata _etext
[9] 0x08060958 0x00000000 OBJT GLOB D 0 .bss _end
[10] 0x08050590 0x00000000 FUNC WEAK D 0 UNDEF _cleanup
[11] 0x08050675 0x00000001 FUNC WEAK D 0 .text _mcount
[12] 0x08060904 0x00000004 OBJT GLOB D 0 .data i
[13] 0x08060908 0x00000004 OBJT GLOB D 0 .data j
[14] 0x0806090c 0x00000004 OBJT GLOB D 0 .data k
[15] 0x08060948 0x00000004 OBJT GLOB D 0 .bss l
[16] 0x08060954 0x00000004 OBJT GLOB D 0 .bss _environ
[17] 0x08060814 0x00000000 OBJT GLOB D 0 .got _GLOBAL_OFFSET_TABLE_
[18] 0x0806094c 0x00000004 OBJT GLOB D 0 .bss m
[19] 0x0806092c 0x00000000 OBJT GLOB D 0 .data.rel.l _edata
[20] 0x08060954 0x00000004 OBJT WEAK D 0 .bss environ
[21] 0x080507f9 0x00000008 FUNC GLOB D 0 .fini _fini
[22] 0x080505a0 0x00000000 FUNC GLOB D 0 UNDEF atexit
[23] 0x08060950 0x00000004 OBJT GLOB D 0 .bss n
[24] 0x08050808 0x00000004 OBJT GLOB D 0 .rodata o
[25] 0x0805080c 0x00000004 OBJT GLOB D 0 .rodata p
[26] 0x00000000 0x00000000 NOTY WEAK D 0 UNDEF _Jv_RegisterClasses
[27] 0x08050810 0x00000004 OBJT GLOB D 0 .rodata q
[28] 0x080505b0 0x00000000 FUNC GLOB D 0 UNDEF __fpstart
[29] 0x08050753 0x00000065 FUNC GLOB D 0 .text main
[30] 0x00000000 0x00000000 NOTY WEAK D 0 UNDEF __register_frame_info_bases
[31] 0x080505c0 0x00000000 FUNC GLOB D 0 UNDEF exit
Symbol Table: .symtab ; 程序链接所需的符号表
index value size type bind oth ver shndx name
[0] 0x00000000 0x00000000 NOTY LOCL D 0 UNDEF
[1] 0x00000000 0x00000000 FILE LOCL D 0 ABS test5
[2] 0x080500d4 0x00000000 SECT LOCL D 0 .interp
[3] 0x080500e8 0x00000000 SECT LOCL D 0 .hash
[4] 0x080501ec 0x00000000 SECT LOCL D 0 .dynsym
[5] 0x080503ec 0x00000000 SECT LOCL D 0 .dynstr
[6] 0x08050508 0x00000000 SECT LOCL D 0 .SUNW_versi
[7] 0x08050528 0x00000000 SECT LOCL D 0 .rel.got
[8] 0x08050540 0x00000000 SECT LOCL D 0 .rel.bss
[9] 0x08050548 0x00000000 SECT LOCL D 0 .rel.plt
[10] 0x08050580 0x00000000 SECT LOCL D 0 .plt
[11] 0x08050600 0x00000000 SECT LOCL D 0 .text
[12] 0x080507ec 0x00000000 SECT LOCL D 0 .init
[13] 0x080507f9 0x00000000 SECT LOCL D 0 .fini
[14] 0x08050804 0x00000000 SECT LOCL D 0 .rodata
[15] 0x08060814 0x00000000 SECT LOCL D 0 .got
[16] 0x08060848 0x00000000 SECT LOCL D 0 .dynamic
[17] 0x08060900 0x00000000 SECT LOCL D 0 .data
[18] 0x08060910 0x00000000 SECT LOCL D 0 .ctors
[19] 0x08060918 0x00000000 SECT LOCL D 0 .dtors
[20] 0x08060920 0x00000000 SECT LOCL D 0 .eh_frame
[21] 0x08060924 0x00000000 SECT LOCL D 0 .jcr
[22] 0x08060928 0x00000000 SECT LOCL D 0 .data.rel.l
[23] 0x0806092c 0x00000000 SECT LOCL D 0 .bss
[24] 0x00000000 0x00000000 SECT LOCL D 0 .symtab
[25] 0x00000000 0x00000000 SECT LOCL D 0 .strtab
[26] 0x00000000 0x00000000 SECT LOCL D 0 .comment
[27] 0x00000000 0x00000000 SECT LOCL D 0 .stab.index
[28] 0x00000000 0x00000000 SECT LOCL D 0 .shstrtab
[29] 0x00000000 0x00000000 SECT LOCL D 0 .stab.index
[30] 0x08050000 0x00000000 OBJT LOCL D 0 .interp _START_
[31] 0x08060958 0x00000000 OBJT LOCL D 0 .bss _END_
[32] 0x00000000 0x00000000 FILE LOCL D 0 ABS crt1.s
[33] 0x00000000 0x00000000 FILE LOCL D 0 ABS crti.s
[34] 0x00000000 0x00000000 FILE LOCL D 0 ABS values-Xa.c
[35] 0x00000000 0x00000000 FILE LOCL D 0 ABS crtstuff.c
[36] 0x08060910 0x00000000 OBJT LOCL D 0 .ctors __CTOR_LIST__
[37] 0x08060918 0x00000000 OBJT LOCL D 0 .dtors __DTOR_LIST__
[38] 0x08060920 0x00000000 OBJT LOCL D 0 .eh_frame __EH_FRAME_BEGIN__
[39] 0x08060924 0x00000000 OBJT LOCL D 0 .jcr __JCR_LIST__
[40] 0x08060928 0x00000000 OBJT LOCL D 0 .data.rel.l p.0
[41] 0x0806092c 0x00000001 OBJT LOCL D 0 .bss completed.1
[42] 0x08050678 0x00000000 FUNC LOCL D 0 .text __do_global_dtors_aux
[43] 0x08060930 0x00000018 OBJT LOCL D 0 .bss object.2
[44] 0x080506e4 0x00000000 FUNC LOCL D 0 .text frame_dummy
[45] 0x00000000 0x00000000 FILE LOCL D 0 ABS test5.c
[46] 0x00000000 0x00000000 FILE LOCL D 0 ABS crtstuff.c
[47] 0x08060914 0x00000000 OBJT LOCL D 0 .ctors __CTOR_END__
[48] 0x0806091c 0x00000000 OBJT LOCL D 0 .dtors __DTOR_END__
[49] 0x08060920 0x00000000 OBJT LOCL D 0 .eh_frame __FRAME_END__
[50] 0x08060924 0x00000000 OBJT LOCL D 0 .jcr __JCR_END__
[51] 0x080507b8 0x00000000 FUNC LOCL D 0 .text __do_global_ctors_aux
[52] 0x00000000 0x00000000 FILE LOCL D 0 ABS crtn.o
[53] 0x080507ec 0x0000000d FUNC GLOB D 0 .init _init
[54] 0x08050804 0x00000004 OBJT GLOB D 0 .rodata _lib_version
[55] 0x08050580 0x00000000 OBJT GLOB D 0 .plt _PROCEDURE_LINKAGE_TABLE_
[56] 0x08050600 0x00000075 FUNC GLOB D 0 .text _start
[57] 0x08060900 0x00000000 OBJT GLOB D 0 .data __dso_handle
[58] 0x08060848 0x00000000 OBJT GLOB D 0 .dynamic _DYNAMIC
[59] 0x00000000 0x00000000 NOTY WEAK D 0 UNDEF __deregister_frame_info_bases
[60] 0x08050814 0x00000000 OBJT GLOB D 0 .rodata _etext
[61] 0x08060958 0x00000000 OBJT GLOB D 0 .bss _end
[62] 0x08050590 0x00000000 FUNC WEAK D 0 UNDEF _cleanup
[63] 0x08050675 0x00000001 FUNC WEAK D 0 .text _mcount
[64] 0x08060904 0x00000004 OBJT GLOB D 0 .data i
[65] 0x08060908 0x00000004 OBJT GLOB D 0 .data j
[66] 0x0806090c 0x00000004 OBJT GLOB D 0 .data k
[67] 0x08060948 0x00000004 OBJT GLOB D 0 .bss l
[68] 0x08060954 0x00000004 OBJT GLOB D 0 .bss _environ
[69] 0x08060814 0x00000000 OBJT GLOB D 0 .got _GLOBAL_OFFSET_TABLE_
[70] 0x0806094c 0x00000004 OBJT GLOB D 0 .bss m
[71] 0x0806092c 0x00000000 OBJT GLOB D 0 .data.rel.l _edata
[72] 0x08060954 0x00000004 OBJT WEAK D 0 .bss environ
[73] 0x080507f9 0x00000008 FUNC GLOB D 0 .fini _fini
[74] 0x080505a0 0x00000000 FUNC GLOB D 0 UNDEF atexit
[75] 0x08060950 0x00000004 OBJT GLOB D 0 .bss n
[76] 0x08050808 0x00000004 OBJT GLOB D 0 .rodata o
[77] 0x0805080c 0x00000004 OBJT GLOB D 0 .rodata p
[78] 0x00000000 0x00000000 NOTY WEAK D 0 UNDEF _Jv_RegisterClasses
[79] 0x08050810 0x00000004 OBJT GLOB D 0 .rodata q
[80] 0x080505b0 0x00000000 FUNC GLOB D 0 UNDEF __fpstart
[81] 0x08050753 0x00000065 FUNC GLOB D 0 .text main
[82] 0x00000000 0x00000000 NOTY WEAK D 0 UNDEF __register_frame_info_bases
[83] 0x080505c0 0x00000000 FUNC GLOB D 0 UNDEF exit
Hash Section: .hash
bucket symndx name
0 [1] _init
1 [2] _lib_version
[3] _PROCEDURE_LINKAGE_TABLE_
2 [4] _start
[5] __dso_handle
4 [6] _DYNAMIC
9 [7] __deregister_frame_info_bases
[8] _etext
10 [9] _end
12 [10] _cleanup
[11] _mcount
[12] i
13 [13] j
14 [14] k
15 [15] l
16 [16] _environ
[17] _GLOBAL_OFFSET_TABLE_
[18] m
[19] _edata
[20] environ
17 [21] _fini
[22] atexit
[23] n
18 [24] o
19 [25] p
20 [26] _Jv_RegisterClasses
[27] q
25 [28] __fpstart
26 [29] main
29 [30] __register_frame_info_bases
[31] exit
13 buckets contain 0 symbols
10 buckets contain 1 symbols
5 buckets contain 2 symbols
2 buckets contain 3 symbols
1 buckets contain 5 symbols
31 buckets 31 symbols (globals)
Global Offset Table: 13 entries
ndx addr value reloc addend symbol
[00000] 08060814 08060848 R_386_NONE 00000000
[00001] 08060818 00000000 R_386_NONE 00000000
[00002] 0806081c 00000000 R_386_NONE 00000000
[00003] 08060820 08050596 R_386_JMP_SLOT 00000000 _cleanup
[00004] 08060824 080505a6 R_386_JMP_SLOT 00000000 atexit
[00005] 08060828 080505b6 R_386_JMP_SLOT 00000000 __fpstart
[00006] 0806082c 080505c6 R_386_JMP_SLOT 00000000 exit
[00007] 08060830 00000000 R_386_GLOB_DAT 00000000 __deregister_frame_info_bases
[00008] 08060834 080505d6 R_386_JMP_SLOT 00000000 __deregister_frame_info_bases
[00009] 08060838 00000000 R_386_GLOB_DAT 00000000 __register_frame_info_bases
[00010] 0806083c 00000000 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses
[00011] 08060840 080505e6 R_386_JMP_SLOT 00000000 _Jv_RegisterClasses
[00012] 08060844 080505f6 R_386_JMP_SLOT 00000000 __register_frame_info_bases
Relocation: .rel.got
type offset section with respect to
R_386_GLOB_DAT 0x8060830 .rel.got __deregister_frame_info_bases
R_386_GLOB_DAT 0x8060838 .rel.got __register_frame_info_bases
R_386_GLOB_DAT 0x806083c .rel.got _Jv_RegisterClasses
Relocation: .rel.bss
type offset section with respect to
R_386_COPY 0x8060954 .rel.bss _environ
Relocation: .rel.plt
type offset section with respect to
R_386_JMP_SLOT 0x8060820 .rel.plt _cleanup
R_386_JMP_SLOT 0x8060824 .rel.plt atexit
R_386_JMP_SLOT 0x8060828 .rel.plt __fpstart
R_386_JMP_SLOT 0x806082c .rel.plt exit
R_386_JMP_SLOT 0x8060834 .rel.plt __deregister_frame_info_bases
R_386_JMP_SLOT 0x8060840 .rel.plt _Jv_RegisterClasses
R_386_JMP_SLOT 0x8060844 .rel.plt __register_frame_info_bases
Dynamic Section: .dynamic
index tag value
[0] NEEDED 0x104 libc.so.1
[1] INIT 0x80507ec
[2] FINI 0x80507f9
[3] HASH 0x80500e8
[4] STRTAB 0x80503ec
[5] STRSZ 0x11a
[6] SYMTAB 0x80501ec
[7] SYMENT 0x10
[8] CHECKSUM 0x6a10
[9] VERNEED 0x8050508
[10] VERNEEDNUM 0x1
[11] PLTRELSZ 0x38
[12] PLTREL 0x11
[13] JMPREL 0x8050548
[14] REL 0x8050528
[15] RELSZ 0x58
[16] RELENT 0x8
[17] DEBUG 0
[18] FEATURE_1 0x1 [ PARINIT ]
[19] FLAGS 0 0
[20] FLAGS_1 0 0
[21] PLTGOT 0x8060814
利用elfdump可以查看ELF文件格式的详细信息,可以在符号表.dynsym和.symtab中找到程序中定义的全局变量和全局常量:
i,j,k在.data section中
l,m,n在.bss section中
o,p,q在.rodata section中
概念:ELF(Executable and Linking Format)可执行连接格式
ELF格式是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。
目前,ELF格式是Unix/Linux平台上应用最广泛的二进制工业标准之一
下图从不同视角给出了ELF文件的一般格式:
Linking 视角 Execution视角
============ ==============
ELF header ELF header
Program header table (optional) Program header table
Section 1 Segment 1
... Segment 2
Section n ...
Section header table Section header table (optional)
图 3-2 ELF文件格式摘自 EXECUTABLE AND LINKABLE FORMAT (ELF)
可以根据test5 ELF文件的Program header table和Section header table中文件偏移量的信息描绘出test5的内容:
entry name 起始文件偏移+实际大小=下个entry起始偏移
-------------------------------------------------------
ELF header 0x0+0x34=0x34
Program header 0x34+0xa0=0xd4
Section 1 .interp 0xd4+0x11=0xe5
000 0xe5+0x3=0xe8
Section 2 .hash 0xe8+0x104=0x1ec
Section 3 .dynsym 0x1ec+0x200=0x3ec
Section 4 .dynstr 0x3ec+0x11a=0x506
00 0x506+0x2=0x508
Section 5 .SUNW_version 0x508+0x20=0x528
Section 6 .rel.got 0x528+0x18=0x540
Section 7 .rel.bss 0x540+0x8=0x548
Section 8 .rel.plt 0x548+0x38=0x580
Section 9 .plt 0x580+0x80=0x600
Section 10 .text 0x600+0x1ec=0x7ec
Section 11 .init 0x7ec+0xd=0x7f9
Section 12 .fini 0x7f9+0x8=0x801
000 0x801+0x3=0x804
Section 13 .rodata 0x804+0x10=0x814
Section 14 .got 0x814+0x34=0x848
Section 15 .dynamic 0x848+0xb8=900
Section 16 .data 0x900+0x10=0x910
Section 17 .ctors 0x910+0x8=0x918
Section 18 .dtors 0x918+0x8=0x920
Section 19 .eh_frame 0x920+0x4=0x924
Section 20 .jcr 0x924+0x4=0x928
Section 21 .data.rel.local 0x928+0x4=0x92c
Section 22 .bss 0x92c+0x0=0x958
Section 23 .symtab 0x92c+0x540=0xe6c
Section 24 .strtab 0xe6c+0x20b=1077
Section 25 .comment 0x1077+0x24d=0x12c4
Section 26 .stab.index 0x12c4+0x24=0x12e8
Section 27 .shstrtab 0x12e8+0xdc=0x13c4
Section 28 .stab.indexstr 0x13c4+0x1c0=0x1584
Section header table 0x1584+0x488=0x1a0c ;29x40=1160=0x488 这是根据Elf header信息算得
------------------------------------------------------
图 3-3 test5的ELF文件格式
# ls -al test5
-rwxr-xr-x 1 root other 6668 2004-12-19 06:56 test5
可以看到test5的大小是0x1a0c字节,即6688字节。
可以看到,.bss section用于保存未初始化的全局变量,因此不占据ELF文件空间;
ELF文件装入时,会按照Section header table 22中的.bss的相关属性,为.bss映射相应大小的内存空间,并初始化为0
ELF文件的Program header table描述了如何将ELF装入内存:
Program Header 2 描述了用户代码段的起始地址和大小: 0x8050000+0x814=0x8050814
Program Header 3 描述了用户数据段的起始地址和大小: 0x8060814+0x144=0x8060958
问题:为何前面pmap得到的结果是数据段从0x8060000开始,而ELF文件的Program Header 3却是从0x8060814开始?
如果查一下ELF文件的格式规范的话,就能找到答案:
Program Header[2]:
p_vaddr: 0x8050000 p_flags: [ PF_X PF_R ]
p_paddr: 0 p_type: [ PT_LOAD ]
p_filesz: 0x814 p_memsz: 0x814
p_offset: 0 p_align: 0x10000 ; 指定64K页对齐
Program Header[3]:
p_vaddr: 0x8060814 p_flags: [ PF_X PF_W PF_R ]
p_paddr: 0 p_type: [ PT_LOAD ]
p_filesz: 0x118 p_memsz: 0x144
p_offset: 0x814 p_align: 0x10000 ; 指定64K页对齐
p_align指定了映射代码段和数据段的时候,必须按照64K页对齐的方式,即起始映射地址必须以0000结尾。
代码段的起始地址正好满足该条件,如果数据段不考虑页对齐的话,应该紧跟代码段的下一个字节即0x8050814开始。
但是正因为64K页对齐的缘故,只能从最接近0x8050814的64K页对齐地址0x8060000开始。
而实际上,在对ELF进行内存映射时,是按页为单位进行映射的,test5的大小是0x1a0c,不足1页大小,代码段和数据段都是在第1页。
映射发生时,先映射这第1页到0x8050000的代码段,属性read/exec;再映射这1页到0x8060000的数据段,属性 read/write/exec。
这样,实际上的数据段起始地址就是0x8060000+0x814=0x8060814
问题:为什么要页对齐 page align?
首先,内存映射是以页为最小单位的,这是因为Solaris的内存管理是页式内存管理(目前大多数现代OS都是如此);
其次,代码段和数据段因为有不同的权限要求(代码段要求只读),因此必须进行2次映射;
最后,就是效率的要求;
尽管ELF文件的映射是solaris内核中seg_vn段驱动程序来完成的,但仍可以通过系统调用mmap(2)来学习基本的内存映射常识。
对于其它系统,如Linux情况也类似。
那为何在本例中是要求64K页对齐呢?答案是:这也是Solaris在Sparc上的页对齐要求
概念:ELF文件loading
根据Program header table及section header table描绘出test5代码段及数据段的内部情况就很容易了:
entry name 起始地址+实际大小=下个entry起始地址
=================User Text============================ 0x8050000
ELF header 0x8050000+0x34=0x8050034
Program header 0x8050034+0xa0=0x80500d4
Section 1 .interp 0x80500d4+0x11=0x80500e5
0 0x80500e5+0x3=0x80500e8 ; 3字节0填充
Section 2 .hash 0x80500e8+0x104=0x80501ec
Section 3 .dynsym 0x80501ec+0x200=0x80503ec
Section 4 .dynstr 0x80503ec+0x11a=0x8050506
0 0x8050506+0x2=0x8050508 ; 2字节0填充
Section 5 .SUNW_version 0x8050508+0x20=0x8050528
Section 6 .rel.got 0x8050528+0x18=0x8050540
Section 7 .rel.bss 0x8050540+0x8=0x8050548
Section 8 .rel.plt 0x8050548+0x38=0x8050580
Section 9 .plt 0x8050580+0x80=0x8050600
Section 10 .text 0x8050600+0x1ec=0x80507ec
Section 11 .init 0x80507ec+0xd=0x80507f9
Section 12 .fini 0x80507f9+0x8=0x8050801
0 0x8050801+0x3=0x8050804 ; 3字节0填充
Section 13 .rodata 0x8050804+0x10=0x8050814 ;o,p,q在代码段的. rodata section中
-----------------------------------------------------
Section 14 .got 0x8050814+0x34=0x8050848 ; 这是代码段的第一页也是最后一页,
Section 15 .dynamic 0x8050848+0xb8=8050900 ; 因此数据段内容会追加到代码段最后一页末尾
Section 16 .data 0x8050900+0x10=0x8050910
Section 17 .ctors 0x8050910+0x8=0x8050918
Section 18 .dtors 0x8050918+0x8=0x8050920
Section 19 .eh_frame 0x8050920+0x4=0x8050924
Section 20 .jcr 0x8050924+0x4=0x8050928
Section 21 .data.rel.local 0x8050928+0x4=0x805092c
Section 22 .bss 0x805092c+0x2c=0x8050958
pending data 0x8050958+0x6a8=8051000 ; 页末是0x6a8字节填充
======================================================
no mapping
=================User Data============================ 0x8060000
ELF header 0x8060000+0x34=0x8060034 ; 这是数据段的最后一页也是第一页,
Program header 0x8060034+0xa0=0x80600d4 ; 因此代码段内容会追加到数据段第一页之前
Section 1 .interp 0x80600d4+0x11=0x80600e5
0 0x80600e5+0x3=0x80600e8 ; 3字节0填充
Section 2 .hash 0x80600e8+0x104=0x80601ec
Section 3 .dynsym 0x80601ec+0x200=0x80603ec
Section 4 .dynstr 0x80603ec+0x11a=0x8060506
0 0x8060506+0x2=0x8060508 ; 2字节0填充
Section 5 .SUNW_version 0x8060508+0x20=0x8060528
Section 6 .rel.got 0x8060528+0x18=0x8060540
Section 7 .rel.bss 0x8060540+0x8=0x8060548
Section 8 .rel.plt 0x8060548+0x38=0x8060580
Section 9 .plt 0x8060580+0x80=0x8060600
Section 10 .text 0x8060600+0x1ec=0x80607ec
Section 11 .init 0x80607ec+0xd=0x80607f9
Section 12 .fini 0x80607f9+0x8=0x8060801
0 0x8060801+0x3=0x8060804 ; 3字节0填充
Section 13 .rodata 0x8060804+0x10=0x8060814
-----------------------------------------------------
Section 14 .got 0x8060814+0x34=0x8060848
Section 15 .dynamic 0x8060848+0xb8=8060900
Section 16 .data 0x8060900+0x10=0x8060910 ;i,j,k在数据段的.data section中
Section 17 .ctors 0x8060910+0x8=0x8060918
Section 18 .dtors 0x8060918+0x8=0x8060920
Section 19 .eh_frame 0x8060920+0x4=0x8060924
Section 20 .jcr 0x8060924+0x4=0x8060928
Section 21 .data.rel.local 0x8060928+0x4=0x806092c
Section 22 .bss 0x806092c+0x2c=0x8060958 ; l,m,n在数据段的.bss section中
0 0x8060958+0x6a8=8061000 ; 页末是0x6a8字节0填充
=======================================================
图 3-4 test5的代码段和数据段内部结构
以下各section因为section header table中的sh_flags不包含SHF_ALLOC,因此不会被映射到内存:
Section 23 .symtab
符号表,不映射到内存,strip命令可以去除
Section 24 .strtab
字符串表,主要保存着和 .symtab的名字字符串以及其它字符串,不映射到内存,strip命令可去除
Section 25 .comment
保存着该二进制程序相关的信息,格式内容决定于二进制程序本身,不映射到内存,strip命令保留该section
Section 26 .stab.index
保存着该二进制程序相关的信息,格式内容决定于二进制程序本身,不映射到内存,strip命令可以去除
Section 27 .shstrtab
字符串表,只保存section name,不映射到内存,strip命令保留该section
Section 28 .stab.indexstr
字符串表,不映射到内存,strip命令可以去除
有了图 3-4,就能清楚的找到全局变量和全局常量的在数据段和代码段的精确位置,也就能回答前面提出的问题:
全局常量o,p,q属于代码段的.rodata section,这个section因为属于代码段而具有只读属性,用于保存只读数据
全局变量i,j,k属于数据段的.data section,用于保存有初值的全局变量,这个section同时在ELF文件和内存中占据空间
全局变量l,m,n属于代码段的.bss section,用于保存未初始化的全局变量,这个section占据内存空间而不占据ELF文件空间
由于分属于几个不同的section,地址空间必定不连续了
下面把ELF文件的装入归纳如下:
* 第一个代码段页面包含了 ELF header、Program header table以及其他信息
* 最后的代码段页末尾追加一个数据段开始的拷贝
* 第一个数据段页面前有一个代码段结束的拷贝
* 最后的数据段页面也许会包含与正在运行的进程无关的文件信息
2. ELF文件装载的验证
ELF文件本身的格式可以直接用工具观察二进制文件,下面的命令可以观察到.comment section的相关内容:
bash-2.05# od -A x -c -j 0x1077 -N 0x24d test5
0000000 G N U C c r t 1 . s /0 a s :
0000010 F o r t e D e v e l o p e r
0000020 7 C o m p i l e r C o m m
0000030 o n 7 . 0 I A 3 2 - i t e a
0000040 m 2 0 0 1 / 1 2 / 1 2 /0 G N U
0000050 C c r t i . s /0 a s : F o
0000060 r t e D e v e l o p e r 7
0000070 C o m p i l e r C o m m o n
0000080 7 . 0 I A 3 2 - i t e a m 2
0000090 0 0 1 / 1 2 / 1 2 /0 /0 @ ( # ) S
00000a0 u n O S 5 . 9 G e n e r i c
00000b0 _ 1 1 2 2 3 4 - 0 3 N o v e m
00000c0 b e r 2 0 0 2 /0 G C C : ( G
00000d0 N U ) 3 . 3 . 2 /0 a s : F o
00000e0 r t e D e v e l o p e r 7
00000f0 C o m p i l e r C o m m o n
0000100 7 . 0 I A 3 2 - i t e a m 2
0000110 0 0 1 / 1 2 / 1 2 /0 G C C : (
0000120 G N U ) 3 . 3 . 2 /0 a s : F
0000130 o r t e D e v e l o p e r 7
0000140 C o m p i l e r C o m m o n
0000150 7 . 0 I A 3 2 - i t e a m
0000160 2 0 0 1 / 1 2 / 1 2 /0 G C C :
0000170 ( G N U ) 3 . 3 . 2 /0 a s :
0000180 F o r t e D e v e l o p e r
0000190 7 C o m p i l e r C o m m o
00001a0 n 7 . 0 I A 3 2 - i t e a m
00001b0 2 0 0 1 / 1 2 / 1 2 /0 G N U
00001c0 C c r t n . o /0 a s : F o r
00001d0 t e D e v e l o p e r 7 C
00001e0 o m p i l e r C o m m o n 7
00001f0 . 0 I A 3 2 - i t e a m 2 0
0000200 0 1 / 1 2 / 1 2 /0 l d : S o f
0000210 t w a r e G e n e r a t i o n
0000220 U t i l i t i e s - S o l
0000230 a r i s L i n k E d i t o r
0000240 s : 5 . 9 - 1 . 2 7 6 /0
000024d
显然,.comment section的内容是编译器和链接器的版本信息。
观察ELF载入内存后的情况则需要该ELF程序的进程在系统中挂起,才能读到相关内容。
利用mdb可以查看装入内存中的test5的代码段和数据段值,下面就验证一下图 3-4:
# mdb test5
Loading modules: [ libc.so.1 ]
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: movl $0,%eax
main+0xe: subl %eax,%esp
main+0x10: movl $4,0x8060948
main+0x1a: movl $5,0x806094c
main+0x24: movl $6,0x8060950
main+0x2e: movl 0x8060908,%eax
main+0x33: addl 0x8060904,%eax
main+0x39: addl 0x806090c,%eax
main+0x3f: addl 0x8060948,%eax
main+0x45: addl 0x806094c,%eax
main+0x4b: addl 0x8060950,%eax
main+0x51: addl 0x8050808,%eax
main+0x57: addl 0x805080c,%eax
main+0x5d: addl 0x8050810,%eax
main+0x63: leave
main+0x64: ret
> main+0x2e:b ; 设置断点
> :r ; 运行
mdb: stop at main+0x2e
mdb: target stopped at:
main+0x2e: movl 0x8060908,%eax
> 0x8050000,0x4/naB ; 查看ELF header头4字节
0x8050000:
0x8050000: 7f
0x8050001: 45
0x8050002: 4c
0x8050003: 46
> 0x8050000,0x4/nac ; 查看ELF header头4字节
0x8050000:
0x8050000:
0x8050001: E
0x8050002: L
0x8050003: F
> 0x80500d4,11/c ; 查看.interp section
0x80500d4: /usr/lib/ld.so.1
> 0x8050600::dis ; 查看.text section的第一的过程,也是ELF的入口点
_start: pushl $0
_start+2: pushl $0
_start+4: movl %esp,%ebp
_start+6: pushl %edx
_start+7: movl $0x8050590,%eax
_start+0xc: testl %eax,%eax
_start+0xe: je +0xf <_start+0x1d>
_start+0x10: pushl $0x8050590
_start+0x15: call -0x75 <PLT=libc.so.1`atexit>
_start+0x1a: addl $4,%esp
_start+0x1d: movl $0x8060848,%eax
_start+0x22: testl %eax,%eax
_start+0x24: je +7 <_start+0x2b>
_start+0x26: call -0x86 <PLT=libc.so.1`atexit>
_start+0x2b: pushl $0x80507f9
_start+0x30: call -0x90 <PLT=libc.so.1`atexit>
_start+0x35: movl +8(%ebp),%eax
_start+0x38: leal +0x10(%ebp,%eax,4),%edx
_start+0x3c: movl %edx,0x8060954
_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 +0x19e <_init>
_start+0x53: call -0xa3 <PLT=libc.so.1`_fpstart>
_start+0x58: call +0xfb <main>
_start+0x5d: addl $0xc,%esp
_start+0x60: pushl %eax
_start+0x61: call -0xa1 <PLT:exit>
_start+0x66: pushl $0
_start+0x68: movl $1,%eax
_start+0x6d: lcall $7,$0
_start+0x74: hlt
> 0x8050804,0x4/nap ; 查看.rodata section,包含真正的o,p,q几个全局常量
_lib_version:
_lib_version:
_lib_version: 1
o: 7
p: 8
q: 9
> 0x8050900,0x4/nap ; 查看填充在代码段之后的.data section,这部分实际上是无效的数据
0x8050900:
0x8050900: 0
0x8050904: 1 ; 全局变量i
0x8050908: 2 ; 全局变量j
0x805090c: 3 ; 全局变量k
> 0x8060000,0x4/naB ; 查看填充到数据段之前的ELF header头4字节,这部分实际是无效的
0x8060000:
0x8060000: 7f
0x8060001: 45
0x8060002: 4c
0x8060003: 46
> 0x8060000,0x4/nac ; 查看填充到数据段之前的ELF header头4字节,这部分实际是无效的
0x8060000:
0x8060000:
0x8060001: E
0x8060002: L
0x8060003: F
> 0x8060804,0x4/nap ; 查看填充到数据段之前的.rodata section,这部分实际是无效的
0x8060804:
0x8060804: 1
0x8060808: 7 ; 全局常量o
0x806080c: 8 ; 全局常量p
0x8060810: 9 ; 全局常量q
> 0x8060900,0x4/nap ; 查看.date section,包含真正的i,j,k几个全局变量
0x8060900:
0x8060900: 0
test5`i: 1
test5`j: 2
test5`k: 3
> 0x806092c,0xb/nap ; 查看.bss section,包含真正的l,m,n几个全局变量
test5`completed.1:
test5`completed.1:
test5`completed.1: 0
test5`object.2: 0
test5`object.2+4: 0
test5`object.2+8: 0
test5`object.2+0xc: 0
test5`object.2+0x10: 0
test5`object.2+0x14: 0
test5`l: 4
test5`m: 5
test5`n: 6
test5`environ: 0x8047e00
> 0x8060958,0x6a8/nab ; 查看数据段末尾追加的0x6a8字节数据,全部为0
0x8060958:
0x8060958: 0
0x8060959: 0
0x806095a: 0
.................
.................
0x8060ffe: 0
0x8060fff: 0
> 0x8060fff,2/nab ; 验证页边界数据是否映射
0x8060fff:
0x8060fff: 0
mdb: failed to read data from target: no mapping for address
0x8061000:
> 0x8050fff,2/nab ; 验证页边界数据是否映射
0x8050fff:
0x8050fff: 0151
mdb: failed to read data from target: no mapping for address
0x8051000:
3. 小结
本次实验再次分析和验证了全局变量和全局常量在进程地址空间的位置以及和ELF文件的关系,并涉及到以下几方面的概念:
Process Address Space 进程地址空间
ELF EXECUTABLE AND LINKABLE FORMAT 可执行链接格式
Page align 页对齐
并且,利用Solaris提供的mdb,pmap,elfdump,od工具,直接观察到ELF文件的装载和格式。