[CC++] 函数调用的栈分配
c语言函数参数压栈顺序

c语言函数参数压栈顺序
在C语言中,函数参数的压栈顺序通常是从右向左。
也就是说,最右边的参数首先被压入栈,然后是次右边的参数,以此类推,最左边的参数最后被压入栈。
考虑下面这个简单的函数调用的例子:
```c
int add(int a,int b,int c){
return a+b+c;
}
int main(){
int result=add(1,2,3);
return0;
}
```
在这个例子中,`add`函数有三个参数:`a`、`b`和`c`。
当调用`add(1,2,3)`时,参数`3`首先被压入栈,然后是参数`2`,最后是参数`1`。
在函数内部,按照相反的顺序,即从栈中弹出参数值,`c`首先获取值`3`,然后是`b`获取值`2`,最后是`a`获取值`1`。
这种从右向左的参数传递方式是C语言的一种传统做法,但请注意,在某些特殊的系统或编译器中,这种顺序可能有所不同。
在汇编程序中调用C函数

3.4.2 在汇编程序中调用C函数从汇编程序中调用C语言函数的方法实际上在上面已经给出。
在上面C语言例子对应的汇编程序代码中,我们可以看出汇编程序语句是如何调用swap()函数的。
现在我们对调用方法作一总结。
在汇编程序调用一个C函数时,程序需要首先按照逆向顺序把函数参数压入栈中,即函数最后(最右边的)一个参数先入栈,而最左边的第1个参数在最后调用指令之前入栈,如图3-6所示。
然后执行CALL 指令去执行被调用的函数。
在调用函数返回后,程序需要再把先前压入栈中的函数参数清除掉。
调用函数时压入堆栈的参数在执行CALL指令时,CPU会把CALL指令的下一条指令的地址压入栈中(见图3-6中的EIP)。
如果调用还涉及代码特权级变化,那么CPU会进行堆栈切换,并且把当前堆栈指针、段描述符和调用参数压入新堆栈中。
由于Linux内核中只使用中断门和陷阱门方式处理特权级变化时的调用情况,并没有使用CALL指令来处理特权级变化的情况,因此这里对特权级变化时的CALL指令使用方式不再进行说明。
汇编中调用C函数比较"自由",只要是在栈中适当位置的内容就都可以作为参数供C函数使用。
这里仍然以图3-6中具有3个参数的函数调用为例,如果我们没有专门为调用函数func()压入参数就直接调用它的话,那么func()函数仍然会把存放EIP位置以上的栈中其他内容作为自己的参数使用。
如果我们为调用func()而仅仅明确地压入了第1、第2个参数,那么func()函数的第3个参数p3就会直接使用p2前的栈中内容。
在Linux 0.1x内核代码中就有几处使用了这种方式。
例如在kernel/sys_call.s汇编程序中第231行上调用copy_process()函数(kernel/fork.c中第68行)的情况。
在汇编程序函数_sys_fork中虽然只把5个参数压入了栈中,但是copy_process()却带有多达17个参数(见下面的程序)。
c语言函数参数传递方式

c语言函数参数传递方式C语言是一种广泛使用的编程语言,函数参数传递方式是C语言中非常重要的概念之一。
函数参数传递方式可以分为按值传递、按址传递和按引用传递三种方式。
本文将针对这三种方式进行详细讲解。
一、按值传递按值传递是指在函数调用时,将实际参数的值复制给形式参数,函数内部对形参的修改不会影响到实际参数的值。
这种方式适用于参数较少、参数值不需要在函数内部被修改的情况。
在按值传递的方式下,函数在栈内存中为形参分配空间,并将实参的值复制到形参中。
函数执行结束后,栈内存中的形参被销毁,不会影响到实参的值。
二、按址传递按址传递是指在函数调用时,将实际参数的地址传递给形式参数,函数内部通过指针对实参进行操作,可以修改实参的值。
这种方式适用于需要在函数内部修改实参值的情况。
在按址传递的方式下,函数在栈内存中为形参分配空间,并将实参的地址传递给形参。
函数内部通过指针对实参进行操作,修改实参的值。
由于传递的是地址,所以函数内部对形参的修改会影响到实参。
三、按引用传递按引用传递是C++中的特性,其本质是通过指针来实现的。
在C语言中,可以通过传递指针的方式来模拟按引用传递。
按引用传递的特点是可以修改实参的值,并且不需要像按址传递那样使用指针操作。
在按引用传递的方式下,函数在栈内存中为形参分配空间,并将实参的地址传递给形参。
函数内部通过引用的方式操作形参,可以直接修改实参的值。
由于传递的是地址,所以函数内部对形参的修改会影响到实参。
需要注意的是,按引用传递需要使用指针来实现。
在函数调用时,需要将实参的地址传递给形参,即传递一个指向实参的指针。
函数内部通过解引用指针来操作实参,可以达到修改实参的目的。
总结:C语言中的函数参数传递方式包括按值传递、按址传递和按引用传递三种方式。
按值传递适用于参数较少、参数值不需要在函数内部被修改的情况;按址传递适用于需要在函数内部修改实参值的情况;按引用传递需要使用指针来实现,通过传递实参的地址来实现对实参的修改。
调用函数的压堆栈方式

