C/C++ 学习笔记三(函数)

导语

函数在编程语言中可谓“头等公民”,理解函数的实现原理,函数的一些方法论对于编程非常有好处。 我将从函数的实现原理以及编写函数的一些建议两个的角度来重新认识一下C、C++中的函数。 那具体函数在汇编层面到底是什么,以及函数是如何跳转的。本文尝试从下面从汇编的角度去理解一下c函数。

函数

首先是一段比较简单的C代码,我编译成汇编,然后解读每一个汇编指令到底做了什么操作。

C代码如下

#include <stdio.h>
int foo1(int m,int n,int p)
{
    int x = m + n + p;
    return x;
}

int main(int argc,char** argv)
{
    int x,y,z,result;
    x=11;
    y=22;
    z=33;
    result = foo1(x,y,z);
    printf("result=%d\n",result);
    return 0;
}

在我Mac 64位机器编译后的汇编主要代码为

Test-foo1:
    0x100000f00 <+0>:  pushq  %rbp
    0x100000f01 <+1>:  movq   %rsp, %rbp
    0x100000f04 <+4>:  movl   %edi, -0x4(%rbp)
    0x100000f07 <+7>:  movl   %esi, -0x8(%rbp)
    0x100000f0a <+10>: movl   %edx, -0xc(%rbp)
    0x100000f0d <+13>: movl   -0x4(%rbp), %edx
    0x100000f10 <+16>: addl   -0x8(%rbp), %edx
    0x100000f13 <+19>: addl   -0xc(%rbp), %edx
    0x100000f16 <+22>: movl   %edx, -0x10(%rbp)
    0x100000f19 <+25>: movl   -0x10(%rbp), %eax
    0x100000f1c <+28>: popq   %rbp
    0x100000f1d <+29>: retq   

Test-main:
    0x100000f20 <+0>:  pushq  %rbp
    0x100000f21 <+1>:  movq   %rsp, %rbp
    0x100000f24 <+4>:  subq   $0x30, %rsp
    0x100000f28 <+8>:  movl   $0x0, -0x4(%rbp)
    0x100000f2f <+15>: movl   %edi, -0x8(%rbp)
    0x100000f32 <+18>: movq   %rsi, -0x10(%rbp)
    0x100000f36 <+22>: movl   $0xb, -0x14(%rbp)
    0x100000f3d <+29>: movl   $0x16, -0x18(%rbp)
    0x100000f44 <+36>: movl   $0x21, -0x1c(%rbp)
    0x100000f4b <+43>: movl   -0x14(%rbp), %edi
    0x100000f4e <+46>: movl   -0x18(%rbp), %esi
    0x100000f51 <+49>: movl   -0x1c(%rbp), %edx
    0x100000f54 <+52>: callq  0x100000f00               ; foo1 at main.c:5
    0x100000f59 <+57>: leaq   0x4d(%rip), %rdi          ; "result=%d\n"
    0x100000f60 <+64>: movl   %eax, -0x20(%rbp)
    0x100000f63 <+67>: movl   -0x20(%rbp), %esi
    0x100000f66 <+70>: movb   $0x0, %al
    0x100000f68 <+72>: callq  0x100000f7a               ; symbol stub for: printf
    0x100000f6d <+77>: xorl   %edx, %edx
    0x100000f6f <+79>: movl   %eax, -0x24(%rbp)
    0x100000f72 <+82>: movl   %edx, %eax
    0x100000f74 <+84>: addq   $0x30, %rsp
    0x100000f78 <+88>: popq   %rbp
    0x100000f79 <+89>: retq

剖析函数的调用过程

这里先复习下汇编知识,下面会经常提及

pushq xx    ##将xx入栈
popq  xx    ##出栈,将结果存至xx
movq   a, b ##将a数据复制到b
callq 0x1234 ##跳转到0x1234地址
addl   a,b   ##将a与b相加,并且将结果放到b中

rbp 栈基针寄存器,指向栈底
rsp 栈指针寄存器,指向栈顶
rip 指令指针寄存器,指向当前执行的地址

1.进入main函数逻辑

从地址为0x100000f20 开始main函数逻辑

  0x100000f20 <+0>:  pushq  %rbp
  0x100000f21 <+1>:  movq   %rsp, %rbp

第一步为将前一栈帧的栈基地址rbp入栈,第二部为将栈顶地址rsp拷贝至rbp中。

完成这一步后,就完成了保留上一帧的基地址,初始化本帧的栈顶地址。

