操作系统20----函数调用堆栈实现源码分析

由bootasm.S中汇编代码实现处理器初始化工作,为内核加载做好准备,其中最后比较重要的部分就是给段寄存器赋初值,设立堆栈指针,调用bootmain方法,开始内核加载。

# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
    movl $0x0, %ebp
    movl $start, %esp
    call bootmain

函数调用堆栈

一个函数调用动作可分解为: 零到多个PUSH指令( 用于参数入栈) , 一个CALL指令。 CALL指令内部其实暗含了一个将返回地址( 即CALL指令下一条指令的地址) 压栈的动作( 由硬件完成) 。 几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

pushl %ebp
movl %esp , %ebp

这样在程序执行到一个函数的实际指令前, 已经有以下数据顺序入栈: 参数、 返回地址、 ebp寄存器。

这两条汇编指令的含义是: 首先将ebp寄存器入栈, 然后将栈顶指针esp赋值给ebp。 “mov ebp esp”这条指令表面上看是用esp覆盖ebp原来的值, 其实不然。 因为给ebp赋值之前, 原ebp值已经被压栈( 位于栈顶) , 而新的ebp又恰恰指向栈顶。 此时ebp寄存器就已经处于一个非常重要的地位, 该寄存器中存储着栈中的一个地址( 原ebp入栈后的栈顶) , 从该地址为基准, 向上( 栈底方向) 能获取返回地址、 参数值, 向下( 栈顶方向) 能获取函数局部变量值, 而该地址处又存储着上一层函数调用时的ebp值。

由于ebp中的地址处总是“上一层函数调用时的ebp值”, 而在每一层函数调用中, 都能通过当时的ebp值“向上( 栈底方向) ”能获取返回地址、 参数值, “向下( 栈顶方向) ”能获取函数局部变量值。 如此形成递归, 直至到达栈底。 这就是函数调用栈。
参数和函数返回值可以通过寄存器或者内存中的栈来保存。

在函数体内来调用foo函数,汇编代码如下

首先将当前函数使用的寄存器和参数变量入栈

   

调用call foo,首先将返回地址即当前指令的下一条指令入栈,然后跳转到foo标号处开始执行,此时ebp指向调用者的栈基指针位置,将ebp入栈,ebp指向esp位置处。此处栈属于向上增长,即入栈操作,相当于esp减小,esp为栈顶指针,push  ebp时首先将esp减4,然后将ebp入栈,mov esp  ebp,此时ebp会指向返回地址上一个位置处。继续执行foo函数,esp上移,参数不断入栈。

  

函数堆栈实现分析

由bootasm.S实现处理器基本设置最后调用bootmain,bootmain实现加载内核,最终跳转到操作系统入口处kern_init

void
kern_init(void){
    extern char edata[], end[];
    memset(edata, 0, end - edata);

    cons_init();                // init the console

    const char *message = "(THU.CST) os is loading ...";
    cprintf("%s\n\n", message);

    print_kerninfo();

    grade_backtrace();

    pmm_init();                 // init physical memory management

    pic_init();                 // init interrupt controller
    idt_init();                 // init interrupt descriptor table

    clock_init();               // init clock interrupt
    intr_enable();              // enable irq interrupt

    //LAB1: CAHLLENGE 1 If you try to do it, uncomment lab1_switch_test()
    // user/kernel mode switch test
    lab1_switch_test();

    /* do nothing */
    while (1);
}

其中grade_backtrace实现函数调用堆栈的效果测试:

调用关系如下

grade_backtrace-->grade_backtrace0-->grade_backtrace1-->grade_backtrace2-->mon_backtrace-->print_stackframe

void __attribute__((noinline))
grade_backtrace2(int arg0, int arg1, int arg2, int arg3) {
    mon_backtrace(0, NULL, NULL);
}

void __attribute__((noinline))
grade_backtrace1(int arg0, int arg1) {
    grade_backtrace2(arg0, (int)&arg0, arg1, (int)&arg1);
}

void __attribute__((noinline))
grade_backtrace0(int arg0, int arg1, int arg2) {
    grade_backtrace1(arg0, arg2);
}

void
grade_backtrace(void) {
    grade_backtrace0(0, (int)kern_init, 0xffff0000);
}

int
mon_backtrace(int argc, char **argv, struct trapframe *tf) {
    print_stackframe();
    return 0;
}

void
print_stackframe(void) {
     /* LAB1 YOUR CODE : STEP 1 */
     /* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
      * (2) call read_eip() to get the value of eip. the type is (uint32_t);
      * (3) from 0 .. STACKFRAME_DEPTH
      *    (3.1) printf value of ebp, eip
      *    (3.2) (uint32_t)calling arguments [0..4] = the contents in address (uint32_t)ebp +2 [0..4]
      *    (3.3) cprintf("\n");
      *    (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
      *    (3.5) popup a calling stackframe
      *           NOTICE: the calling funciton's return addr eip  = ss:[ebp+4]
      *                   the calling funciton's ebp = ss:[ebp]
      */
    uint32_t ebp = read_ebp(), eip = read_eip();

    int i, j;
    for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
        uint32_t *args = (uint32_t *)ebp + 2;
        for (j = 0; j < 4; j ++) {
            cprintf("0x%08x ", args[j]);
        }
        cprintf("\n");
        print_debuginfo(eip - 1);
        eip = ((uint32_t *)ebp)[1];
        ebp = ((uint32_t *)ebp)[0];
    }
}

对于print_stackframe实现具体的函数堆栈打印;

首先获取当前ebp,eip寄存器值;

依据最大打印栈深度STACKFRAME_DEPTH以及ebp值来循环打印每一层函数调用:

      打印每一层ebp,eip值;

      打印四个参数值;

      打印函数名以及所在行号;

      更新上一层ebp,eip值;

获取当前ebp,eip值,使用内联汇编实现

static __noinline uint32_t
read_eip(void) {
    uint32_t eip;
    asm volatile("movl 4(%%ebp), %0" : "=r" (eip));
    return eip;
}
static inline uint32_t
read_ebp(void) {
    uint32_t ebp;
    asm volatile ("movl %%ebp, %0" : "=r" (ebp));
    return ebp;
}

每一层函数参数的打印,这里使用的地址位数为32位,如上所示,因此将ebp+2即指向参数1位置处

        //将ebp转化为32位整形变量,此时+2相当于指针上移两个内存单元所指位置处
        uint32_t *args = (uint32_t *)ebp + 2;
        for (j = 0; j < 4; j ++) {
            cprintf("0x%08x ", args[j]);
        }
        cprintf("\n");

打印函数名以及行号print_debuginfo(eip - 1)留待以后分析;

每打印一层函数之后,更新eip,ebp值使其指向调用函数,调用者eip相当于指针ebp+4所指向的内存单元中的值,而调用者的ebp即为ebp所指向的内存单元的值

 the calling funciton's return addr eip  = ss:[ebp+4]
 the calling funciton's ebp = ss:[ebp]

        //将ebp作为数组的首地址,eip  = ss:[ebp+4]
		//eip为函数调用者的返回地址
        eip = ((uint32_t *)ebp)[1];
		//ebp指向ebp指针指向的内存单元内容,即调用者的ebp指针地址
        ebp = ((uint32_t *)ebp)[0];

最终显示效果如下:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值