调用函数的压堆栈方式
在计算机编程中,当一个函数被调用时,会发生压栈操作。
这
是因为计算机需要保存当前函数的执行状态,以便在函数执行完毕
后能够回到调用该函数的地方继续执行。
下面我将从多个角度来解
释函数的压栈方式。
1. 参数传递,在调用函数时,参数会被压入栈中。
这样函数就
可以在栈中找到这些参数并使用它们。
2. 返回地址,在调用函数时,调用方的返回地址会被压入栈中。
这样函数执行完毕后可以通过返回地址回到调用方。
3. 保存旧的栈帧指针,在函数调用时,当前函数的栈帧指针会
被压入栈中,以便在函数执行完毕后能够回到调用方的栈帧。
4. 保存局部变量,在函数调用时,当前函数的局部变量会被压
入栈中,以便在函数执行期间可以使用这些局部变量。
5. 保存寄存器状态,在函数调用时,一些寄存器的状态会被保
存到栈中,以便函数执行期间可以使用这些寄存器。
总的来说,函数的压栈方式是为了保存当前函数的执行状态,以便在函数执行完毕后能够回到调用方继续执行。
这种方式是计算机实现函数调用和返回的基础,也是程序执行的重要机制之一。
希望这些解释能够帮助你理解函数的压栈方式。
c语言的内存结构

c语言的内存结构C语言是一种高级编程语言,但实际上在计算机中运行时,C语言程序会被编译成可执行文件,然后在计算机内存中运行。
因此,了解C 语言的内存结构对于理解C程序的运行及性能优化至关重要。
C语言的内存结构主要可以分为以下几个部分:栈(Stack)、堆(Heap)、全局内存(Global Memory)和代码区(Code Segment)。
首先是栈(Stack),栈是一种自动分配和释放内存的数据结构。
它用于存储局部变量、函数参数和函数调用信息等。
栈的特点是后进先出(LIFO),也就是最后进入的数据最先被释放。
栈的大小在程序运行时是固定的,一般由编译器设置。
栈的操作速度较快,但内存空间有限。
其次是堆(Heap),堆是一种动态分配和释放内存的数据结构。
它用于存储动态分配的变量、数据结构和对象等。
堆的大小一般由操作系统管理,并且可以在运行时进行动态扩展。
堆的操作相对较慢,因为需要手动分配和释放内存,并且容易产生内存碎片。
全局内存(Global Memory)是用于存储全局变量和静态变量的区域。
全局变量在程序的生命周期内都存在,并且可以在多个函数之间共享。
静态变量作用于其所在的函数内,但是生命周期与全局变量相同。
全局内存由编译器进行分配和管理。
代码区(Code Segment)存储了程序的指令集合,它是只读的。
在程序运行时,代码区的指令会被一条一条地执行。
代码区的大小由编译器决定,并且在程序执行过程中不能修改。
此外,C语言还具有特殊的内存区域,如常量区和字符串常量区。
常量区用于存储常量数据,如字符串常量和全局常量等。
常量区的数据是只读的,且在程序的整个生命周期内存在。
字符串常量区是常量区的一个子区域,用于存储字符串常量。
在C语言中,内存分配和释放是程序员的责任。
通过使用malloc和free等函数,程序员可以在堆中动态地分配和释放内存,从而灵活地管理程序的内存使用。
不过,应当注意避免内存泄漏和野指针等问题,以免出现内存错误和性能问题。
c语言cpu分配内存的原则

c语言cpu分配内存的原则:
以下是一些关于C语言中内存分配的原则:
1.静态存储区:这部分内存是在程序编译时分配的,包括全局变量和静态变量。
这些
变量的生命周期是整个程序的执行期间。
2.栈内存:这部分内存是在程序执行期间动态分配的,主要用来存储函数调用的局部
变量和函数参数。
当函数执行结束时,这部分内存会自动释放。
3.堆内存:这是动态内存分配区域,通过malloc,calloc等函数分配。
当不再需要这部
分内存时,应使用free函数释放。
需要注意的是,如果不正确地使用这些函数(例如,试图释放同一块内存两次或者在释放内存后继续使用它),可能会导致程序崩溃或未定义的行为。
4.代码段:也称为文本段,这是用来存储程序的二进制代码的区域。
这部分内存通常
不可写,因为它是只读的,以防止程序意外地修改其指令。
5.运行时内存分配:C语言标准库提供了一些函数用于在运行时动态分配和释放内存,
如malloc()、calloc()、realloc()和free()。
这些函数允许程序员在运行时分配和释放内存,这在处理大量数据或需要根据程序运行情况动态调整数据结构大小时非常有用。
c语言栈内存的申请

