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 条评论
登录 后参与评论

相关文章

来自专栏Android干货

Android项目实战(八):列表右侧边栏拼音展示效果

2745
来自专栏xingoo, 一个梦想做发明家的程序员

Spark踩坑——java.lang.AbstractMethodError

百度了一下说是版本不一致导致的。于是重新检查各个jar包,发现spark-sql-kafka的版本是2.2,而spark的版本是2.3,修改spark-sql-...

870
来自专栏曾大稳的博客

ffmpeg添加水印和滤镜效果

更多的特效使用: http://www.ffmpeg.org/ffmpeg-filters.html

1013
来自专栏web开发

移动端打印输出内容以及网络请求-vconsole.js

今天,无意间从别人那里得知一个很好的js插件--vconsole.min.js,可以实现在移动端打印输出内容以及查看网络请求。下面记录使用方式。 1、下载vco...

17110
来自专栏用户2442861的专栏

Tesseract ocr文字识别

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/haluoluo211/article/details...

612
来自专栏Jack的Android之旅

模仿QQ运动item的界面

是不是很像呢,那具体是实现是怎样的呢,即使概括的来说就是 1.计算各个变量的值(记得是会随整个View的大小变化而变化)。 2其次利用好canvas.tra...

643
来自专栏向治洪

android smartbar适配

1.使用魅族的demo里的SmartBarUtils.java 2.在mainifest中的Application  android:theme="@...

1726
来自专栏强仔仔

Java基础系列之正则表达式

Java在处理一些复杂的字符串操作时,往往不是通过String中函数实现的,而是通过Java中正则表达式实现的。 下面通过一个具体的例子简单的介绍一下Java中...

1676
来自专栏专知

2018年SCI期刊最新影响因子排行,最高244,人工智能TPAMI9.455

2018年6月26日,最新的SCI影响因子正式发布,涵盖1万2千篇期刊。CA-Cancer J Clin 依然拔得头筹,其影响因子今年再创新高,达244.585...

842
来自专栏xingoo, 一个梦想做发明家的程序员

20120919-二叉树 数据结构《数据结构与算法分析》

又是一次的毕业季,羡慕嫉妒啊.... 二叉查找树类的框架: 1 template <typename Comparable> 2 class BinaryS...

1759

扫码关注云+社区