操作系统实验Ucore:kernel_init(三)

本文首发于我的博客

1.进入Kernel_init()

从bootmain加载完ELF之后,就会跳转到0x100000位置,这里即kernel_init函数所在的位置
在这里插入图片描述
先看第一部分,这里开头就有两个变量 edata 和 end,首先他们的类型是一个指向char数组的指针,其次,由于使用了extern关键字,代表这两个变量在其他位置定义,那么问题就在于到底是在哪里定义的呢。找遍了所有的代码都没有找到哪里有定义这些变量…终于,在链接脚本里边找到了在这里插入图片描述
这里使用了PROVIDE关键字,简而言之,这个关键字可以在链接的时候定义括号里的变量。如果把PROVIDE删掉,那么编译的时候就好报未定义的符号错误。

这里定义的变量的意义为(只看重要的):

1.etext = 代码段的末尾位置 (0x103940)

2.edata = 数据段的起始位置 (0x1100950)

3.end = 段的末尾位置 (0x111dc0)
在这里插入图片描述
现在的kernel的内存为

注:这个内存分布的具体数值不是固定的,只能表示相对位置在这里插入图片描述

2.初始化Console

在控制台输出字符需要向一个特定的内存写入数据,这个地址的范围是0xB8000 - 0xBFFFF

这里其实一共有三种输出的方式,并口,串口和CGA。在调用输出字符函数时同时调用了这三个函数。
在这里插入图片描述
但是经过我的尝试后发现好像只有cga是有用的,其余两个并不能完成输出。

初始化Console之后,就可以输出字符了

这里有一个已经写好的函数cprintf可以用来给我们输出字符串

3.可变参数

cprintf函数比较复杂,但总体上是解析输入的字符串然后挨个字符的输出到屏幕上。这里有一点就是cprintf和printf函数一样是支持可变参数的。要使用可变参数,就需要定义在libs/stdarg下的两个宏
在这里插入图片描述
第一个宏传入最后一个参数,将剩余可变参数的值以va_list的形式放在last中。

第二个宏传入上面获取的va_list,同时传入变量的类型,就可以返回变量的值。每调用一次返回一个值。

我写了一个简单的例子用于理解这个过程
在这里插入图片描述
如果运行这个程序,会输出三个可变参数的值。

如果传入的参数少于预期的参数,会返回一个垃圾值

具体实现的原理跟函数使用栈来传递参数有关。

4.函数调用栈

终于到了第一个编程任务,这个编程任务要求我们打印出函数的调用栈。

首先,在bootasm的时候,我们设置ebp = 0,esp = 7c00

在调用bootmain时,会push ebp
在这里插入图片描述

并令ebp = esp ,ebp = 0x7c00 - 4 就是所有函数调用栈帧的起点。
在这里插入图片描述
这里需要弄清楚一个问题,就是硬件和编译器具体分别做了什么。

函数调用肯定要使用call指令,call指令会分成两步

  • push returnAddress

  • jmp target

所以说call对于堆栈的影响只有push 返回地址(返回地址就是call语句下一条语句的地址)。

至于如何传递参数,如何设置调用栈,都是编译器组织的。

编译器组织的部分,又分为调用者和被调用者两个部分。
在这里插入图片描述
这是一个典型的调用者的操作

首先在调用函数之前,调用者需要

  • 压入所有的参数(这个于具体编译器有关,即函数的调用约定。也不一定要用栈来传递参数,也可以使用寄存器。对于GCC的编译器,至少在现在这个程序中,使用栈来传递参数的,而且入栈的顺序时从右往左)

  • call 被调用者(隐含psuh 返回地址)

在这里插入图片描述
这是对应的被调用者的操作

  • 保存旧的ebp,也就是调用者的ebp。

  • 设置自己的ebp

  • 设置自己的栈空间

在这里插入图片描述
当被调用者返回的时候,需要执行以上两条指令。

leave 指令等同于

  • movl %ebp, %esp

  • popl %ebp

完成了恢复esp和恢复ebp,然后调用ret弹出返回地址。完成返回

至于为什么要这样做,我个人的理解是:

从宏观上看,函数调用前和调用后栈帧应当是没有变化的(除了返回值会保存在自己的栈帧上),而被调用者也需要使用栈,所以就必须保护现场。等到返回时再恢复现场。那么所有的函数的最后都至少要有 pop %ebp 和 ret 指令

我在网上找到了一张助于理解的图
在这里插入图片描述

5.code

理解了上面的问题,代码就很简单了。我们的最初ebp是0,所以我们打印到倒数第二个帧就停下来。

#define ADDR2UINT(addr) (*((uint32_t*)(addr)))
void
print_stackframe(void) {
    uint32_t ebp = read_ebp();
    uint32_t deepth = 0;
    while (ebp != 0)
    {
        cprintf("=================\n");
        cprintf("fp = %d \n", deepth);
        cprintf("ebp = %08x   ra = %08x \n", ebp, ADDR2UINT(ebp+4));
        cprintf("arg1 = %08x  arg2 = %08x   arg3 = %08x   arg4 = %08x \n",\
                ADDR2UINT(ebp+8),ADDR2UINT(ebp+12),ADDR2UINT(ebp+16),ADDR2UINT(ebp+20));
        print_debuginfo(ADDR2UINT(ebp+4));
        ebp = ADDR2UINT(ebp);
        deepth += 1;
    }
}

最终的运行效果就会打印所有的栈帧在这里插入图片描述
这是最后一个栈帧,返回地址是第一个call的函数bootmain

注意:我们没有办法知道函数传入了几个参数,编译器会再编译时根据函数声明的参数来安排如何获得参数,而对于可变参数,就只能靠我们手动获取参数,需要分析栈上的空间分布。

6.总结

这个部分主要讲了函数是如何调用的,想要深入的了解这个部分,必须需要自己动手分析,不能只看资料。

有错误欢迎大家指出~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值