c语言栈内存的申请
C语言中的栈内存是一种用于存储局部变量和函数调用信息的内存区域。
在C语言中,栈内存的申请和释放是由编译器自动完成的,无需程序员手动管理。
当一个函数被调用时,编译器会在栈内存中为该函数分配一块内存空间,用于存储函数的局部变量、函数参数、返回地址等信息。
当函数执行完毕后,这块内存空间会被自动释放,以便其他函数使用。
在C语言中,栈内存的申请和释放是由编译器自动完成的,程序员无需关心具体的内存管理细节。
这种自动管理的方式使得编程更加方便和安全,避免了内存泄漏和内存溢出等问题。
然而,栈内存的大小是有限的,一般情况下只有几兆字节的大小。
因此,如果在函数中申请过多的局部变量或者使用递归调用等方式导致栈内存空间不足,就会发生栈溢出的错误。
为了避免这种情况的发生,程序员需要合理地设计函数和变量的使用,避免占用过多的栈内存空间。
总之,C语言中的栈内存是一种自动管理的内存区域,用于存储函数调用信息和局部变量。
程序员无需手动管理栈内存,但需要
注意避免栈溢出的问题。
通过合理设计函数和变量的使用,可以更好地利用栈内存的有限空间,确保程序的正常运行。
堆和栈,malloc分配的空间是堆,局部变量都在栈中

