10.进入 main 函数前的最后一跃

10.进入 main 函数前的最后一跃

上回书咱们说到,我们终于把这些杂七杂八的,idt、gdt、页表都设置好了,并且也开启了保护模式,相当于所有苦力活都做好铺垫了,之后我们就要准备进入 main.c!那里是个新世界!

注意不是进入,而是准备进入哦,就差一哆嗦了。

由于上一讲的知识量非常大,所以这一讲将会非常简单,作为进入 main 函数前的衔接,大家放宽心。

这仍然要回到上一讲我们跳转到设置分页代码的那个地方(head.s 里),这里有个骚操作帮我们跳转到 main.c。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
after_page_tables:    
    push 0    
    push 0    
    push 0    
    push L6    
    push _main    
    jmp setup_paging
    ...
setup_paging:    
    ...    
    ret

直接解释起来非常简单。

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 里了!我们先一睹为快一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void main(void) {    
    ROOT_DEV = ORIG_ROOT_DEV;    
    drive_info = DRIVE_INFO;    
    memory_end = (1<<20) + (EXT_MEM_K<<10);    
    memory_end &= 0xfffff000;    
    if (memory_end > 16*1024*1024)        
        memory_end = 16*1024*1024;    
    if (memory_end > 12*1024*1024)         
        buffer_memory_end = 4*1024*1024;    
    else if (memory_end > 6*1024*1024)        
        buffer_memory_end = 2*1024*1024;    
    else        
        buffer_memory_end = 1*1024*1024;    
    main_memory_start = buffer_memory_end;    
    mem_init(main_memory_start,memory_end);    
    trap_init();    
    blk_dev_init();    
    chr_dev_init();    
    tty_init();    
    time_init();    
    sched_init();    
    buffer_init(buffer_memory_end);    
    hd_init();    
    floppy_init();    
    sti();    
    move_to_user_mode();    
    if (!fork()) {        
        init();    
    }    
    for(;;) pause();
}

没错,这就是这个 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 函数开始执行。