前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C/C++ 学习笔记三(函数)

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

原创
作者头像
Celebi
修改2017-08-21 10:11:47
1.1K6
修改2017-08-21 10:11:47
举报
文章被收录于专栏:Celebi的专栏Celebi的专栏

导语

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

函数

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

C代码如下

代码语言:javascript
复制
#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位机器编译后的汇编主要代码为

代码语言:javascript
复制
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

剖析函数的调用过程

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

代码语言:javascript
复制
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函数逻辑

代码语言:javascript
复制
  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回到上一函数的栈顶,便达到了回收局部变量的功能。

代码语言:javascript
复制
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;

代码语言:javascript
复制
 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 的操作数不能是立即数,所以必须要先将立即数传递到内存区域,再从内存区域传递至寄存器。

代码语言:javascript
复制
 movl   -0x14(%rbp), %edi
 movl   -0x18(%rbp), %esi
 movl   -0x1c(%rbp), %edx

5.函数跳转

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

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

6.子函数调用

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

代码语言:javascript
复制
  0x100000f00 <+0>:  pushq  %rbp
  0x100000f01 <+1>:  movq   %rsp, %rbp

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

7.获取形参与计算

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

代码语言:javascript
复制
   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此时指示的指令地址继续执行程序

代码语言:javascript
复制
popq   %rbp
retq

执行指令后

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

建议:

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

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

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

例如以下:

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

代码语言:javascript
复制
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
    }
}

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

代码语言:javascript
复制
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修饰符声明,防止函数修改该值

代码语言:javascript
复制
char * strCopy(char * strDest,const char * strSrc)
{
    ...
    return ....;
}

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

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

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

代码语言:javascript
复制
func()
{
    return 1;
}
int main()
{
    printf("%d",func());
}

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

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

代码语言:javascript
复制
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等的分支逻辑,应该先主后次,先正常逻辑再异常逻辑。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导语
  • 函数
  • 剖析函数的调用过程
  • 建议:
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档