堆和栈,malloc分配的空间是堆,局部变量都在栈中堆和栈的区别⼀个由C/C++编译的程序占⽤的内存分为以下⼏个部分1、栈区(stack)— 由编译器⾃动分配释放,存放函数的参数值,局部变量的值等。
其操作⽅式类似于数据结构中的栈。
2、堆区(heap) — ⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。
注意它与数据结构中的堆是两回事,分配⽅式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在⼀块的,初始化的全局变量和静态变量在⼀块区域,未初始化的全局变量和未初始化的静态变量在相邻的另⼀块区域。
- 程序结束后由系统释放。
4、⽂字常量区 —常量字符串就是放在这⾥的。
程序结束后由系统释放5、程序代码区—存放函数体的⼆进制代码。
例⼦:#include <stdio.h>int a = 0; 全局初始化区char *p1; 全局未初始化区main(){ int b; 栈 char s[] = "abc"; 栈 char *p2; 栈 char *p3 = "123456"; 123456\0在常量区,p3在栈上。
static int c =0;全局(静态)初始化区 p1 = (char *)malloc(10); p2 = (char *)malloc(20); //分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成⼀个地⽅。
}。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
[C/C++] 函数调用的栈分配
压栈,跳转到被调函数的入口点。
进入被调函数时,函数将esp减去相应字节数获取局部变量存储空间。
被调函数返回(ret)时,将esp加上相应字节数,归还栈空间,弹出主调函数压在栈中的代码执行指针(eip),跳回主调函数。
再由主调函数恢复到调用前的栈。
为了访问函数局部变量,必须有方法定位每一个变量。
变量相对于栈顶esp的位置在进入函数体时就已确定,但是由于esp会在函数执行期变动,所以将esp 的值保存在ebp中,并事先将原ebp的值压栈保存,以声明中的顺序(即压栈的相反顺序)来确定偏移量。
访问函数的局部变量和访问函数参数的区别:
局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。
对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。
函数的返回值不同于函数参数,可以通过寄存器传递。
如果返回值类型可以放入32位变量,比如int、short、char、指针等类型,将通过eax寄存器传递。
如果返回值类型是64位变量,如_int64,则通过edx+eax传递,edx存储高32位,eax存储低32位。
如果返回值是浮点类型,如float和double,通过专用的浮点数寄存器栈的栈顶返回。
如果返回值类型是struct或class类型,编译器将通过隐式修改函数的签名,以引用型参数的形式传回。
由于函数返回值通过寄存器返回,不需要空间分配等操作,所以返回值的代价很低。
基于这个原因,C89规范中约定,不写明返回值类型的函数,返回值类型默认为int。
这一规则与现行的C++语法相违背,因为C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。
这种语法不兼容性是为了加强C++的类型安全,但同时也带来了一些代码兼容性问题。
代码示例
VarType Func (Arg1, Arg2, Arg3, ... ArgN)
{
VarType Var1, Var2, Var3, ...VarN;
//...
return VarN;
}
假设sizeof(VarType) = 4(DWORD), 则一次函数调用汇编代码示例为:
调用方代码:
push ArgN ; 依次逆序压入调用参数
push ...
push Arg1
call Func_Address ; 压入当前EIP后跳转
跳转至被调方代码:
push ebp ; 备份调用方EBP指针
mov ebp, esp ; 建立被调方栈底
sub esp, N * 4; 为局部变量分配空间
mov dword ptr[esp - 4 * 1 ], 0 ; 初始化各个局部变量= 0 这里假定VarType不是类
mov dword ptr[esp - 4 * ... ], 0
mov dword ptr[esp - 4 * N ], 0
. . . . . . ; 这里执行一些函数功能语句(比如将第N个参数[ebp + N * 4]存入局部变量), 功能完成后将函数返回值存至eax
add esp, N * 4 ; 销毁局部变量
mov esp, ebp ; 恢复主调方栈顶
pop ebp ; 恢复主调方栈底
ret ; 弹出EIP 返回主调方代码
接上面调用方代码:
add esp, N * 4 ; 释放参数空间, 恢复调用前的栈
mov dword ptr[ebp - 4], eax ; 将返回值保存进调用方的某个VarType型局部变量
进入函数时堆栈分配示意图
内存低地址| ESP - - - - - - - - - - - - - - - - EBP - - - - - - - - - - - - - - - - - - - - - >| 内存高地址
Stack State: VarN . . . Var3 Var2 Var1 SFP EIP Arg1 Arg2 Arg3 . . . ArgN
//资料区............................................................................................................................ ...
SFP 解释:除了堆栈指针(ESP指向堆栈顶部的的低地址)之外, 为了使用方便还有指向帧内固定地址的指针叫做帧指针(FP)。
有些文章把它叫做局部基指针(LB-local base pointer)。
从理论上来说, 局部变量可以用SP加偏移量来引用。
然而, 当有字被压栈和出栈后, 这些偏移量就变了。
尽管在某些情况下编译器能够跟踪栈中的字操作, 由此可以修正偏移量, 但是在某些情况下不能。
而且在所有情况下, 要引入可观的管理开销。
而且在有些机器上, 比如Intel处理器, 由SP加偏移量访问一个变量需要多条指令才能实现。
因此, 许多编译器使用第二个寄存器, FP, 对于局部变量和函数参数都可以引用, 因为它们到FP的距离不会受到PUSH和POP操作的影响。
在Intel CPU中, BP(EBP)用于这个目的。
在Motorola CPU 中, 除了A7(堆栈指针SP)之外的任何地址寄存器都可以做FP。
考虑到我们堆栈的增长方向, 从FP的位置开始计算, 函数参数的偏移量是正值, 而局部变量的偏移量是负值。
当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以恢复这个被保存的FP称为SFP)。
然后它把SP复制到FP, 创建新的FP, 把SP向前移动为局部变量保留空间。
这称为例程的序幕(prolog)工作。
当例程退出时, 堆栈必须被清除干净, 这称为例程的收尾(epilog)工作。
Intel 的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于有效地序幕和收尾工作。
所有局部变量都在栈中由函数统一分配,形成了类似逆序数组的结构,可以通过指针逐一访问。
这一特点具有很多有趣性质,比如,考虑如下函数,找出其中的错误及其造成的结果:
void f()
{
int i,a[10];
for(i=0;i<=10;++i)a[i]=0;/An error occurs here!
}
这个函数中包含的错误,即使是C++新手也很容易发现,这是老生常谈的越界访问问题。
但是这个错误造成的结果,是很多人没有想到的。
这次的越界访问,并不会像很多新手预料的那样造成一个“非法操作”消息,也不会像很多老手估计的那样会默不作声,而是导致一个死循环。
错误的本质显而易见,我们访问了a[10],但是a[10]并不存在。
C++标准对于越界访问只是说“未定义操作”。
我们知道,a[10]是数组a所在位置之后的一个位置,但问题是,是谁在这个位置上。
是i!
根据前面的讨论,i在数组a之前被声明,所以在a之前分配在栈上。
但是,I386上栈是向下增长的,所以,a的地址低于i的地址。
其结果是在循环的最后,a[i]引用到了i自己!接下来的事情就不难预见了,a[i],也就是i,被重置为0,然后继续循环的条件仍然成立……这个循环会一直继续下去,直到在你的帐单上产生高额电费,直到耗光地球电能,直到太阳停止燃烧……呵呵,或者直到聪明的你把程序Kill 了……。