您的位置 首页 技术

汇编技能内情(2)

问题:为什么用EAX寄存器保存函数返回值?实际上IA32并没有规定用哪个寄存器来保存返回值。但如果反汇编SolarisLinux的二进制文件,就会

问题:为什么用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二进制程序的功用。

声明:本文内容来自网络转载或用户投稿,文章版权归原作者和原出处所有。文中观点,不代表本站立场。若有侵权请联系本站删除(kf@86ic.com)https://www.86ic.net/xinpin/jishu/260221.html

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱: kf@86ic.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部