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 函数开始执行。

updatedupdated2024-05-102024-05-10