研究X86汇编中的栈段初始化问题时候得到的一些经验

1.前情提要

学了一段时间汇编,发现李忠老师的《X86汇编语言:从实模式到保护模式》十分不错,读下来的过程中能有一些自己的理解。
栈段一直是个困扰我的问题,主要原因就是栈段不同于代码段和数据段,栈段的扩展方式是向下扩展的(向低地址扩展),所以栈段的段界限是其下限,而上限是无限制的(其实是有的,一般就是可用的最大的内存地址)。看过一个论坛里边,对向上同向下扩展的段的偏移地址的描述十分到位:“当段最大为1M时,在自然的向高扩展段内,从0~limit的偏移是合法有效的偏移,而从limit+1~1M-1的偏移是非法无效的偏移;在向低扩展段中,情形刚好相反,从0~limit的偏移是非法无效的偏移,而从limit+1~1M-1的偏移是合法有效的偏移。”。 以32位X86为例,段最大为4GB,考虑到段描述符中的D/B位的影响:(1) 当B=0时,那么sp的上部边界(也就是sp寄存器的最大值)为0xFFFF,个人认为在要兼容16位处理器程序时或者需要开辟的栈比较小的情况下才需要将此位置零;(2) 当B=1时,则esp的上限为(4GB-1),即最大的合法偏移地址为0xFFFF FFFF(注:上下限均表示偏移地址的范围)。
Alt [图1-存储器的段描述符格式]

对于向下扩展的段,实际使用的段界限和粒度G有关,如果G=0,最低可用的偏移地址为(描述符中的段界限+1),实际使用的段界限就是描述符中记载的段界限;如果G=1,最低可用的偏移地址为:(描述符中的段界限+1)*0x1000,实际使用的段界限为:(描述符中的段界限+1)*0x1000-1。
下面用32位的X86架构的保护模式工作方式为例,看看一个栈段会被初始化成怎样。
假设一个栈段基址为0x7C00,段界限为0xFFFFE,粒度G=1,D/B位B=1,权限为0的内核栈段,可得它的段描述符:
0x00CF 9600 ;高32位
0x7C00 FFFE ;低32位
而它实际使用的段界限为:(描述符中的段界限+1)*0x1000-1,即0xFFFF EFFF,所以该栈的合法偏移地址的范围为:0xFFFF F000-0xFFFF FFFF,根据其基址可以得出其合法的线性地址为:0x0000 6C00-0x0000 7BFF。
而初始化偏移地址方面,不论栈是在代码中显示定义的,如下:

 [bits 32]
SECTION stack align=16 vstart=0
	resb 4096
stack_end: 

亦或是栈段的内存是由程序随机动态分配的,没有了vstart=0子句的情况,李忠老师在程序中都是将偏移esp初始化为0。开始我一直不太理解,为什么都是0?
按我原先的理解,后者,即没有了vstart=0子句的栈段的合法偏移值应该为0xFFFF F000-0xFFFF FFFF,所以栈段的结构为图2这样,图中标的为esp的所有可能的值,箭头为esp初始值0xFFFFF000:
Alt
[图2-错误的初始化方式]

我原先的想法是,上图中,例如当需要push一个双字时,esp会减4先,此时在栈段自身内部发生内存回卷,esp的值变为0xFFFF FFFC,到0xFFFF FFFF刚好可以容纳一个双字。
这个问题困扰了我一段时间,直到有一天闲来无事的时候思考,终于想通了。按作者的方法将栈段初始偏移esp初始化为0才是正确的。此时栈的内存结构如图3所示,箭头为栈底的下一个地址0x7C00,在栈的外边,也是ss:esp的初始值:图3
[图3-正确的初始化方式]

上图中,例如当需要push一个双字时,esp会减4先,esp的值变为0xFFFF FFFC,到0xFFFF FFFF刚好可以容纳一个双字。

2.正确的初始化方式

当然,栈的这种变化方法只是我的猜测,具体怎样,还要用bochs对程序调试验证一下。
如图4,在内核程序中已为此用户程序动态分配了一个栈,基址为0x00101800,limit为0xFFFF EFFF,可知该栈大小为4KB:图4
[图4-段寄存器]

然后初始化esp为0:图5
[图5-初始化esp为0]

然后查看通用寄存器,看看esp初值:

图6
[图6-通用寄存器]

看看此时若发生过程调用,栈指针esp和栈内数据会如何变化,见图7:图7
[图7-做完截图才发现图中的描述有个小小偏差,“执行完”应改为“执行时”更适合,eip始终指向正在执行指令的下一条指令,图中在执行callf时,eip已为0x23,所以push eip的操作push的是0x0000 0023]

由上图所示,执行callf的时候先往栈中push cs,再push eip,随后被调用的过程中保留会用到的寄存器。且需要注意的是,绝大多数情况下,在主程序中esp都为0,栈为空,只有在过程调用中会出现esp不为0的情况,不是过程调用的时候,都不建议去动用栈的空间。若实在要用到,记得要保持栈平衡。

3.错误的初始化方式和BUG

接下来试一下将esp按我原先的想法初始化为0xFFFF F000,如图8:图8
[图8-将esp初始化为0xFFFF F000]

执行初始化过后再查看此时的esp值,可见此时栈内已有4KB的数据,不能再容纳任何值了:

图9
[图9-esp初值,此时4KB的栈已满]

按我原先的理解,栈满之后会发生内存回卷,回到栈底继续使用栈,但我错了,内存回卷不适合栈已满的这种情况,栈满再push就会发生溢出,这体现在上层应用中一般都是比较严重的bug,比如段错误导致程序崩溃。
图10
[图10-栈已满情况下调用过程,出现非预期的结果]

图11
[图11-cs:eip并未入用户程序的栈]

执行callf,栈溢出之后程序转到0xF000:0xFFF0处开始执行(见图12),即物理地址0xFFFF0(对于16位机器、寻址空间只有1MB的机器来讲)、??或许是0xFFFF FFF0(对于32位机器,寻址空间有4GB的机器来讲)??。(见图12),这个是BIOS的入口地址,在我看来这就是拥有内核权限的栈发生了溢出之后,计算机崩溃了,若要继续,只可以由最初的指令开始。并且此时栈的基址、偏移、大小均为0。
图12
[图12-CPU最开始执行的指令地址]

见上图,在bochs模拟器最开始运行时第一条指令就是位于0xF000:0xFFF0,可见这个就是计算机运作的开始,分别查看0xFFFF0和0xFFFF FFF0处的内存单元的数据,最开始的九个字节刚好都是jmpf 0xF000:0xE05B的机器码(高字节在高地址,低字节在低地址)。

4.总结

这次纠结于栈指针 esp 的初始化问题,并且在思考验证得出答案的过程中收获颇丰。初始时应将栈置空,即栈指针指向栈底的下一个地址。若应用程序的栈溢出,只是该应用程序会崩溃,若是操作系统内核层面的栈溢出,整个计算机系统就崩溃了。所以在使用一些硬件或者系统资源比较少的硬件平台时,过程调用中尽量避免太多的嵌套。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值