函数调用过程中堆栈在内存中存放的结构如何?

📅 2026/7/3 1:23:08
函数调用过程中堆栈在内存中存放的结构如何?
算机嵌入式设备智能设备等其实都是有软件和硬件两部分组成具体实现也许复杂但整体的结构也就如此。软件运行在硬件上告诉硬件该干什么。操作系统软件是在启动过程中经过BIOSbootloarder等如果有这些过程的话从磁盘加载到内存中而自定义软件则是编写存放到磁盘中只有通过加载才会到内存中运行。首先我们来看一下什么是堆、栈还有堆栈我们经常说堆栈其实它是等同于栈的概念。可以通俗意义上这样理解堆堆是一段非常大的内存空间供不同的程序员从其中取出一段供自己使用使用之后要由程序员自己释放如果不释放的话这部分存储空间将不能被其他程序使用。堆的存储空间是不连续的因为会因为不同时间不同大小的堆空间的申请导致其不连续性。堆的生长是从低地址向高地址增长的。对栈的理解是栈是一段存储空间供系统或者操作系统使用对程序员来说一般是不可见的除非从一开始由程序员自己通过汇编等自己构建栈栈会由系统管理单元自己申请释放。栈是从高地址向低地址生长的既栈底在高地址栈顶低地址。其次我们看一下应用程序的加载应用程序被加载进内存后由操作系统为其分配堆栈程序的入口函数会是main函数。不过main函数也不是第一个被调用的函数我们通过简单的例子讲解。#include stdio.h #include string.h int function(int arg) { return arg; } int main(void) { int i 10; int j; j function(i); printf(%d\n,j); return 0; }用gcc -S main.c 生成汇编文件main.s, 其中function的汇编代码如下function: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) movl -4(%rbp), %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc看以看到当函数被调用时首先会把调用函数的栈底压栈到自己函数的栈中pushq %rbp然后将原来函数栈顶rsp作为当前函数的栈底movq %rsp, %rbp。函数运行完成时会将压入栈中的rbp重新出栈到rbp中popq %rbp。当前function汇编函数没有显示出栈顶的变化rsp的变化我们可以通过main函数来看栈顶的变化汇编代码如下main: .LFB1: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $10, -4(%rbp) movl -4(%rbp), %eax movl %eax, %edi call function movl %eax, -8(%rbp) movl -8(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc从上面的汇编代码可以看到首先也是压栈和设置新栈底的过程从此可以看出main函数也是被调用的函数而不是第一个调用函数。代码中的黄色部分是当前栈顶变化从使用的subq可以知道栈顶的地址要小于栈底的地址所以栈是从高地址向低地址生长。接下来可能有点绕慢慢读将用语言描述函数调用过程调用函数会将被调用函数的实参从右往左的顺序压入调用函数的栈中通过call指令调用被调用函数首先将return address也就是call指令的后一条指令的地址压入调用函数栈中这时rsp寄存器中存储的地址是存放return address内存地址的下一地址值这时调用函数的栈结构形成然后就会进入被调用函数的作用域中。被调用函数首先将调用函数的rbp压入被调用函数栈中其实这个地址就是rsp寄存器中存储的地址接下来将会将这个地址作为被调用函数的rbp地址才会有movq %rsp, %rbp指令设置被调用函数的栈底。如上所描述的构成了函数调用的堆栈结构如下图所示。此图来自http://www.cnblogs.com/taek/archive/2012/02/05/2338877.html此图中MOV EBP,ESP与本文的movq指令操作不同。2. 汇编语言中callretleave等具体操作时如何push将数据压入栈中具体操作是rsp先减然后将数据压入sp所指的内存地址中。rsp寄存器总是指向栈顶但不是空单元。pop将数据从栈中弹出然后rsp加操作确保rsp寄存器指向栈顶不是空单元。call将下一条指令的地址压入当前调用函数的栈中将PC指令压入栈中因为在从内存中取出call指令时PC指令已经自动增加然后改变PC指令的为call的function的地址程序指针跳转到新function。ret当指令指到ret指令行时说明一个函数已经结束了这时候rsp已经从被调用函数的栈指到了调用函数构建的返回地址位置。ret是将rsp所指栈顶地址中的内容赋值给PC接下来将执行call function的下一条指令。leave相当于mov %esp, %ebp, pop ebp。头一条指令其实是把ebp所指的被调用函数的栈底作为新的栈顶pop指令时相当于把被调用函数的栈底弹出rsp指向返回地址。int通过其后加中断号实现软件引发中断linux操作系统中系统调用多有此实现其他实时操作系统中在操作系统移植时会有tick心脏函数也有此实现。其他的汇编指令在此就不多讲了因为汇编指令众多硬件cpu寄存器也因硬件不同而不同此节就讲了函数构建进入和离开函数时用到的几个汇编指令这几条指令和栈变化有关。自己构建汇编函数或者是在读linux操作系统的系统调用时会对其理解有帮助。硬件寄存器中rsp和rbp用于指示栈顶和栈底。3. linux中任务的堆栈数据存放是如何linux的任务堆栈分为两种内核态堆栈和用户态堆栈。接下来简单介绍一下这两个堆栈如果以后有机会将详细介绍这两个堆栈。1. 内核态堆栈linux操作系统分为内核态和用户态。用户态代码访问代码和数据收到诸多限制用户态主要是为程序员编写程序使用处于用户态的代码不可以随便访问linux内核态的数据这主要就是设置用户态的权限安全考虑。但是用户态可以通过系统调用接口中断异常等访问指定内核态的内容。内核态主要是用于操作系统内核运行以及管理可以无限制的访问内存地址和数据权限比较大。linux操作系统的进程是动态的有生命周期进程的运行和普通的程序运行一样需要堆栈的帮助如果在内核存储区域内为其提前分配堆栈的话既浪费内核内存任务地址大约3G的空间也不能灵活的构建任务所以linux操作系统在创建新的任务时为其分配了8k的存储区域用于存放进程内核态的堆栈和线程描述符。线程描述符位于分配的存储区域的低地址区域大小固定而内核态堆栈则从存储区域的高地址开始向低地址延伸。如果之前版本为内核态堆栈和线程描述符分配4k的存储空间时则需要为中断和异常分配额外的栈供其使用防止任务堆栈溢出。