这里以我debug的地址为例,此时rbp 的值为 0x730,rsp值也为0x730

2. 分配栈空间

接下来rsp减去0x30 (48)个字节,即栈顶向低字节移动48个字节,变成0x700,相当于当前栈帧为当前的函数分配了48个字节的空间,用于存放函数局部参数。当前函数执行完后,rsp回到上一函数的栈顶,便达到了回收局部变量的功能。

subq   $0x30, %rsp

此时的栈信息如下

3.为局部变量赋值

接着下面6个命令为局部变量赋值。前面3个命名暂时忽略,由第4个命令开始,分别是将立即数0xb (11) 写入到 rbp往低地址偏移0x14字节的内存块中。将立即数0x16 (22) 写入到 rbp往低地址偏移0x18字节的内存块中。将立即数0x21 (33) 写入到 rbp往低地址偏移0x1c字节的内存块中。这也就是C函数中局部变量赋值操作x=11;y==22;z=33;

 movl   $0x0, -0x4(%rbp)
 movl   %edi, -0x8(%rbp)
 movq   %rsi, -0x10(%rbp)
 movl   $0xb, -0x14(%rbp)  ## 11 --> x
 movl   $0x16, -0x18(%rbp) ## 22 --> y
 movl   $0x21, -0x1c(%rbp) ## 33 --> z

此时的栈

4. 传递参数

接下来的三个指令非常简单,便是将上一步骤中的三个全局变量x,y,z移动至寄存器 edi,esi,edx中。 看到这里便有一个疑问,其实做一个传递立即数的操作,为什么需要先传递到内存,再传递到寄存器用于函数调用呢?这是因为movl 的操作数不能是立即数,所以必须要先将立即数传递到内存区域,再从内存区域传递至寄存器。

 movl   -0x14(%rbp), %edi
 movl   -0x18(%rbp), %esi
 movl   -0x1c(%rbp), %edx

5.函数跳转

callq 的操作为下一条指令的地址(0x100000f59)入栈,然后跳转至 0x100000f00。跳转后rip为0x100000f00

 0x100000f54 <+52>: callq  0x100000f00               ; foo1 at main.c:5
 0x100000f59 <+57>: leaq   0x4d(%rip), %rdi          ; "result=%d\n"

6.子函数调用

将前一个堆栈的栈基地址寄存器rbp入栈。rsp向低地址偏移8个字节。 并且将rsp赋值给rbp。

  0x100000f00 <+0>:  pushq  %rbp
  0x100000f01 <+1>:  movq   %rsp, %rbp

由此开始便是子函数的栈帧。此时rbp和rsp是相同。

7.获取形参与计算

到这里便是从刚才的edi 中取出x , 从esi中取出y ,从edx取出y,分别放置到rbp偏移0x4,0x8,0xc的内存中。 并将三者相加将结果放置eax中。

   movl   %edi, -0x4(%rbp)
   movl   %esi, -0x8(%rbp)
   movl   %edx, -0xc(%rbp)
   movl   -0x4(%rbp), %edx
   addl   -0x8(%rbp), %edx
   addl   -0xc(%rbp), %edx 
   movl   %edx, -0x10(%rbp) 
   movl   -0x10(%rbp), %eax

8 子程序跳出函数,跳转回到main函数

执行前的堆栈

最后便是回到main函数的步骤。第一个指令将栈顶的数据出栈,并且将其赋值给rbp。从上步骤中可以看到,栈顶数据其实便是0x730,即main函数的栈底。

下一步执行ret ,继续将栈顶出栈,并且将值付给rip。按照rip此时指示的指令地址继续执行程序

popq   %rbp
retq

执行指令后

到此,子程序便退出,回到了main函数的prinf函数中,继续执行。

建议:

1.避免在非调度函数中使用控制函数

在日常编程中,有时会非常自然的根据一些配置参数,来实现具体的功能,也很自然的在函数中根据参数的值的不同,函数体内将不同情况的分支情况都写在一起。

调度函数指根据输入的消息类型或者控制命令来启动相应功能实体。 简单而言,便是根据配置,调用其他功能函数,其本身只关心“what to do”。 而非调度函数(功能函数)实现具体的某个功能,其本身关心“hot to do”。 以此为规则可以清晰的将函数进行冗余的函数进行分层。

例如以下:

这里使用了一个calu_flg参数进行加减法的区分。这种方式其实是非常的不合理,违背了函数实现单一功能的原则。

int calculate(int a ,int b , int calu_flg)
{
    if(calu_flag = 1)
    {
        return a+b;
    }else if (calu_flag == 2){
        return a-b;
    }else{
        return -1
    }
}

