函数堆栈
-
esp
栈顶指针
-
ebp
栈基址指针
- call:将ebp压栈,并让ebp=esp
- ret:将esp=ebp,并将原来的ebp出栈,还原ebp
-
eip
下一条要执行代码的地址
- 顺序执行:指向下一条连续的指令
- 跳转分支:根据程序修改eip的值
- call:将当前eip压栈,并让eip指向函数入口
- ret:函数返回,原来的eip出栈,还原eip
-
函数参数
- call:参数入栈
- ret:还原ebp,参数自动出栈
汇编中的一些重要指令
pushl %eax
subl $4, $esp //栈顶指针减4
movl %eas, (%esp) //压栈
popl %eax
movl (%esp), %eax //出栈
addl $4, %esp //栈顶指针加4
call 0x123
pushl %eip //保存下一行代码的地址
movl $0x123, %eip //修改下一行执行代码的地址为函数地址
ret
popl %eip //还原调用函数前的eip,则指向函数返回的下一行代码
enter
push %ebp //压入当前栈基地址
movl $esp, %ebp //变更栈基地址为栈顶地址
leave
movl %ebp, %esp //函数出栈
popl %ebp //还原栈基地址为上一层函数的基地址
整个栈又多块函数栈组成,它们不一定要连续的,可以根据ebp指针,构成一个链表,把多个函数栈关联起来
内嵌汇编
在实现对esp, ebp, eip等寄存器的操作时,c语言无法直接操作,必须通过内嵌汇编来实现
内嵌汇编的格式如下:
__asm__ (
汇编语句模板:
输出部分:
输入部分:
破坏描述部分
);
例子:
unsigned int val1 = 1;
unsigned int val2 = 1;
unsigned int val3 = 1;
asm volatile(
"movl $0, %%eax\n\t"
"addl %1, %%eax\n\t"
"addl %2, %%eax\n\t"
"movl %%eax, %0\n\t"
: "=m"(val3)
: "c"(val1), "d"(val2)
);
/*
eax = 0;
eax += val1;
eax += val2;
val3 = eax;
*/
中断
中断指令会引起用户态切换到内核态
它会保存以下信息:
-
用户态栈顶地址 esp
-
当前状态字
-
当前指令地址 eip
-
把寄存器内容压栈
函数调用
-
压入函数调用参数
-
保存函数返回后的下一行代码的eip
-
修改eip为函数的入口地址
-
ebp压栈,函数调用前的基地址
-
更新栈基地址ebp为栈顶指针esp
-
执行函数代码
-
函数的返回值保存在寄存器eax
-
弹出原来函数的ebp,栈基地址
-
弹出eip,函数返回的下一行代码的地址
-
执行下一行代码
例子如下:
c代码:
int g(int x)
{
return x+3;
}
int main()
{
return g(8) + 1;
}
转为汇编:
g:
pushl $ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $3, %eax
popl %ebp
ret
main:
pushl $ebp
movl %esp, %ebp
pushl $8
call g
addl $1, %eax
popl %ebp
ret
中断--系统调用
kernel-sys-call用户态
系统调用通过中断0x80来实现
通过eax等寄存器来传递参数
例如time系统调用:
time_t tt;
time(tt)
等价于
asm volatile(
"mov $0xd,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
:"=m"(tt)
);
陷入中断前用eax的值代表调用哪个系统调用函数
内核态
ENTRY(system_call)
SAVE_ALL //保存上下文
cmpl $nr_syscalls, %eax
call sys_call_table(,%eax,4) //syscall函数表
movl %eax, PT_EAX(%esp)
syscall_exit //还原上下文
进程切换
进程切换用简单的话说,就是当前进程放弃CPU的执行权,选择一个新的进程,并执行新进程的代码
对于CPU的执行权切换,可以用以下代码实现
asm volatile(
"pushfl\n\t" //保存flags
"pushl %%ebp\n\t" //保存当前进程的栈基地址ebp
"movl %%esp, %[prev_sp]\n\t" //保存旧进程的栈顶地址
"movl %[next_sp], %%esp\n\t" //把新进程的栈顶地址esp设置为当前执行的栈顶地址
"movl $1f, $[prev_ip]\n\t" //设置旧的恢复执行地址eip为1地址
"pushl %[next_ip]" //使用新进程的eip,以便函数返回时,eip弹出堆栈,恢复原来执行的位置
“jmp __switch_to\n”
"1:\t"
"popl $$ebp\n\t" //还原新进程的栈基地址
"popfl\n" //还原flags
);
ref:
网友评论