本文首发于我的博客
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.总结
这个部分主要讲了函数是如何调用的,想要深入的了解这个部分,必须需要自己动手分析,不能只看资料。
有错误欢迎大家指出~