Linux 0.11 里面的 inline 问题
Linux 0.11 里面的 inline 问题
问题描述
如下所示linux 0.11中main函数关于fork和pause的定义如下:
1 | static inline _syscall0(int,fork) |
本篇文章讨论这里的inline是否有存在的必要,如果去掉会引发什么后果
c 程序运行结构分析
函数调用与执行
为了更好的解决该问题,我们首先需要分析一下c程序运行的结构
以如下一段简单的代码为例:
1 | int fun(int a,int b); |
程序运行时,内存中展示了如下三个区域

代码区里面装载了要执行的机器指令,静态数据区装载了全局变量m。动态数据区,在程序没执行的时候还什么都没有,只有在函数执行的时候才会进行压栈清栈的操作。
cpu中有三种寄存器,eip,ebp,esp。eip指向要执行的下一条语句,他有两种方式:一个是顺序执行,一个是跳转。
ebp和esp用于管理栈,ebp指向栈底,esp指向栈顶。
运行初始时,eip指向main函数第一条指令,eip和esp位置由操作系统内核设定。

此时将ebp当前的位置压栈,保证在函数执行完以后,ebp还能回到原来的位置,esp自动向下移动指向栈顶。、
下面开始构造main函数自己的栈
因为上面ebp原来的值已经压栈保存了,现在把ebp向下移动,和esp重合。因为此时main函数还没有用到函数自己的栈,函数栈为空。
eip指向下一条指令int i=4;
i=4,局部变量用到数据栈,把4压栈,esp向下移动,eip指向下一条指令。
同理5也被存到栈中。
调用fun函数
首先执行传参的指令,参数入栈的顺序是从右到左,此时用的还是mian的栈

为fun函数返回值开辟空间,(IA32中是用eax寄存器存储返回值,就不用像这里额外开辟空间存储),接下来把fun函数返回后要继续执行的地址压栈

fun函数开始执行,准备建立fun函数的栈
首先保存当前ebp的地址,ebp指向esp。

