10.进入 main 函数前的最后一跃
上回书咱们说到,我们终于把这些杂七杂八的,idt、gdt、页表都设置好了,并且也开启了保护模式,相当于所有苦力活都做好铺垫了,之后我们就要准备进入 main.c!那里是个新世界!
注意不是进入,而是准备进入哦,就差一哆嗦了。
由于上一讲的知识量非常大,所以这一讲将会非常简单,作为进入 main 函数前的衔接,大家放宽心。
这仍然要回到上一讲我们跳转到设置分页代码的那个地方(head.s 里),这里有个骚操作帮我们跳转到 main.c。
|
|
直接解释起来非常简单。
push 指令就是压栈,五个 push 指令过去后,栈会变成这个样子。
然后注意,setup_paging
最后一个指令是 ret
,也就是我们上一回讲的设置分页的代码的最后一个指令,形象地说它叫返回指令,但 CPU 可没有那么聪明,它并不知道该返回到哪里执行,只是很机械地把栈顶的元素值当做返回地址,跳转去那里执行。
再具体说是,把 esp
寄存器(栈顶地址)所指向的内存处的值,赋值给 eip 寄存器,而 cs:eip 就是 CPU 要执行的下一条指令的地址。而此时栈顶刚好是 main.c 里写的 main 函数的内存地址,是我们刚刚特意压入栈的,所以 CPU 就理所应当跳过来了。
当然 Intel CPU 是设计了 call 和 ret 这一配对儿的指令,意为调用函数和返回,具体可以看后面本回扩展资料里的内容。
至于其他压入栈的 L6 是用作当 main 函数返回时的跳转地址,但由于在操作系统层面的设计上,main 是绝对不会返回的,所以也就没用了。而其他的三个压栈的 0,本意是作为 main 函数的参数,但实际上似乎也没有用到,所以也不必关心。
总之,经过这一个小小的骚操作,程序终于跳转到 main.c 这个由 c 语言写就的主函数 main 里了!我们先一睹为快一下。
|
|
没错,这就是这个 main 函数的全部了。
而整个操作系统也会最终停留在最后一行死循环中,永不返回,直到关机。
好了,至此,整个第一部分就圆满结束了,为了跳进 main 函数的准备工作,我称之为进入内核前的苦力活,就完成了!我们看看我们做了什么。
我把这些称为进入内核前的苦力活,经过这样的流程,内存被搞成了这个样子。
之后,main 方法就开始执行了,靠着我们辛辛苦苦建立起来的内存布局,向崭新的未来前进!
欲知后事如何,且听下回分解。
------- 本回扩展资料 -------
关于 ret 指令,其实 Intel CPU 是配合 call 设计的,有关 call 和 ret 指令,即调用和返回指令,可以参考 Intel 手册:
Intel 1 Chapter 6.4 CALLING PROCEDURES USING CALL AND RET
可以看到还分为不改变段基址的 near call 和 near ret
以及改变段基址的 far call 和 far ret
压栈和出栈的具体过程,上面文字写的清清楚楚,下面 Intel 手册还非常友好地放了张图。
可以看到,我们本文就是左边的那一套,把 main 函数地址值当做 Calling EIP 压入栈,仿佛是执行了 call 指令调用了一个函数一样,但实际上这是我们通过骚操作代码伪造的假象,骗了 CPU。
然后 ret 的时候就把栈顶的那个 Calling EIP 也就是 main 函数地址弹出栈,存入 EIP 寄存器,这样 CPU 就相当于“返回”到了 main 函数开始执行。