linux下linux shell main函数数的参数是在栈里还是在堆里

函数调用栈由连续的栈帧组成。每个栈帧记录一个函数调用的信息,这些信息包括函数参数,函数变量,函数运行地址。
当程序启动后,栈中只有一个帧,这个帧就是main函数的帧。我们把这个帧叫做初始...
函数调用过程栈帧变化详解
函数调用另一个词语表示叫作 过程。一个过程调用包括将数据和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在推出时释放这些空间。而...
gdb调试core文件,直接backtrace,然后查看用frame n切换栈帧。
info local可以查看当前栈的局部变量。对于烂掉的栈帧,可以参考如下文章来推测对应的符号。
http://...
栈:是程序存放数据内存区域之一,特点是LIFO(后进先出)。
PUSH:入栈
使用场景:
1.保存动态分配的自动变量使用栈
2.函数调用时,用栈传递函数参数,半寸返回地...
一个码农要是没遇见过coredump,那他就是神仙了。core file(coredump的转储文件)中保存的最重要内容之一,就是函数的call trace。还原这部分内容(栈回溯),并与原代...
我们知道,栈溢出通常是因为递归调用层次太深导致,那么为什么递归调用层次太深回导致栈溢出呢,解决这个问题
之前我们先看一下与函数调用有关的栈的基本概念:
1. 每一个线程拥有一个调用栈结构(...
地址栏中输入http://localhost:8080/ ,系统弹框提示“XDB的服务器localhost要求用户输入用户名和密码”,上网查了资料后才发现原来是因为tomcat的8080端口被另一个w...
Linux进程的栈和进程中函数的栈帧及其结构
主要理解了Linux进程的栈和函数的栈帧的区别和联系
1)如何从调用者进入被调者?call
2)如何从被调者返回调用者?ret
3)调用者如何支配被调者(...
他的最新文章
他的热门文章
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)Linux中main是如何执行的
这是一个看似简单的问题,但是要从Linux底层一点点研究问题比较多。找到了一遍研究这个问题的文章,但可能比较老了,还是在x86机器上进行的测试。
问题很简单:linux是怎么执行我的main()函数的?
在这片文档中,我将使用下面的一个简单c程序来阐述它是如何工作的。这个c程序的文件叫做&simple.c&
return (0);
gcc -o simple simple.c
生成可执行文件simple.
在可执行文件中有些什么?
为了看到在可执行文件中有什么,我们使用一个工具&objdump&
objdump -f simple
file format elf32-i386
architecture: i386, flags 0x:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x
输出给出了一些关键信息。首先,这个文件的格式是&ELF64&。其次是给出了程序执行的开始地址 &0x&
什么是ELF?
ELF是执行和链接格式(Execurable and Linking Format)的缩略词。它是UNIX系统的几种可执行文件格式中的一种。对于我们的这次探讨,有关ELF的有意思的地方是它的头格式。每个ELF可执行文件都有ELF头,像下面这个样子:
typedef struct
unsigned char
e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half
/* Object file type */
Elf32_Half
/* Architecture */
Elf32_Word
/* Object file version */
Elf32_Addr
/* Entry point virtual address */
/* Program header table file offset */
/* Section header table file offset */
Elf32_Word
/* Processor-specific flags */
Elf32_Half
/* ELF header size in bytes */
Elf32_Half
/* Program header table entry size */
Elf32_Half
/* Program header table entry count */
Elf32_Half
/* Section header table entry size */
Elf32_Half
/* Section header table entry count */
Elf32_Half
/* Section header string table index */
上面的结构中,&e_entry&字段是可执行文件的开始地址。
地址&0x&上存放的是什么?是程序执行的开始地址么?
对于这个问题,我们来对&simple&做一下反汇编。有几种工具可以用来对可执行文件进行反汇编。我在这里使用了objdump:
objdump --disassemble simple
输出结果有点长,我不会分析objdump的所有输出。我们的意图是看一下地址0x上存放的是什么。下面是输出:
$0xfffffff0,%esp
68 20 84 04 08
$0x8048420
68 74 82 04 08
$0x8048274
68 d0 83 04 08
$0x80483d0
e8 cb ff ff ff
80482bc &_init+0x48&
看上去开始地址上存放的是叫做&_start&的启动例程。它所做的是清空寄存器,向栈中push一些数据并且调用一个函数。
-------------------
-------------------
-------------------
-------------------
-------------------
-------------------
-------------------
-------------------
-------------------
现在,可能你已经想到了,关于这个我们有一些问题。
这些16进制数是什么?
地址80482bc上存放的是什么,哪个函数被_start调用了?
看起来这些汇编指令并没有用一些有意义的值来初始化寄存器。那么谁来初始化这些寄存器?
让我们来一个一个回答这个问题。
Q1&关于16进制数
如果你仔细研究了用objdump得到的反汇编输出,你就能很容易回答这个问题。
下面是这个问题的回答:
0x80483d0: 这是main()函数的地址。
0x8048274: _init()函数的地址。
0x8048420: _finit()函数地址。
_init和_finit是GCC提供的initialization/finalization 函数。
现在,我们不要去关心这些东西。基本上所有这些16进制数都是函数指针。
Q2&地址80482bc上存放的是什么?
让我们再次在反汇编输出中寻找地址80482bc。
如果你看到了,汇编代码如下:
ff 25 48 95 04 08
*0x8049548
这里的*0x8049548是一个指针操作。它跳到地址0x8049548存储的地址值上。
更多关于ELF和动态链接
使用ELF,我们可以编译出一个可执行文件,它动态链接到几个libraries上。这里的&动态链接&意味着实际的链接过程发生在运行时。否则我们就得编译出一个巨大的可执行文件,这个文件包含了它所调用的所有libraries(&一个『静态链接的可执行文件』&)。如果你执行下面的命令:
ldd simple
libc.so.6 =& /lib/i686/libc.so.6 (0x)
/lib/ld-linux.so.2 =& /lib/ld-linux.so.2 (0x)
你就能看到simple动态链接的所有libraries。所有动态链接的数据和函数都有『动态重定向入口(dynamic relocation entry)』。
这个概念粗略的讲述如下:
在链接时我们不会得知一个动态符号的实际地址。只有在运行时我们才能知道这个实际地址。
所以对于动态符号,我们为其实际地址预留出了存储单元。加载器会在运行时用动态符号的实际地址填充存储单元。
我们的应用通过使用一种指针操作来间接得知动态符号的存储单元。在我们的例子中,在地址80482bc上,有一个简单的jump指令。jump到的单元由加载器在运行时存储到地址0x8049548上。
我们通过使用objdump命令可以看到所有的动态链接入口:
objdump -R simple
file format elf32-i386
DYNAMIC RELOCATION RECORDS
0804954c R_386_GLOB_DAT
__gmon_start__
R_386_JUMP_SLOT
__register_frame_info
R_386_JUMP_SLOT
__deregister_frame_info
R_386_JUMP_SLOT
__libc_start_main
这里的地址0x8049548被叫做&JUMP SLOT&,非常贴切。根据这个表,实际上我们想调用的是 __libc_start_main。
__libc_start_main是什么?
我们在玩一个接力游戏,现在球被传到了libc的手上。__libc_start_main是libc.so.6中的一个函数。如果你在glibc中查找__libc_start_main的源码,它的原型可能是这样的:
extern int BP_SYM (__libc_start_main) (int (*main) (int, char **, char **),
char *__unbounded *__unbounded ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void *__unbounded stack_end)
__attribute__ ((noreturn));
所有汇编指令需要做的就是建立一个参数栈然后调用__libc_start_main。这个函数需要做的是建立/初始化一些数据结构/环境然后调用我们的main()。让我们看一下关于这个函数原型的栈帧,
-------------------
-------------------
-------------------
-------------------
-------------------
-------------------
_rtlf_fini
-------------------
-------------------
-------------------
根据这个栈帧我们得知,esi,ecx,edx,esp,eax寄存器在函数 __libc_start_main()被执行前需要被填充合适的值。很清楚的是这些寄存器不是被前面我们所展示的启动汇编指令所填充的。那么,谁填充了这些寄存器呢?现在只留下唯一的一个地方了——内核。现在让我们回到第三个问题上。
Q3&内核做了些什么?
当我们通过在shell上输入一个名字来执行一个程序时,下面是Linux接下来会发生的:
Shell调用内核的带argc/argv参数的系统调用&execve&。
内核的系统调用句柄开始处理这个系统调用。在内核代码中,这个句柄为&sys_execve&.在x86机器上,用户模式的应用会通过以下寄存器将所有需要的参数传递到内核中。
ebx:执行程序名字的字符串
ecx:argv数组指针
edx:环境变量数组指针
通用的execve内核系统调用句柄——也就是do_execve——被调用。它所做的是建立一个数据结构,将所有用户空间数据拷贝到内核空间,最后调用search_binary_handler()。Linux能够同时支持多种可执行文件格式,例如a.out和ELF。对于这个功能,存在一个数据结构&struct linux_binfmt&,对于每个二进制格式的加载器在这个数据结构都会有一个函数指针。search_binary_handler()会找到一个合适的句柄并且调用它。在我们的例子中,这个合适的句柄是load_elf_binary()。解释函数的每个细节是非常乏味的工作。所以我在这里就不这么做了。如果你感兴趣,阅读相关的书籍即可。接下来是函数的结尾部分,首先为文件操作建立内核数据结构,来读入ELF映像。然后它建立另一个内核数据结构,这个数据结构包含:代码容量,数据段开始处,堆栈段开始处,等等。然后为这个进程分配用户模式页,将argv和环境变量拷贝到分配的页面地址上。最后,argc和argv指针,环境变量数组指针通过create_elf_tables()被push到用户模式堆栈中,使用start_thread()让进程开始执行起来。
当执行_start汇编指令时,栈帧会是下面这个样子。
-------------
-------------
argv pointer
-------------
env pointer
-------------
汇编指令通过以下方式从栈中获取所有信息:
&--- get argc
move %esp, %ecx
&--- get argv
actually the argv address is the same as the current
stack pointer.
现在所有东西都准备好了,可以开始执行了。
其他的寄存器呢?
对于esp来说,它被用来当做应用程序的栈底。在弹出所有必要信息之后,_start例程简单的调整了栈指针(esp)——关闭了esp寄存器4个低地址位,这完全是有道理的,对于我们的main程序,这就是栈底。对于edx,它被rtld_fini使用,这是一种应用析构函数,内核使用下面的宏定义将它设为0:
#define ELF_PLAT_INIT(_r)
_r-&ebx = 0; _r-&ecx = 0; _r-&edx = 0; \
_r-&esi = 0; _r-&edi = 0; _r-&ebp = 0; \
_r-&eax = 0; \
} while (0)
0意味着在x86 Linux上我们不会使用这个功能。
关于汇编指令
这些汇编codes来自哪里?它是GCC codes的一部分。这些code的目标文件通常在/usr/lib/gcc-lib/i386-redhat-linux/XXX 和 /usr/lib下面,XXX是gcc版本号。文件名为crtbegin.o,crtend.o和gcrt1.o。
我们总结一下整个过程。
GCC将你的程序同crtbegin.o/crtend.o/gcrt1.o一块进行编译。其它默认libraries会被默认动态链接。可执行程序的开始地址被设置为_start。
内核加载可执行文件,并且建立正文段,数据段,bss段和堆栈段,特别的,内核为参数和环境变量分配页面,并且将所有必要信息push到堆栈上。
控制流程到了_start上面。_start从内核建立的堆栈上获取所有信息,为__libc_start_main建立参数栈,并且调用__libc_start_main。
__libc_start_main初始化一些必要的东西,特别是C library(比如malloc)线程环境并且调用我们的main函数。
我们的main会以main(argv,argv)来被调用。事实上,这里有意思的一点是main函数的签名。__libc_start_main认为main的签名为main(int, char , char ),如果你感到好奇,尝试执行下面的程序。
main(int argc, char** argv, char** env)
int i = 0;
while(env[i] != 0)
printf(&%s\n&, env[i++]);
return(0);
在Linux中,我们的C main()函数由GCC,libc和Linux二进制加载器的共同协作来执行。
&man objdump&
ELF header
/usr/include/elf.h
__libc_start_main
glibc source
./sysdeps/generic/libc-start.c
sys_execve
linux kernel source code
arch/i386/kernel/process.c
linux kernel source code
struct linux_binfmt
linux kernel source code
include/linux/binfmts.h
load_elf_binary
linux kernel source code
fs/binfmt_elf.c
create_elf_tables
linux kernel source code
fs/binfmt_elf.c
start_thread
linux kernel source code
include/asm/processor.h
阅读(...) 评论()一、malloc()和free()的基本概念以及基本用法:
1、函数原型及说明:
void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内...
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样...
char* r = "hello word!";
char b[]="hello word!"
其实应该是语法错误,可是...
在计算机领域,堆栈是一个不容忽视的概念,但是很多人甚至是计算机专业的人也没有明确堆栈其实是两种数据结构。要点:堆:顺序随意栈:先进后出堆和栈的区别 一、预备知识—程序的内存分配 一个由c/C++编译...
内容非原创,但也绝不是转载,实在选不好分类,索性就原创了,惭愧惭愧。闲话少说,进入正题。
昨天在做个试题时遇到了个问题,自己写了个strcat函数,在测试过程中出现了错误,很让我诧异啊,感觉自己写代...
在计算机领域,堆栈是一个不容忽视的概念,但是很多人甚至是计算机专业的人也没有明确堆栈其实是两种数据结构。
堆:顺序随意
栈:先进后出
堆和栈的区别
一、预备知识—程序的内存分配...
内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄...
在计算机系统中,运行的应用程序的数据都是保存在内存中的,不同类型的数据,保存的内存区域不同。
一、内存分区
栈区(stack) 由编译器自动分配并释放,存放函数的参数值,局部变量等。栈...
他的最新文章
他的热门文章
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)Linux系统中,main函数的执行过程
我的图书馆
Linux系统中,main函数的执行过程
How main() is executed on Linux &
1. 问题:linux如何执行main函数。
& & 本文使用一个简单的C程序(simple.c)作为例子讲解。代码如下,
[cpp] int&main()&&{&&&&&&return(0);&&}&&
~#gcc -o simple simple.c
3. 查看可执行文件的基本信息
~#objdump -f simple
file format elf32-i386
architecture: i386, flags 0x:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x
借助objdump这个工具,可以获得可执行文件的一些关键信息。
比如,simple文件的格式是“ELF32”,该文件的起始地址是0x80482d0,,等。
4. 什么是ELF
ELF是Executable and Linking Format的缩写,是Unix上常见的几种目标文件格式(及可执行文件格式)之一。
ELF的头部结构提供了ELF文件的基本信息,其数据结构可以在
[cpp] typedef&struct&&{&&&&&&unsigned&char&&&e_ident[EI_NIDENT];&/*&Magic&number&and&other&info&*/&&&&&&Elf32_Half&&e_&&&&&&&&&/*&Object&file&type&*/&&&&&&Elf32_Half&&e_&&&&&&/*&Architecture&*/&&&&&&Elf32_Word&&e_&&&&&&/*&Object&file&version&*/&&&&&&Elf32_Addr&&e_&&&&&&&&/*&Entry&point&virtual&address&*/&&&&&&Elf32_Off&&&e_&&&&&&&&/*&Program&header&table&file&offset&*/&&&&&&Elf32_Off&&&e_&&&&&&&&/*&Section&header&table&file&offset&*/&&&&&&Elf32_Word&&e_&&&&&&&&/*&Processor-specific&flags&*/&&&&&&Elf32_Half&&e_&&&&&&&/*&ELF&header&size&in&bytes&*/&&&&&&Elf32_Half&&e_&&&&&&&&/*&Program&header&table&entry&size&*/&&&&&&Elf32_Half&&e_&&&&&&&&/*&Program&header&table&entry&count&*/&&&&&&Elf32_Half&&e_&&&&&&&&/*&Section&header&table&entry&size&*/&&&&&&Elf32_Half&&e_&&&&&&&&/*&Section&header&table&entry&count&*/&&&&&&Elf32_Half&&e_&&&&&/*&Section&header&string&table&index&*/&&}&Elf32_E&&
其中,e_entry存储了该执行文件的起始地址。
[cpp] 80482d0&&_start&:&&80482d0:&&&&&&&31&ed&&&&&&&&&&&&&&&&&&&xor&&&&%ebp,%ebp&&80482d2:&&&&&&&5e&&&&&&&&&&&&&&&&&&&&&&pop&&&&%esi&&80482d3:&&&&&&&89&e1&&&&&&&&&&&&&&&&&&&mov&&&&%esp,%ecx&&80482d5:&&&&&&&83&e4&f0&&&&&&&&&&&&&&&&and&&&&$0xfffffff0,%esp&&80482d8:&&&&&&&50&&&&&&&&&&&&&&&&&&&&&&push&&&%eax&&80482d9:&&&&&&&54&&&&&&&&&&&&&&&&&&&&&&push&&&%esp&&80482da:&&&&&&&52&&&&&&&&&&&&&&&&&&&&&&push&&&%edx&&80482db:&&&&&&&68&20&84&04&08&&&&&&&&&&push&&&$0x8048420&&80482e0:&&&&&&&68&74&82&04&08&&&&&&&&&&push&&&$0x8048274&&80482e5:&&&&&&&51&&&&&&&&&&&&&&&&&&&&&&push&&&%ecx&&80482e6:&&&&&&&56&&&&&&&&&&&&&&&&&&&&&&push&&&%esi&&80482e7:&&&&&&&68&d0&83&04&08&&&&&&&&&&push&&&$0x80483d0&&80482ec:&&&&&&&e8&cb&ff&ff&ff&&&&&&&&&&call&&&80482bc&&_init+0x48&&&80482f1:&&&&&&&f4&&&&&&&&&&&&&&&&&&&&&&hlt&&&&&&80482f2:&&&&&&&89&f6&&&&&&&&&&&&&&&&&&&mov&&&&%esi,%esi&&
80482bc: ff 25 48 95 04 08
*0x8049548
可以看到这是一个跳转指令,跳转到0x8049548存储的代码地址。
[cpp] libc.so.6&=&&/lib/i686/libc.so.6&(0x)&&/lib/ld-linux.so.2&=&&/lib/ld-linux.so.2&(0x)&&
[cpp] simple:&&&&&file&format&elf32-i386&&&&DYNAMIC&RELOCATION&RECORDS&&OFFSET&&&TYPE&&&&&&&&&&&&&&VALUE&&&0804954c&R_386_GLOB_DAT&&&&__gmon_start__&&&R_386_JUMP_SLOT&&&__register_frame_info&&&R_386_JUMP_SLOT&&&__deregister_frame_info&&&R_386_JUMP_SLOT&&&__libc_start_main&&可以看到,call 0x80482bc实际上会跳转到__libc_start_main。
[cpp] extern&int&BP_SYM&(__libc_start_main)&(int&(*main)&(int,&char&**,&char&**),&&&&&&&&&&int&argc,&&&&&&&&&&char&*__unbounded&*__unbounded&ubp_av,&&&&&&&&&&void&(*init)&(void),&&&&&&&&&&void&(*fini)&(void),&&&&&&&&&&void&(*rtld_fini)&(void),&&&&&&&&&&void&*__unbounded&stack_end)&&__attribute__&((noreturn));&&
再联系前面_start的代码,可以得到这样的映射:
[cpp] Stack&Top&&&&&&&&&&&-------------&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&argc&&&&&&&&&&&&&&&&&&&&&&&&&&-------------&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&argv&pointer&&&&&&&&&&&&&&&&&&&&&&&&&&-------------&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&env&pointer&&&&&&&&&&&&&&&&&&&&&&&&&&-------------&&&
然后,这些信息传递给_start函数:
TA的最新馆藏[转]&[转]&
喜欢该文的人也喜欢Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈
转载请注明出处:
栈是什么?栈有什么作用?
首先,栈 (stack) 是一种串列形式的 数据结构。这种数据结构的特点是 后入先出 (LIFO, Last In First Out),数据只能在串列的一端 (称为:栈顶 top) 进行 推入 (push) 和 弹出 (pop) 操作。根据栈的特点,很容易的想到可以利用数组,来实现这种数据结构。但是本文要讨论的并不是软件层面的栈,而是硬件层面的栈。
大多数的处理器架构,都有实现硬件栈。有专门的栈指针寄存器,以及特定的硬件指令来完成 入栈/出栈 的操作。例如在 ARM 架构上,R13 (SP) 指针是堆栈指针寄存器,而 PUSH 是用于压栈的汇编指令,POP 则是出栈的汇编指令。
【扩展阅读】:
ARM 处理器拥有 37 个寄存器。 这些寄存器按部分重叠组方式加以排列。 每个处理器模式都有一个不同的寄存器组。 编组的寄存器为处理处理器异常和特权操作提供了快速的上下文切换。
提供了下列寄存器:
- 三十个 32 位通用寄存器:
- 存在十五个通用寄存器,它们分别是 r0-r12、sp、lr
- sp (r13) 是堆栈指针。C/C++ 编译器始终将 sp 用作堆栈指针
- lr (r14) 用于存储调用子例程时的返回地址。如果返回地址存储在堆栈上,则可将 lr 用作通用寄存器
- 程序计数器 (pc):指令寄存器
- 应用程序状态寄存器 (APSR):存放算术逻辑单元 (ALU) 状态标记的副本
- 当前程序状态寄存器 (CPSR):存放 APSR 标记,当前处理器模式,中断禁用标记等
- 保存的程序状态寄存器 (SPSR):当发生异常时,使用 SPSR 来存储 CPSR
上面是栈的原理和实现,下面我们来看看栈有什么作用。栈作用可以从两个方面体现:函数调用 和 多任务支持 。
一、函数调用
我们知道一个函数调用有以下三个基本过程:
- 调用参数的传入
- 局部变量的空间管理
- 函数返回
函数的调用必须是高效的,而数据存放在 CPU通用寄存器 或者 RAM 内存 中无疑是最好的选择。以传递调用参数为例,我们可以选择使用 CPU通用寄存器 来存放参数。但是通用寄存器的数目都是有限的,当出现函数嵌套调用时,子函数再次使用原有的通用寄存器必然会导致冲突。因此如果想用它来传递参数,那在调用子函数前,就必须先 保存原有寄存器的值,然后当子函数退出的时候再 恢复原有寄存器的值 。
函数的调用参数数目一般都相对少,因此通用寄存器是可以满足一定需求的。但是局部变量的数目和占用空间都是比较大的,再依赖有限的通用寄存器未免强人所难,因此我们可以采用某些 RAM 内存区域来存储局部变量。但是存储在哪里合适?既不能让函数嵌套调用的时候有冲突,又要注重效率。
这种情况下,栈无疑提供很好的解决办法。一、对于通用寄存器传参的冲突,我们可以再调用子函数前,将通用寄存器临时压入栈中;在子函数调用完毕后,在将已保存的寄存器再弹出恢复回来。二、而局部变量的空间申请,也只需要向下移动下栈顶指针;将栈顶指针向回移动,即可就可完成局部变量的空间释放;三、对于函数的返回,也只需要在调用子函数前,将返回地址压入栈中,待子函数调用结束后,将函数返回地址弹出给 PC 指针,即完成了函数调用的返回;
于是上述函数调用的三个基本过程,就演变记录一个栈指针的过程。每次函数调用的时候,都配套一个栈指针。即使循环嵌套调用函数,只要对应函数栈指针是不同的,也不会出现冲突。
【扩展阅读】:
函数调用经常是嵌套的,在同一时刻,栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等,函数调用时入栈的顺序为:
实参N~1 → 主调函数返回地址 → 主调函数帧基指针EBP → 被调函数局部变量1~N
栈帧的边界由 栈帧基地址指针 EBP 和 栈指针 ESP 界定,EBP 指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。函数调用栈的典型内存布局如下图所示:
二、多任务支持
然而栈的意义还不只是函数调用,有了它的存在,才能构建出操作系统的多任务模式。我们以 main 函数调用为例,main 函数包含一个无限循环体,循环体中先调用 A 函数,再调用 B 函数。
func main():
试想在单处理器情况下,程序将永远停留在此 main 函数中。即使有另外一个任务在等待状态,程序是没法从此 main 函数里面跳转到另一个任务。因为如果是函数调用关系,本质上还是属于 main 函数的任务中,不能算多任务切换。此刻的 main 函数任务本身其实和它的栈绑定在了一起,无论如何嵌套调用函数,栈指针都在本栈范围内移动。
由此可以看出一个任务可以利用以下信息来表征:
1. main 函数体代码
2. main 函数栈指针
3. 当前 CPU 寄存器信息
假如我们可以保存以上信息,则完全可以强制让出 CPU 去处理其他任务。只要将来想继续执行此 main 任务的时候,把上面的信息恢复回去即可。有了这样的先决条件,多任务就有了存在的基础,也可以看出栈存在的另一个意义。在多任务模式下,当调度程序认为有必要进行任务切换的话,只需保存任务的信息(即上面说的三个内容)。恢复另一个任务的状态,然后跳转到上次运行的位置,就可以恢复运行了。
可见每个任务都有自己的栈空间,正是有了独立的栈空间,为了代码重用,不同的任务甚至可以混用任务的函数体本身,例如可以一个main函数有两个任务实例。至此之后的操作系统的框架也形成了,譬如任务在调用 sleep() 等待的时候,可以主动让出 CPU 给别的任务使用,或者分时操作系统任务在时间片用完是也会被迫的让出 CPU。不论是哪种方法,只要想办法切换任务的上下文空间,切换栈即可。
【扩展阅读】:任务、线程、进程 三者关系
任务是一个抽象的概念,即指软件完成的一个活动;而线程则是完成任务所需的动作;进程则指的是完成此动作所需资源的统称;关于三者的关系,有一个形象的比喻:
- 任务 = 送货
- 线程 = 开送货车
- 系统调度 = 决定合适开哪部送货车
- 进程 = 道路 + 加油站 + 送货车 + 修车厂
Linux 中有几种栈?各种栈的内存位置?
介绍完栈的工作原理和用途作用后,我们回归到 Linux 内核上来。内核将栈分成四种:
一、进程栈
进程栈是属于用户态栈,和进程 虚拟地址空间 (Virtual Address Space) 密切相关。那我们先了解下什么是虚拟地址空间:在 32 位机器下,虚拟地址空间大小为 4G。这些虚拟地址通过页表 (Page Table) 映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元 (MMU) 硬件引用。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间。
Linux 内核将这 4G 字节的空间分为两部分,将最高的 1G 字节(0xCxFFFFFFFF)供内核使用,称为 内核空间。而将较低的3G字节(0xxBFFFFFFF)供各个进程使用,称为 用户空间。每个进程可以通过系统调用陷入内核态,因此内核空间是由所有进程共享的。虽然说内核和用户态进程占用了这么大地址空间,但是并不意味它们使用了这么多物理内存,仅表示它可以支配这么大的地址空间。它们是根据需要,将物理内存映射到虚拟地址空间中使用。
Linux 对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下:
- 程序段 (Text Segment):可执行文件代码的内存映射
- 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射
- BSS段 (BSS
Segment):未初始化的全局变量或者静态变量(用零页初始化)
- 堆区 (Heap) : 存储动态内存分配,匿名的内存映射
- 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等
- 映射段(Memory Mapping Segment):任何内存映射文件
而上面进程虚拟地址空间中的栈区,正指的是我们所说的进程栈。进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux 内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表)。但是并不是说栈区可以无限增长,它也有最大限制 RLIMIT_STACK (一般为 8M),我们可以通过 ulimit 来查看或更改 RLIMIT_STACK 的值。
【扩展阅读】:如何确认进程栈的大小
我们要知道栈的大小,那必须得知道栈的起始地址和结束地址。栈起始地址 获取很简单,只需要嵌入汇编指令获取栈指针 esp 地址即可。栈结束地址 的获取有点麻烦,我们需要先利用递归函数把栈搞溢出了,然后再 GDB 中把栈溢出的时候把栈指针 esp 打印出来即可。代码如下:
void *orig_stack_
void blow_stack() {
blow_stack();
int main() {
__asm__("movl %esp, orig_stack_pointer");
blow_stack();
$ g++ -g stacksize.c -o ./stacksize
$ gdb ./stacksize
Starting program: /home/home/misc-code/setrlimit
Program received signal SIGSEGV, Segmentation fault.
blow_stack () at setrlimit.c:4
blow_stack();
(gdb) print (void *)$esp
$1 = (void *) 0xffffffffff7ff000
(gdb) print (void *)orig_stack_pointer
$2 = (void *) 0xffffc800
(gdb) print 0xffffc800-0xff7ff000
$3 = 8378368
上面对进程的地址空间有个比较全局的介绍,那我们看下 Linux 内核中是怎么体现上面内存布局的。内核使用内存描述符来表示进程的地址空间,该描述符表示着进程所有地址空间的信息。内存描述符由 mm_struct 结构体表示,下面给出内存描述符结构中各个域的描述,请大家结合前面的 进程内存段布局 图一起看:
struct mm_struct {
struct vm_area_struct *
struct rb_root mm_
struct list_
unsigned long total_
unsigned long locked_
unsigned long pinned_
unsigned long shared_
unsigned long exec_
unsigned long stack_
unsigned long def_
unsigned long start_code, end_code, start_data, end_
unsigned long start_brk, brk, start_
unsigned long arg_start, arg_end, env_start, env_
mm_context_
unsigned long
【扩展阅读】:进程栈的动态增长实现
进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)。通过异常陷入内核态后,异常会被内核的 expand_stack() 函数处理,进而调用 acct_stack_growth() 来检查是否还有合适的地方用于栈的增长。
如果栈的大小低于 RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。然而,如果达到了最大栈空间的大小,就会发生 栈溢出(stack overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号。
动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。
二、线程栈
从 Linux 内核的角度来说,其实它并没有线程的概念。Linux 把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了 task_struct 中。线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别。线程创建的时候,加上了 CLONE_VM 标记,这样 线程的内存描述符 将直接指向 父进程的内存描述符。
if (clone_flags & CLONE_VM) {
atomic_inc(&current-&mm-&mm_users);
tsk-&mm = current-&
虽然线程的地址空间和进程一样,但是对待其地址空间的 stack 还是有些区别的。对于 Linux 进程或者说主线程,其 stack 是在 fork 的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。然而对于主线程生成的子线程而言,其 stack 将不再是这样的了,而是事先固定下来的,使用 mmap 系统调用,它不带有 VM_STACK_FLAGS 标记。这个可以从 glibc 的nptl/allocatestack.c 中的 allocate_stack() 函数中看到:
mem = mmap (NULL, size, prot,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
由于线程的 mm-&start_stack 栈地址和所属进程相同,所以线程栈的起始地址并没有存放在 task_struct 中,应该是使用 pthread_attr_t 中的 stackaddr 来初始化 task_struct-&thread-&sp(sp 指向 struct pt_regs 对象,该结构体用于保存用户进程或者线程的寄存器现场)。这些都不重要,重要的是,线程栈不能动态增长,一旦用尽就没了,这是和生成进程的 fork 不同的地方。由于线程栈是从进程的地址空间中 map 出来的一块内存区域,原则上是线程私有的。但是同一个进程的所有线程生成的时候浅拷贝生成者的 task_struct 的很多字段,其中包括所有的 vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。
三、进程内核栈
在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。进程内核栈在进程创建的时候,通过 slab 分配器从 thread_info_cache 缓存池中分配出来,其大小为 THREAD_SIZE,一般来说是一个页大小 4K;
union thread_union {
struct thread_info thread_
unsigned long stack[THREAD_SIZE/sizeof(long)];
thread_union 进程内核栈 和 task_struct 进程描述符有着紧密的联系。由于内核经常要访问 task_struct,高效获取当前进程的描述符是一件非常重要的事情。因此内核将进程内核栈的头部一段空间,用于存放 thread_info 结构体,而此结构体中则记录了对应进程的描述符,两者关系如下图(对应内核函数为 dup_task_struct()):
有了上述关联结构后,内核可以先获取到栈顶指针 esp,然后通过 esp 来获取 thread_info。这里有一个小技巧,直接将 esp 的地址与上 ~(THREAD_SIZE - 1) 后即可直接获得 thread_info 的地址。由于 thread_union 结构体是从 thread_info_cache 的 Slab 缓存池中申请出来的,而 thread_info_cache 在 kmem_cache_create 创建的时候,保证了地址是 THREAD_SIZE 对齐的。因此只需要对栈指针进行 THREAD_SIZE 对齐,即可获得 thread_union 的地址,也就获得了 thread_union 的地址。成功获取到 thread_info 后,直接取出它的 task 成员就成功得到了 task_struct。其实上面这段描述,也就是 current 宏的实现方法:
register unsigned long current_stack_pointer asm ("sp");
static inline struct thread_info *current_thread_info(void)
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
#define get_current() (current_thread_info()-&task)
#define current get_current()
四、中断栈
进程陷入内核态的时候,需要内核栈来支持内核函数调用。中断也是如此,当系统收到中断事件后,进行中断处理的时候,也需要中断栈来支持函数调用。由于系统中断的时候,系统当然是处于内核态的,所以中断栈是可以和内核栈共享的。但是具体是否共享,这和具体处理架构密切相关。
X86 上中断栈就是独立于内核栈的;独立的中断栈所在内存空间的分配发生在 arch/x86/kernel/irq_32.c 的 irq_ctx_init() 函数中(如果是多处理器系统,那么每个处理器都会有一个独立的中断栈),函数使用 __alloc_pages 在低端内存区分配 2个物理页面,也就是8KB大小的空间。有趣的是,这个函数还会为 softirq 分配一个同样大小的独立堆栈。如此说来,softirq 将不会在 hardirq 的中断栈上执行,而是在自己的上下文中执行。
而 ARM 上中断栈和内核栈则是共享的;中断栈和内核栈共享有一个负面因素,如果中断发生嵌套,可能会造成栈溢出,从而可能会破坏到内核栈的一些重要数据,所以栈空间有时候难免会捉襟见肘。
Linux 为什么需要区分这些栈?
为什么需要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供大家讨论:
为什么需要单独的进程内核栈?
所有进程运行的时候,都可能通过系统调用陷入内核态继续执行。假设第一个进程 A 陷入内核态执行的时候,需要等待读取网卡的数据,主动调用 schedule() 让出 CPU;此时调度器唤醒了另一个进程 B,碰巧进程 B 也需要系统调用进入内核态。那问题就来了,如果内核栈只有一个,那进程 B 进入内核态的时候产生的压栈操作,必然会破坏掉进程 A 已有的内核栈数据;一但进程 A 的内核栈数据被破坏,很可能导致进程 A 的内核态无法正确返回到对应的用户态了;
为什么需要单独的线程栈?
Linux 调度程序中并没有区分线程和进程,当调度程序需要唤醒”进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈;但是线程和父进程完全共享一份地址空间,如果栈也用同一个那就会遇到以下问题。假如进程的栈指针初始值为 0x7ffc;父进程 A 先执行,调用了一些函数后栈指针 esp 为 0x7ffc8000FF00,此时父进程主动休眠了;接着调度器唤醒子线程 A1:
此时 A1 的栈指针 esp 如果为初始值 0x7ffc,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据。
如果此时线程 A1 的栈指针和父进程最后更新的值一致,esp 为 0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?无论栈指针被设置到哪个值,都会有问题不是吗?
进程和线程是否共享一个内核栈?
No,线程和进程创建的时候都调用 dup_task_struct 来创建 task 相关结构体,而内核栈也是在此函数中 alloc_thread_info_node 出来的。因此虽然线程和进程共享一个地址空间 mm_struct,但是并不共享一个内核栈。
为什么需要单独中断栈?
这个问题其实不对,ARM 架构就没有独立的中断栈。
大家还有什么观点,可以在留言下来 :-D
文章写到这也就结束了,您要是还能看到这,我一定要表示忠心的感谢,文章是在太长了,我都写快一个星期了。好了,终于可以 Released 了,hexo deploy,再会!
看过本文的人也看了:
我要留言技术领域:
取消收藏确定要取消收藏吗?
删除图谱提示你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?
删除节点提示无法删除该知识节点,因该节点下仍保存有相关知识内容!
删除节点提示你确定要删除该知识节点吗?

我要回帖

更多关于 linux 打印函数调用栈 的文章

 

随机推荐