如下是将调度函数与非调度函数(功能函数)进行区分

int add(a,b)
{
    return a+b;
}
int minus(a,b)
{
    return a-b;
}
int calculate(int a ,int b , int calu_flg)
{
    if(calu_flag = 1)
    {
        return add(a,b);
    }else if (calu_flag == 2){
        return minus(a,b)
    }else{
        return -1
    }
}

2.使用const防止指针类型变量被修改

如果参数仅作为输入,则使用const修饰符声明,防止函数修改该值

char * strCopy(char * strDest,const char * strSrc)
{
    ...
    return ....;
}

3. 函数如无返回值时,显式声明void类型的返回

听起来其实非常简单,日常编程中也不容易遗漏。这里提及一下C的早期版本中,支持不填返回值。且默认的返回值为int。

如下的函数声明在某些版本下是可以正常编译

func()
{
    return 1;
}
int main()
{
    printf("%d",func());
}

4.确保函数入口与出口的安全性

入口即参数的合法性。以”永不信任的原则“,对传入的参数合法性进行校验。

void func(char * p1,char *p2)
{
    assert((NULL!=p1)&&(NULL!=p2));
    //...
}

出口即return的返回值必须涵盖所有的正常与异常情况。

在使用其他函数时,也需要对调用函数的返回值进行判断,同时也需对错误的返回值进行相应的错误处理。

5.局部变量不易过多

人类大脑同时记住的7个不同的东西,超过这个就会犯糊涂。因此局部变量的数目应该少,应该不差过5-10个

小结

1 .函数的栈的实现其实是修改来rbp与rsp的实现的。通过控制这个两个寄存器在函数调用前保存前一函数的rbp压栈,函数体执行完成后出栈回退至上一个函数的rbp,来达到函数调用的效果。

2 . 函数的局部变量是通过移动rsp的值而分配的。函数退出时,rsp回到前函数的栈顶,这便达到了函数推出时,局部变量也随之释放的效果。

3 .对于函数的功能架构而言,应该遵从功能与调度的分离,尽量做到各尽其事。

4 .对于函数体内的个个switch与if等的分支逻辑,应该先主后次,先正常逻辑再异常逻辑。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏顶级程序员

Linux吃掉我的内存

在Windows下资源管理器查看内存使用的情况,如果使用率达到80%以上,再运行大程序就能感觉到系统不流畅了,因为在内存紧缺的情况下使用交换分区,频繁地从磁盘...

3155
来自专栏Java开发

ajaxFileUpload.js 的一些Bug

这里以前提到过 http://blog.csdn.net/qq_30930805/article/details/62427726

681
来自专栏杨熹的专栏

Python 爬虫 1 快速入门

Python 爬虫 快速入门 参考资料:极客学院: Python定向爬虫 代码:1.crawler-basic.ipynb 本文内容: 正则表达式 用正则表达式...

2784
来自专栏Spark学习技巧

Hbase源码系列之源码前奏hbase:meta表相关详细介绍

一,基本功能介绍 -root-表在HBase 0.9.6以后的版本被移除了。 Hbase 0.9.6以前,三个重要信息: 1,-root-表的位置存储在Zook...

37210
来自专栏King_3的技术专栏

leetcode-344-Reverse String

2175
来自专栏青枫的专栏

java注解用法详解——@SuppressWarnings

  在java编译过程中会出现很多警告,有很多是安全的,但是每次编译有很多警告影响我们对error的过滤和修改,我们可以在代码中加上 @SuppressWarn...

1493
来自专栏web前端

JavaScript基础学习--08 JS作用域

Demos:   https://github.com/jiangheyan/JavaScriptBase 一、浏览器      1、“JS解析器”(至少分为两...

1915
来自专栏蓝天

c99 增加的restrict关键字

c99中新增加了一个类型定义,就是restrict。 restrict的定义是It can be applied only to pointers, and i...

582
来自专栏python学习指南

Python爬虫(十二)_XPath与lxml类库

Python学习指南 有同学说,我正则用的不好,处理HTML文档很累,有没有其他的方法? 有!那就是XPath,我们可以用先将HTML文档转换成XML文...

26110
来自专栏编程

浅谈如何定义和调用Python的函数

函数是python编程核心内容之一,笔者在本文中主要介绍下函数的概念和基础函数相关知识点。函数是什么?有什么作用、定义函数的方法及如何调用函数。 函数是可以实现...

1695

扫码关注云+社区