这是我的第一篇博客。由于公司项目需要,暂时告别C语言。所以这里记录一下我之前学习C语言的一些心得,希望能分享给大家,也可以记录下自己在学习过程中遇到的问题和疑惑(其实就是我自己的地方学习过程中没看懂)。好了,废话不多说,开始微博内容,O(∩_∩)O哈哈~接下来我们通过以下问题来分析函数调用中对栈的理解:栈在内存中存储的结构在函数调用期间如何?汇编语言中的call、ret、leave等具体操作呢?linux中的任务栈和数据存储是怎样的?1、函数调用过程中内存中存放的栈是什么结构?计算机、嵌入式设备、智能设备等其实都是由软件和硬件组成的。具体实现可能比较复杂,但整体结构是一样的。软件在硬件上运行并告诉硬件做什么。操作系统软件在启动过程中通过BIOS、bootloader等(如果有这些进程)从磁盘加载到内存中,而自定义软件则写入存储在磁盘中,只会运行在加载后的内存。首先,我们先了解一下什么是堆,栈,栈。我们常说栈其实和栈是同一个概念。堆可以通俗的理解。堆是一块非常大的内存空间,不同的程序员可以拿出一段供自己使用。使用后,程序员必须释放它。如果不释放,这部分存储空间将无法使用。被其他程序使用。堆的存储空间是不连续的,因为在不同的时间申请不同大小的堆空间,会导致不连续。堆的增长是从低地址向高地址增长。对栈的理解就是,栈是系统或操作系统使用的一段存储空间。一般对程序员是不可见的,除非程序员一开始就通过汇编等方式自己建立栈,栈会由系统来管理。单位自行申请解除。栈从高地址向低地址增长,即栈底在高地址,栈顶在低地址。接下来,让我们看看应用程序的加载。应用程序加载到内存后,操作系统为其分配一个堆栈。程序的入口函数将是主函数。不过main函数并不是第一个被调用的函数,所以我们通过一个简单的例子来说明一下。#include#includeintfunction(intarg){returnarg;}intmain(void){inti=10;诠释j;j=函数(i);printf("%d\n",j);return0;}使用gcc-Smain.c生成汇编文件main.s,其中function的汇编代码如下:function:.LFB0:.cfi_startprocpushq%rbp.cfi_def_cfa_offset16.cfi_offset6,-16movq%rsp,%rbp.cfi_def_cfa_register6movl%edi,-4(%rbp)movl-4(%rbp),%eaxpopq%rbp.cfi_def_cfa7,8ret.cfi_endproc看看函数何时被调用,它会先将调用函数的栈底压入本函数的栈中(pushq%rbp),再将原函数栈顶的rsp作为当前函数的栈底(movq%rsp,%rbp)。当函数运行结束后,压入栈中的rbp会被重新弹出到rbp(popq%rbp)。当前函数汇编函数没有显示栈顶的变化(rsp的变化)。我们可以通过main函数看到栈顶的变化。汇编代码如下:main:.LFB1:.cfi_startprocpushq%rbp.cfi_def_cfa_offset16.cfi_offset6,-16movq%rsp,%rbp.cfi_def_cfa_register6subq$16,%rspmovl$10,-4(%rbp)movl-4(%rbp),%eaxmovl%eax,%edi调用函数movl%eax,-8(%rbp)movl-8(%rbp),%eaxmovl%eax,%esimovl$.LC0,%edimovl$0,%eaxcallprintfmovl$0,%eaxleave.cfi_def_cfa7,8ret.cfi_endproc从上面的汇编代码可以看出,压栈和设置新栈底的过程是第一个。由此可见,main函数也是被调用函数,而不是第一个调用函数。代码中黄色部分是当前栈顶的变化。从使用的subq可以知道,栈顶的地址小于栈底的地址,所以栈是从高地址向低地址增长的。下面可能有点乱,慢慢看,用语言描述函数调用过程。调用函数会将被调用函数的实参从右向左压入调用函数的栈中,通过call指令调用被调用函数。首先将返回地址(即调用指令后的下一条指令的地址)压入调用函数栈。此时rsp寄存器中存放的地址就是下一个存放返回地址内存地址的地址值。这时,调用函数的栈结构就形成了,进而进入被调用函数的作用域。被调用函数首先将调用函数的rbp压入被调用函数的栈中(其实这个地址就是rsp寄存器存放的地址),然后这个地址会作为被调用函数的rbp地址,这样就会有movq%rsp,%rbp指令设置被调用函数的栈底。构成上述函数调用的栈结构如下图所示。2、汇编语言中的call、ret、leave等具体操作呢? push:将数据压入栈中。具体操作是先将rsp递减,然后将数据压入sp指向的内存地址。rsp寄存器始终指向栈顶,但不指向空位置。 pop:从栈中弹出数据,然后加上rsp,保证rsp寄存器指向栈顶,不是空单元。 call:将下一条指令的地址压入当前调用函数的栈中(将PC指令压入栈中,因为PC指令在call指令取出内存时已经自动递增),然后改变PC指令的地址为调用函数的地址,程序指针跳转到新函数。 ret:当指令指向ret指令行时,表示一个函数已经结束。这时rsp已经从被调用函数的栈中指向了调用函数构造的返回地址。ret是将rsp指向的栈顶地址的内容赋值给PC,然后执行call函数的下一条指令。 leave:相当于mov%esp,%ebp,popebp。第一条指令实际上是把ebp指向的被调用函数的栈底作为新的栈顶。pop指令相当于弹出被调用函数的栈底,rsp指向返回地址。 int:在其后加上中断号,软件触发中断。Linux操作系统中的大多数系统调用都有这种实现。在其他实时操作系统中,操作系统移植的时候,也会有滴答心函数,也有这个实现。 这里就不说其他的汇编指令了,因为汇编指令很多,硬件cpu寄存器也因硬件不同而不同。这些指令与堆栈更改有关。自己构建汇编函数,或者阅读linux操作系统的系统调用,有助于理解。在硬件寄存器中,rsp和rbp用来表示栈顶和栈底。3、linux中的任务栈和数据存储是怎样的?linux中有两种任务栈:内核态栈和用户态栈。接下来简单介绍一下这两个栈。以后有机会的话,我会详细介绍这两个栈的。1.内核态栈Linux操作系统分为内核态和用户态。用户模式代码访问代码和数据受到很多限制。用户态主要是程序员用来写程序的。用户态的代码不能随便访问linux内核态的数据。这主要是为了安全考虑设置用户态权限。但是用户态可以通过系统调用接口、中断、异常等方式访问指定内核态的内容。内核模式主要用于操作系统内核的运行和管理。它可以不受限制地访问内存地址和数据,权限比较大。linux操作系统的进程是动态的,是有生命周期的。进程的运行和普通程序一样,需要栈的帮助。如果在内核存储区提前分配栈,会浪费内核内存(任务地址约3G空间),而且不能灵活构建任务,所以Linux操作系统在创建新任务时,会分配一块8k的存储空间用于存放进程内核态的栈和线程描述符的区域。线程描述符位于分配的存储区的低地址区,大小固定,而内核态栈则从存储区的高地址向低地址延伸。如果之前的版本为内核栈和线程描述符分配了4k的存储空间,那么需要为中断和异常额外分配一个栈,以防止任务栈溢出。2、用户态栈对于32位的Linux操作系统,每个任务都会有4G地址空间,其中0-3G为用户地址空间,3G-4G为内核地址空间。每个任务的创建都会有0-3G的用户地址空间,但是3G-4G的内核地址空间是所有任务共享的。这些地址都是线性地址,需要通过地址映射转换成物理地址。为了保证每个任务在访问0-3G的用户空间时不混淆地址,每个任务的内存管理单元都会有自己的页目录pgd,在任务开始时会创建一个新的pgd创建,任务会通过Addressmapping映射0-3G空间的物理地址。用户态栈分配在0-3G用户地址空间,和前面的main函数和function函数一样建栈,但是具体要映射到的物理地址需要内存管理单元执行映射操作。总之,linux任务用户态的栈和正常的应用程序一样由操作系统分配和释放,对程序员来说是不可见的。但是,由于操作系统的原因,任务用户程序的寻址是有限的。如果有机会,我会介绍一下我个人对Linux内存管理的理解。