抛开JOS不谈,一个函数在调用时,肯定要压入参数给函数体传值,然后要压入函数结束后的下一条指令的地址,以便函数可以正确的返回,其次因为公用一个堆栈所以要压入BP也就是基址寄存器的值,和在函数体中使用到的寄存器的值,以便返回时可以恢复现场。但是这些值压入的顺序和规则目前还是不知道的,需要一些额外的资料。
esp的含义是“这个地址以下的空间是未被使用的堆栈控件”,
ebp的含义是“这个地址以下至esp的空间是属于目前所执行函数的堆栈空间”,所以图中saved%ebp和 ret%eip就是属于调用此函数的函数的ebp和eip。
通过阅读汇编代码我们可以发现,一个函数在调用之前,其调用者会将参数压栈(顺序没深究,和编译器有关),也就是压入arg2 和arg1,
然后调用call,call的动作会把ret%eip压栈,同时转到函数体执行,在函数体执行的开头有一段预处理代码,也就是图中的prologue,会将ebp寄存器(call指令不改变ebp的值,此时的ebp还是上一个函数的)内容压栈,
然后将当前esp赋值给ebp,随后进行现场保存的工作,存储在local variables空间里,值得注意的是,在预处理时会一下申请足够的空间,包括保存现场所需空间,局部变量所需空间(这大概也就是标准C的变量声明需要放在函数开头的原因吧,为了方便编译器),调用其它函数所压入变量的空间,换句话说图中arg1,arg2是属于上一个函数的local variables空间,这也就是backtrace不能准确的判断出函数所传参数个数而统一要求打印出5个参数的原因。
int mon_backtrace(int argc,char** argv,struct Trapframe*tf)
{
uint32_t *ebp,*eip;
uint32_t arg0,arg1,arg2,arg3,arg4;
ebp=(uint32_t*)read_ebp();
eip=(uint32_t*)ebp[1];
arg0=ebp[2];
arg1=ebp[3];
arg2=ebp[4];
arg3=ebp[5];
arg4=ebp[6];
cprintf("Stackbacktrace:\n");
while(ebp!=0){
cprintf("ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",ebp,eip ,arg0,arg1,arg2,arg3,arg4);
ebp=(uint32_t*)ebp[0];
eip=(uint32_t*)ebp[1];
arg0=ebp[2];
arg1=ebp[3];
arg2=ebp[4];
arg3=ebp[5];
arg4=ebp[6];
}
return 0;
}
int mon_backtrace(int argc,char** argv,struct Trapframe*tf)
{
uint32_t *ebp;
ebp=(uint32_t*)read_ebp();
cprintf("Stackbacktrace:\n");
while(ebp!=0x0){
cprintf("ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",ebp,ebp[1],ebp[2],ebp[3],ebp[4],ebp[5],ebp[6]);
ebp=(uint32_t*)ebp[0];
struct Eipdebuginfo info;
debuginfo_eip(ebp[1],&info);
cprintf("%s,%d,%.*s+%d\n",info.eip_file,
info.eip_line,
info.eip_fn_namelen,
info.eip_fn_name,
ebp[1]-info.eip_fn_addr );
ebp=(uint32_t*)ebp[0];
}
return 0;
}