为何C语言(的函数调用)需要堆栈,而汇编语言不需要

转自:Uboot中start.S源码中指令级的详尽解析 green-waste
为何 C 语言(的函数调用)需要堆栈,而汇编语言却需要堆栈
之前看了很多关亍uboot的分析,其中就有说要为C语言的运行,准备好堆栈。
而自己在Uboot的start.S汇编代码中,关于系统初始化,也看到有堆栈指针初始化这个动作。但是,从来只看到有人说系统初始化要初始化堆栈,即正确给堆栈指针sp赋值,但是却从来没有看到有人解释,为何要初始化堆栈。所以,接下来的内容,就是经过一定的探究,试图来解释一下,为何要初始化堆栈,即:
为何C语言的函数调用要用到堆栈,而汇编却不需要初始化堆栈。
要明白这个问题,首先要了解堆栈的作用。
关于堆栈的作用,要详细讲解的话,要很长的篇幅,所以此处只是做简略介绍。
总的来说,堆栈的作用就是:保存现场/上下文,传递参数。
4.6.1.保存现场/上下文
现场,意思就相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉后,你就无法恢复现场了。而此处说的现场,就是指CPU运行的时候,用到了一些寄存器,比如r0,r1等等,对于这些寄存器的值,如果你不保存而直接跳转到子函数中去执行,那么很可能就被破坏了,因为其他函数执行也要用到这些寄存器。因此,在函数调用前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场。这样CPU就可以正确的继续执行了。在计算机中,你常可以看到上下文这个词,对应的英文是context。
4.6.1.1. 什么叫做上下文 context
保存现场,也叫保存上下文。
上下文,英文叫做context,就是上面的文章,和下面的文章,即与你此刻,当前CPU运行有关系的内容,即那些你用到寄存器。所以,和上面的现场,是一个意思。
保存寄存器的值,一般用的是push指令,将对应的某些寄存器的值,一个个放到堆栈中,把对应的值压入到堆栈里面,即所谓的压栈。然后待被调用的子函数执行完毕的时候,再调用pop,把堆栈中的一个个的值,赋值给对应的那些你刚开始压栈时用到的寄存器,把对应的值从堆栈中弹出去,即所谓的出栈。
其中保存的寄存器中,也包括lr的值(因为用bl指令运行跳转的话,那么之前的pc的值是存在lr中的),然后在子程序执行完毕的时候,再把堆栈中的lr的值pop出来,赋值给pc,这样就实现了子函数的正确的返回。
4.6.2.传递参数
C语言运行函数调用的时候,常常会传递给被调用的函数一些参数,对于这些C语言级别的参数,被编译器翻译成汇编语言的时候,就要找个地方存放一下,并且让被调用的函数能够访问,否则就没法实现传递参数了。对于找个地方放一下,分两种情况。
一种情况是,本身传递的参数就很少,就可以通过寄存器传送参数。
因为在前面的保存现场的动作中,已经保存好了对应的寄存器的值,那么此时,这些寄存器就是空闲的,可以供我们使用的了,那就可以放参数,而参数少的情冴下,就足够存放参数了,比如参数有2个,那么就用r0和r1存放即可。(关于参数1和参数2,具体哪个放在r0,哪个放在r1,就是和APCS中的“在函数调用之间传递/返回参数”相关了,APCS中会有详细的约定。感兴趣的自己去研究。)
但是如果参数太多,寄存器不够用,那么就得把多余的参数堆栈中了。即,可以用堆栈来传递所有的寄存器放不下的那些多余的参数。
4.6.3.举例分析 C 语言函数调用是如何使用堆栈的
对于上面的解释的堆栈的作用显得有些抽象,此处再用例子来简单说明一下,就容易明白了:
arm-inux-objdump –d u-boot > dump_u-boot.txt
可以得到dump_u-boot.txt文件。该文件就是中,包含了u-boot中的程序的可执行的汇编代码,其中我们可以看到C语言的函数的源代码,到底对应着那些汇编代码。
下面贴出两个函数的汇编代码,
一个是clock_init,
另一个是不clock_init在同一C源文件中的,另外一个凼数CopyCode2Ram:
33d0091c <CopyCode2Ram>:
33d0091c: e92d4070 push {r4, r5, r6, lr}
33d00920: e1a06000 mov r6, r0
33d00924: e1a05001 mov r5, r1
33d00928: e1a04002 mov r4, r2
33d0092c: ebffffef bl 33d008f0 <bBootFrmNORFlash>
... ...
33d00984: ebffff14 bl 33d005dc <nand_read_ll>
... ...
33d009a8: e3a00000 mov r0, #0 ; 0x0
33d009ac: e8bd8070 pop {r4, r5, r6, pc}
33d009b0 <clock_init>:
33d009b0: e3a02313 mov r2, #1275068416 ; 0x4c000000
33d009b4: e3a03005 mov r3, #5 ; 0x5
33d009b8: e5823014 str r3, [r2, #20]
... ...
33d009f8: e1a0f00e mov pc, lr
(1)clock_init 部分的代码
可以看到该凼数第一行:
33d009b0: e3a02313 movr2, #1275068416 ; 0x4c000000
就没有我们所期望的push指令,没有去将一些寄存器的值放到堆栈中。这是因为,我们clock_init返部分的内容,所用到的r2,r3等寄存器,和前面调用clock_init之前所用到的寄存器r0,没有冲突,所以此处可以不用push去保存返类寄存器的值,不过有个寄存器要注意,那就是r14,即lr,其是在前面调用clock_init的时候,用的是bl指令,所以会自动把跳转时候的pc的值赋值给lr,所以也不需要push指令去将PC的值保存到堆栈中。而clock_init的代码的最后一行:
33d009f8: e1a0f00e movp c, lr
就是我们常见的mov pc, lr,把lr的值,即之前保存的凼数调用时候的PC值,赋值给现在的PC,
这样就实现了凼数的正确的返回,即返回到了凼数调用时候下一个指令的位置。
这样CPU就可以继续执行原先凼数内剩下那部分的代码了。
(2)CopyCode2Ram 部分的代码
其第一行:
33d0091c: e92d4070 push {r4, r5, r6, lr}
就是我们所期望的,用push指令,保存了r4,r5,r以及lr。
用push去保存r4,r5,r6,那是因为所谓的保存现场,以后后续函数返回时候再恢复现场,
而用push去保存lr,那是因为此凼数里面,还有其他凼数调用:
33d0092c: ebffffef bl 33d008f0 <bBootFrmNORFlash>
... ...
33d00984: ebffff14 bl 33d005dc <nand_read_ll>
... ...
也用到了bl指令,会改变我们最开始迕入clock_init时候的lr的值,所以我们要用push也暂时保存起来。
而对应地,CopyCode2Ram的最后一行:
33d009ac: e8bd8070 pop {r4, r5, r6, pc}
就是把之前push的值,给pop出来,迓给对应的寄存器,其中最后一个是将开始push的lr的值,pop出来给赋给PC,因为实现了函数返回。
另外,我们注意到,在CopyCode2Ram的倒数第二行是:
33d009a8: e3a00000 movr0, #0 ; 0x0
是把0赋值给r0寄存器,这个就是我们所谓返回值的传递,是通过r0寄存器的。
此处的返回值是0,也对应着C诧言的源码中的“return 0” .
对于使用哪个寄存器来传递迒回值:当然你也可以用其他暂时空闲没有用到的寄存器来传递返回值,但是这些处理方式,本身是根据ARM的APCS的寄存器的使用的约定而设计的,你最好不要随便改变使用方式,最好还是按照其约定的来处理,这样程序更加符合规范。

附:什么是 APCS
APCS,ARM 过程调用标准(ARM Procedure Call Standard),提供了紧凑的编写例程的一种机制,定义的例程可以与其他例程交织在一起。最显著的一点是对这些例程来自哪里没有明确的限制。它们可以编译自 C、 Pascal、也可以是用汇编语言写成的。
APCS 定义了:
>>对寄存器使用的限制。
>>使用栈的惯例。
>>在函数调用之间传递/返回参数。
>>可以被“回溯”的基于栈的结构的格式,用来提供从失败点到程序入口的函数(和给予的参数)的列表。

时间: 2024-08-28 18:54:23

为何C语言(的函数调用)需要堆栈,而汇编语言不需要的相关文章

c语言中函数调用的本质从汇编角度分析

今天下午写篇博客吧,分析分析c语言中函数调用的本质,首先我们知道c语言中函数的本质就是一段代码,但是给这段代码起了一个名字,这个名字就是他的的这段代码的开始地址 这也是函数名的本质,其实也就是汇编中的标号.下面我们会接触到一些东西 比如 eip 就是我们常常说的程序计数器,还有ebp和esp (这里是俩个指针,记得我们以前学8086也就一个sp堆栈指针)分别为EBP是指向栈底的指针,在过程调用中不变,又称为帧指针.ESP指向栈顶,程序执行时移动,ESP减小分配空间,ESP增大释放空间,ESP又称

C++语言学习(十二)——C++语言常见函数调用约定

C++语言学习(十二)--C++语言常见函数调用约定 一.C++语言函数调用约定简介 C /C++开发中,程序编译没有问题,但链接的时候报告函数不存在,或程序编译和链接都没有错误,但只要调用库中的函数就会出现堆栈异常等现象.上述现象出现在C和C++的代码混合使用的情况下或在C++程序中使用第三方库(非C++语言开发)的情况下,原因是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则导致的.函数调用约定决定函数参数入栈的顺序,以及由调用者函数还是被

C语言之函数调用19—总结

C程序的函数调用共涵盖18个例题,包含了递归.递推.字符串操作.矩阵操作.格式输出等内容. 相比之前的基本算法,函数调用已经初见其效率,至此,相信大家对函数调用也有了比较好的认识,就是先表达出功能,当要解决问题时,只要简单调用即可.我们常常会听说C的强大,也是因为她有着调用函数的功能,而且函数可以自己实现. 接下来作者会推出文件操作的基础编程方法,希望大家能够共同进步. 不积跬步无以至千里!共勉! 二〇一五年八月二日  夜 附: C语言之函数调用18-多字符串排序 2015-08-02 01:0

深入理解C语言的函数调用过程 【转】

转自:http://blog.chinaunix.net/uid-25909619-id-4240084.html 原文地址:深入理解C语言的函数调用过程 作者:wjlkoorey258 本文主要从进程栈空间的层面复习一下C语言中函数调用的具体过程,以加深对一些基础知识的理解.    先看一个最简单的程序: 点击(此处)折叠或打开 /*test.c*/ #include <stdio.h> int foo1(int m,int n,int p) { int x = m + n + p; ret

函数调用导致堆栈不对称。原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配。

原文:函数调用导致堆栈不对称.原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配. 在dllimport中加入CallingConvention参数就行了,[DllImport(PCAP_DLL, CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] 要注意C++与NET中数据类型的对应: //c++:char * ---- c#:string //传入参数 //c++:char * ---- c#

在linux代码中打印函数调用的堆栈的方法

之前一直有这样的需求,当时问到,也没搜到方法,现在竟然既问到了,也搜到了,哎,世事真是不能强求啊! 在Linux内核调试中,经常用到的打印函数调用堆栈的方法非常简单,只需在需要查看堆栈的函数中加入: dump_stack();或 __backtrace();即可. dump_stack()在~/kernel/ lib/Dump_stack.c中定义 void dump_stack(void){ printk(KERN_NOTICE  "This architecture does not imp

C语言之函数调用06—彩球排列

//函数调用+递归法 /* ========================================================== 题目:将4个红球,3个白球,3个黄球排成一排,共有多少种排法?球无编号! ========================================================== */ #include<stdio.h> int J(int n) { if(n==0||n==1) return 1; else return n*J(n-1

C语言之函数调用07—求4个数的最大公约数和最小公倍数

//函数调用 /* ========================================================== 题目:求4个数的最大公约数和最小公倍数. ========================================================== */ #include<stdio.h> long gys(long m,long n) { int t,r; if(m<n) {t=m;m=n;n=t;} while((r=m%n)!=0)

C语言之函数调用09—判断一个数是否为素数

//函数调用 /* ========================================================== 题目:判断一个数是否为素数.如3是素数,4不是素数. ========================================================== */ #include<stdio.h> #include<stdlib.h> int sushu(int n) { int i,k; k=n/2;//控制结束位置! for(

c语言函数, 函数调用及函数递归

1. 函数的定义: 返回值类型 函数名(形参列表) {函数体(函数的实现内容)}, 注意: 如果没有参数, 小括号也是必不可少的.  函数与函数之间可以嵌套调用(也就是在一个函数内部可以调用另外一个函数), 但是不能嵌套定义(不能在一个函数内部定义另外一个函数) 形参: 形式上的参数, 在函数定义时, 给定的参数叫做形参, 是一个一个的变量, 存储的数据在调用之前完未知 实参: 函数调用时给定的参数叫做实参, 是一个唯一确定的数据. 实参向形参传递的过程是一个拷贝的过程 函数相当于公司的部门,