C++ 汇编代码分析——递归函数调用、浮点数比较、选择语句

11 篇文章 2 订阅

环境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这个弱符号应该被排除在外了才对呀。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值