1 | int fun(int a,int b){ |
继续执行fun函数,将函数的局部变量压栈:
此时局部变量在fun函数的栈中,参数在main函数(主调函数)的栈中,以ebp作为分界线寻找

函数返回恢复现场
首先需要恢复栈ebp,首先栈顶是ebp要恢复的位置,出栈后移动ebp,esp自动上移
接下来执行RET指令,这条指令将fun函数执行后的返回地址给eip使程序跳转到调用函数后的指令,清栈esp上移
将fun函数返回值传给m,现在栈里面由fun函数传的参数,这些已经没有用了,因此这里就会进行清栈
main函数返回时的清栈工作和上面是一致的
如果两个inline都去掉是什么情况
理论分析
1 | void main(void){ |
首先正常的执行顺序是,进程1执行的时候从进程0的fork的res处返回:
1 | type fork(void) \ |
返回后执行到语句,因为进程1eax设置了0,从而执行init
1 | if (!fork()) { /* we count on this going ok */ |
以上是基于inline的情况
那么如果都不inline了会有什么后果?
这里会出现问题,主要原因是进程1从fork里面出来的时候走的并不是完整的函数返回过程,只是将eip进行了切换,fork的函数返回地址都在进程0的fork返回后从进程0的用户栈中被清理了(进程0的esp向高地址移动,此时进程1的esp没变)。如果是inline,他并不是一真正的函数,而是之间插进去了,没有函数的栈所以不会有问题
我们先考虑一个最简单的过程:进程0执行到pause的时候,发生进程调度,进程切换到进程1.
此时如果两个都不是inline
进程0的用户栈*里面存的内容:
fork函数返回后的地址已经被清除了,因为pause函数已经调用了,所以有pause函数返回后的地址,在pause执行后面就调用int 0x80了,这些东西就放在进程0的内核栈里面了与用户栈无关,所以这里没有。
切换到进程1以后,因为进程1的esp和ebp和进程0调用fork时是完全一样的(指向用户栈),而且进程1是复制了进程0的页表,他们俩指向的是同一个物理空间。(注:首先copy_process 设置进程1的esp和ss,这两个值和进程0的一致,接着调用copy_mem,对进程1 的ldt进行了重新设置,对应的是进程1 的线性地址空间,在copy_page_table中,将进程0和进程1的线性地址空间指向了同一个物理页面,基于上述过程,当调度到进程1 执行后,进程1的ss查询进程1的ldt[2]得到进程1 的用户数据段基址,映射到的物理地址和进程0的用户数据段对应的物理地址一致).此时,进程0,进程1,esp,ebp是一样的,物理地址里面的内容也是一样的,可以看成是在进程1开始发生压栈动作(写操作)之前都和进程0共用了一个物理栈.
1 | int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, |
并且注意到在复制页表的时候:也就是说进程0比较特殊,他仍具有写权限,==所以进程0在创建进程1以后执行pause的时候不会触发copy on write的写时复制==,这一点非常关键。所以进程1从fork return 后使用的用户栈就是进程0那个有pause返回地址的栈。
1 | int copy_page_tables(unsigned long from,unsigned long to,long size) |
因为代码不是inline的所以进程1走到return (type) __res; \
的时候,发生return引发清栈和恢复现场的操作,此时因为fork的已经清了,返回地址就变长了pause的返回地址,进程1出来就不会走init而是卡死在了pause里面,因为此时eax并不是按照需要等于非0,所以程序跑飞,会出现问题。
小附加:有没有可能正好正常
有可能。上面讨论的是进程0规规矩矩的执行到pause以后进程1调度的情况。实际上还有可能存在:
进程0从fork出来且还没有进入pause的时候(为什么不能是执行完copy_process且还没有执行完fork的时候切换:因为在内核态,不允许进程切换)进程0的时间片就没有进程1足了,此时在发生时钟中断,do_timer的时候,就会发生调度,切换到进程1
此时进程0的栈里面已经没有fork了,但是注意清栈的操作是esp向高地址移动,而数据本身并没有从内存中抹去。由于在进程1创建的时候,复制的是进程1有fork的返回值的时候的esp,因此此时的情景是这样的:

所有尽管进程0已经清栈了,但是此时数据还没有被覆盖。此时进程1的用户栈里面还有fork的返回后地址。在进程1fork ret返回时可以回到正确的位置,当进程1使用自己的栈的时候,会发生写时复制,进程1和0的栈彻底分成两个。
为啥有的时候行有的时候不行
因为操作系统调度是根据时间片进行的,而不是指令数。时间比较依赖cpu 硬件或虚拟机类型
代码测试
只去掉pause的inline
理论分析
当去掉pause的inline的时候,当进程0执行到pause的时候切到进程1,执行·fork的返回,此时因为fork是inlie的所以进程1可以正常的执行到if(!fork)的位置,执行init(),此时进程1要为init建立函数栈,要进行写操作,发生copy on write。
代码测试
只去掉fork的inline
理论分析
去掉fork的inline的时候,当进程0执行到pause的时候切到进程1,因为pause是inline的所以进程0虽然执行了pause但是不会把pause的返回地址压入栈中,所以此时用户栈和下图是一致的,进程1执行的时候可以正常跳到if(!fork())继续执行

代码测试
小结关键点
内存:代码区,静态数据区。动态数据区 cpu:eip(指向下一条指令) ebp(栈底) esp(栈顶指针) 函数调用时,参数入栈顺序,从右向左),函数调用的时候首先将传参压入数据栈中。 然后将函数执行完的返回地址压栈,保存main函数的栈底(压栈),ebp挪到fun函数栈底,运行函数程序 函数局部变量压栈。 函数返回,根据栈中内容恢复现场,函数传参已经没用了,进行清栈。 用的是用户栈,是共用一个栈:理由copy page tables 进程0完全复制给进程1了,因此物理内存是一个物理内存,线性地址空间不一样,但是物理空间一样,所以可以看成共用一个栈,当发生写时复制才变两个栈,因为进程0本来是可读写的所以没导致copy on write 所以还是共用了一个栈。 注意:进程0的压栈没有导致写时复制 为啥卡在pause? 因为进程0压栈了fork和pause的返回地址,但是fork返回了就清栈了,所以现在栈里面就是pause所以fork跳出来的时候返回到pause里面去了 有没有可能跑对?有可能 为啥有的时候行有的时候不行,因为操作系统调度是根据时间片进行的,而不是指令数。时间比较依赖cpu 硬件或虚拟机类型 当前进程有tss switch to 里面也有一套tss