以前通过C语言的学习大概了解了一些函数调用的过程。即在进程的栈中存在栈指针和基指针两种指针,一直指向栈顶,而基指针则指向当前函数的返回值。通过改变栈指针和基指针来实现函数的调用和返回。自从接触汇编后才对这块儿有了更深的理解。
在x86平台的汇编中栈涉及到三个寄存器:
SS(段寄存器)
SP(栈指针)
BP(基指针)
SS寄存器一直指向栈段地址,SP指向栈段偏移地址,因为汇编中地址的表示方式是"XXXX:XXXX"(不过居然不存在栈底这个概念)。
当一个函数在执行的过程中需要调用另一个函数时则会使用call指令,call指令执行时会先将IP寄存器(代码段偏移地址)的值push入栈中然后再调用jmp指令进行跳转,跳转到调用函数的地址,然后开始执行调用的函数的代码。
在执行完成后再使用ret指令进行返回,ret指令会将栈顶元素pop到IP寄存器中从而继续执行之前函数的代码。
那么举个例子,先上C语言测试代码:
int cz(int a, int b) {
return a + b;
}
int main() {
int c = cz(7, 8);
return 0;
}
内容很简单,先定义一个cz函数,将两个变量a和b相加并返回。之后在main函数里进行调用。编译成可执行文件后再用反汇编工具看到汇编代码如下:
; ================ B E G I N N I N G O F P R O C E D U R E ================
; Variables:
; var_4: int32_t, -4
; var_8: int32_t, -8
_cz:;cz函数入口
0000000100000f60 push rbp ;将BP指针推入栈,BP此时的内容是cz函数的返回栈地址
0000000100000f61 mov rbp, rsp
0000000100000f64 mov dword [rbp+var_4], edi;下面几步执行a和b的相加
0000000100000f67 mov dword [rbp+var_8], esi
0000000100000f6a mov esi, dword [rbp+var_4];由于栈是向下生长,所以地址要做减法
0000000100000f6d add esi, dword [rbp+var_8]
0000000100000f70 mov eax, esi
0000000100000f72 pop rbp
0000000100000f73 ret;函数返回并更新IP指针让main函数继续执行
; endp
0000000100000f74 align 128
; ================ B E G I N N I N G O F P R O C E D U R E ================
; Variables:
; var_4: int32_t, -4
; var_8: int32_t, -8
_main:;mian函数入口
0000000100000f80 push rbp
0000000100000f81 mov rbp, rsp
0000000100000f84 sub rsp, 0x10
0000000100000f88 mov edi, 0x7 ; 参数a
0000000100000f8d mov esi, 0x8 ; 参数b
0000000100000f92 mov dword [rbp+var_4], 0x0
0000000100000f99 call _cz ;执行cz函数
0000000100000f9e xor esi, esi
0000000100000fa0 mov dword [rbp+var_8], eax
0000000100000fa3 mov eax, esi
0000000100000fa5 add rsp, 0x10
0000000100000fa9 pop rbp
0000000100000faa ret
; endp
0000000100000fab db 0x90 ; '.'
关键的地方加了注释,由于我的电脑是64位机,所以寄存器前面加了“r”表示取64位的值。
PS:当时在索尼现场搞bug的时候经常会看到的PC指针应该就是高通平台的代码段片偏移地址,当时看着底层的各种crash栈信息真是一脸懵逼。
网友评论