环境ubuntu 18.04 LTS
课程地址:https://www.icourse163.org/course/NJU-1001625001
一、递归过程调用
示例程序,这是一个简单的递归加法。
int nn_sum(int n){
int result;
if(n<=0)
{result =0;}
else {
result = n+nn_sum(n-1);
}
return result;
}
int main(){
int q=1;
q=nn_sum(2);
return 0;
}
编译,注意采用-O0(欧零)级别的优化,才能更好的反映程序本身的逻辑。
gcc -O0 a.cpp -o a
反汇编
objdump -d -m i386 a > a.txt
打开
vim a.txt
看核心部分
00000000000005fa <_Z6nn_sumi>:
5fa: 55 push %ebp
5fb: 48 dec %eax
5fc: 89 e5 mov %esp,%ebp
5fe: 48 dec %eax
5ff: 83 ec 20 sub $0x20,%esp
602: 89 7d ec mov %edi,-0x14(%ebp)
605: 83 7d ec 00 cmpl $0x0,-0x14(%ebp) //n与0比
609: 7f 09 jg 614 <_Z6nn_sumi+0x1a> //若n大于0,跳转614
60b: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp) // 若n<=0,令result=0
612: eb 17 jmp 62b <_Z6nn_sumi+0x31> //返回result
614: 8b 45 ec mov -0x14(%ebp),%eax
617: 83 e8 01 sub $0x1,%eax
61a: 89 c7 mov %eax,%edi
61c: e8 d9 ff ff ff call 5fa <_Z6nn_sumi>
621: 89 c2 mov %eax,%edx
623: 8b 45 ec mov -0x14(%ebp),%eax
626: 01 d0 add %edx,%eax // %eax+= sum(n-1)
628: 89 45 fc mov %eax,-0x4(%ebp)
62b: 8b 45 fc mov -0x4(%ebp),%eax
62e: c9 leave
62f: c3 ret
0000000000000630 <main>:
630: 55 push %ebp
631: 48 dec %eax
632: 89 e5 mov %esp,%ebp
634: 48 dec %eax
635: 83 ec 10 sub $0x10,%esp
638: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp) //初始化q=1
63f: bf 02 00 00 00 mov $0x2,%edi //先将参数n=2传入%edi以供函数使用
644: e8 b1 ff ff ff call 5fa <_Z6nn_sumi>
649: 89 45 fc mov %eax,-0x4(%ebp) //将返回值赋值给q
64c: b8 00 00 00 00 mov $0x0,%eax //准备return 0
651: c9 leave
652: c3 ret
653: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%eax,%eax,1)
65a: 00 00 00
65d: 0f 1f 00 nopl (%eax)
1)需要注意的是,这里参数n用%edi进行了传送。而袁书上说的是形参用压栈的方式传送。
这是因为我们采用64位gcc编译,64位环境下, 16个通用寄存器,资源非常之丰富,所以可以使用大量空余的GPR传送参数。
如果指定gcc采用32位模式编译,输入参数-m32,
gcc -O0 -m32 a.cpp -o a
#如果提示bits/c++config.h: 没有那个文件或目录
#sudo apt-get install gcc-multilib
就能得到
000004ed <_Z6nn_sumi>:
4ed: 55 push %ebp
4ee: 89 e5 mov %esp,%ebp
4f0: 83 ec 18 sub $0x18,%esp
4f3: e8 74 00 00 00 call 56c <__x86.get_pc_thunk.ax>
4f8: 05 e4 1a 00 00 add $0x1ae4,%eax
4fd: 83 7d 08 00 cmpl $0x0,0x8(%ebp)
501: 7f 09 jg 50c <_Z6nn_sumi+0x1f>
503: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
50a: eb 1c jmp 528 <_Z6nn_sumi+0x3b>
50c: 8b 45 08 mov 0x8(%ebp),%eax
50f: 83 e8 01 sub $0x1,%eax
512: 83 ec 0c sub $0xc,%esp
515: 50 push %eax
516: e8 d2 ff ff ff call 4ed <_Z6nn_sumi>
51b: 83 c4 10 add $0x10,%esp
51e: 89 c2 mov %eax,%edx
520: 8b 45 08 mov 0x8(%ebp),%eax
523: 01 d0 add %edx,%eax
525: 89 45 f4 mov %eax,-0xc(%ebp)
528: 8b 45 f4 mov -0xc(%ebp),%eax
52b: c9 leave
52c: c3 ret
0000052d <main>:
52d: 8d 4c 24 04 lea 0x4(%esp),%ecx
531: 83 e4 f0 and $0xfffffff0,%esp
534: ff 71 fc pushl -0x4(%ecx)
537: 55 push %ebp
538: 89 e5 mov %esp,%ebp
53a: 51 push %ecx
53b: 83 ec 14 sub $0x14,%esp
53e: e8 29 00 00 00 call 56c <__x86.get_pc_thunk.ax>
543: 05 99 1a 00 00 add $0x1a99,%eax
548: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%ebp)
54f: 83 ec 0c sub $0xc,%esp
552: 6a 02 push $0x2
554: e8 94 ff ff ff call 4ed <_Z6nn_sumi>
559: 83 c4 10 add $0x10,%esp
55c: 89 45 f4 mov %eax,-0xc(%ebp)
55f: b8 00 00 00 00 mov $0x0,%eax
564: 8b 4d fc mov -0x4(%ebp),%ecx
567: c9 leave
568: 8d 61 fc lea -0x4(%ecx),%esp
56b: c3 ret
在32模式的代码里,可以看到在call nn_sumi之前,形参0x2是通过push压栈的方式传送的。
而在nn_sumi里,通过0x8(%ebp)来读取这个形参0x2。
这个细节,仅仅是32位与64位的区别罢了。
2)这里有个让人惊讶的地方。
正常来说我以为,cmp o1,o2,是用o1-o2然后判断大小。毕竟AT&T系的语法,都是先src再dst的。
实际代码中,cmp $0x0, -0x14(%ebp) 这句,却是用后者减前者然后判大小。
包括袁老师的ppt上,cmpl $0,%ebx 之后,也是按照后者减前者的逻辑判大小跳转的。
而袁老师的书中说了本课程采用AT&T格式。
如果你在某知名搜索引擎上找博文,也会看到很多人说是o1-o2。
甚至某知名搜索引擎的百科里,也是这样写的。
而oracle给的IA32汇编手册里,却对3种可能的cmp语法,即1)cmp 寄存器寻址,存储器寻址 2)cmp 存储器寻址,寄存器寻址 3)cmp 立即数,存储器寻址 ,都标明了是后者减前者。【看起来似乎是,立即数无法与寄存器直接做比较。】
看了一些不同资料后,我给出一个暂时性的结论。
在AT&T格式的IA-32汇编指令中,cmp o1.o2的运算方式是,o2-o1,然后根据标志位判大小。
更本质地说,是dst-src,判大小。
二、浮点数比较
源代码:
#include <cstdlib>
int main(){
float a=0;
double q=1.0;
double p=2.0;
if(p>q){
a=3.0;
}
return 0;
}
编译、反汇编
gcc -O0 -m32 b.cpp -o b
objdump -d b > b.txt
得到
000004ed <main>:
4ed: 8d 4c 24 04 lea 0x4(%esp),%ecx
4f1: 83 e4 f8 and $0xfffffff8,%esp
4f4: ff 71 fc pushl -0x4(%ecx)
4f7: 55 push %ebp
4f8: 89 e5 mov %esp,%ebp
4fa: 51 push %ecx
4fb: 83 ec 24 sub $0x24,%esp
4fe: e8 3b 00 00 00 call 53e <__x86.get_pc_thunk.ax>
503: 05 d9 1a 00 00 add $0x1ad9,%eax
508: d9 ee fldz
50a: d9 5d e4 fstps -0x1c(%ebp)
50d: d9 e8 fld1
50f: dd 5d e8 fstpl -0x18(%ebp)
512: dd 80 f4 e5 ff ff fldl -0x1a0c(%eax)
518: dd 5d f0 fstpl -0x10(%ebp)
51b: dd 45 e8 fldl -0x18(%ebp)
51e: dd 45 f0 fldl -0x10(%ebp)
521: df e9 fucomip %st(1),%st
523: dd d8 fstp %st(0)
525: 76 09 jbe 530 <main+0x43>
527: d9 80 fc e5 ff ff flds -0x1a04(%eax)
52d: d9 5d e4 fstps -0x1c(%ebp)
530: b8 00 00 00 00 mov $0x0,%eax
535: 83 c4 24 add $0x24,%esp
538: 59 pop %ecx
539: 5d pop %ebp
53a: 8d 61 fc lea -0x4(%ecx),%esp
53d: c3 ret
可以看到,关键代码都是用专门的浮点数指令完成的。
尤其是比较这步,采用fucomip %st(1),%st,接fstp %st(0),然后才jbe跳出比较。
三、强弱符号链接例题
a.c文件
#include<stdio.h>
int x=100;
short y=1,z=2;
int main(){
p1();
printf("x=%d,z=%d\n", x, z);
}
b.c文件
#include<stdio.h>
double x;
void p1(){
x=-1.0;
}
编译链接
gcc -g -m32 -c a.c -o a.o
gcc -g -m32 -c b.c -o b.o
gcc -g -m32 -static a.o b.o -o mytest
查看反汇编代码
objdump -d -i 386 mytest > m.txt
080488a5 <main>:
80488a5: 8d 4c 24 04 lea 0x4(%esp),%ecx
80488a9: 83 e4 f0 and $0xfffffff0,%esp
80488ac: ff 71 fc pushl -0x4(%ecx)
80488af: 55 push %ebp
80488b0: 89 e5 mov %esp,%ebp
80488b2: 53 push %ebx
80488b3: 51 push %ecx
80488b4: e8 c7 fe ff ff call 8048780 <__x86.get_pc_thunk.bx>
80488b9: 81 c3 47 07 09 00 add $0x90747,%ebx
80488bf: e8 33 00 00 00 call 80488f7 <p1>
80488c4: 0f b7 83 6e 00 00 00 movzwl 0x6e(%ebx),%eax
80488cb: 0f bf d0 movswl %ax,%edx
80488ce: 8b 83 68 00 00 00 mov 0x68(%ebx),%eax
80488d4: 83 ec 04 sub $0x4,%esp
80488d7: 52 push %edx
80488d8: 50 push %eax
80488d9: 8d 83 c8 2d fd ff lea -0x2d238(%ebx),%eax
80488df: 50 push %eax
80488e0: e8 0b 70 00 00 call 804f8f0 <_IO_printf>
80488e5: 83 c4 10 add $0x10,%esp
80488e8: b8 00 00 00 00 mov $0x0,%eax
80488ed: 8d 65 f8 lea -0x8(%ebp),%esp
80488f0: 59 pop %ecx
80488f1: 5b pop %ebx
80488f2: 5d pop %ebp
80488f3: 8d 61 fc lea -0x4(%ecx),%esp
80488f6: c3 ret
080488f7 <p1>:
80488f7: 55 push %ebp
80488f8: 89 e5 mov %esp,%ebp
80488fa: e8 14 00 00 00 call 8048913 <__x86.get_pc_thunk.ax>
80488ff: 05 01 07 09 00 add $0x90701,%eax
8048904: c7 c2 68 90 0d 08 mov $0x80d9068,%edx
804890a: d9 e8 fld1
804890c: d9 e0 fchs
804890e: dd 1a fstpl (%edx)
8048910: 90 nop
8048911: 5d pop %ebp
8048912: c3 ret
main中,先call了80488f7处的p1函数。
call之前,查看当前变量值。
不知道为什么x的值奇奇怪怪的,y和z倒是正常。
打印二进制看到x是个0。
然后进入p1,调用浮点数指令。
再次查看变量值。
x设置为-1,y=0,z=-16400。
显示二进制,结果又有点不同。
我怀疑是gdb里的p这个命令有点毛病,不知道为什么,p x就会出错。
稍后我们换成x命令查看内存。
在此之前,先来看底层原理。
p1这个函数,将-1.0赋值给a文件中定义的int x。
我们将-1.0写成64位double形式,按照IEEE754定义。
101111111111 (前12位)
000...000000(后52位全0)
使用命令objdump -t mytest查看符号表,从杂乱无章的数据里抽离出三个关键信息。
可知x存储在相对数据段偏移地址0x2008的位置,长度0x4字节,而y在0x200c,z在0x200e,长度都是0x2字节。
又因为IA-32采用小端法存储,低位优先存储在低地址部分,于是得到下图。
因此打印相应地址中的内容如下。
可以看到确实,x==y==0,而z=1011,1111,1111,0000(binary)=-16400(decimal)
可见使用x &变量名指令能获得正确结果。
继续执行,完成printf,看到打印出来也的确是x=0,z=-16400。
这题的本质是利用了一个64位的-1.0浮点数,穿越了规定好的int存储区域,强行修改了x周边的内存值。
留有一个疑问是,为什么在p1中-1.0被解读为64位的浮点数?为什么不是32位?
于是我修改了b.c中定义的double x,改成float x,重新编译调试。
32位浮点数-1.0的二进制表示
101111111(前9位)
000...0000(后23位)
这次打印的结果如下。
因为32位的浮点数-1.0正好能被32位的int类型装下,所以x的内存范围内就是自己。
y实际上是一个16位的short型,但是gdb默认显示从y首地址开始的32位内存内容,
所以只有右边的低16位是有效的属于y的0x01。
左边高16位其实是z的内容0x02。
可以看到,在这个例子中,修改了b.c中的x为32位之后,-1.0就被解读为32位的浮点数。
为什么会这样呢?
不是说好强弱符号在一起取强符号吗?
double这个弱符号应该被排除在外了才对呀。