抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

庖丁解牛linux内核

堆栈寄存器

  • esp, 堆栈指针
  • ebp,基址指针

利用堆栈实现函数的调用和返回

  • cs:eip总是指向下一条指令的地址,在跳转时cs:eip的值会根据程序需要被修改
  • call将当前cs:eip的值压入栈顶,cs:eip指向被调用函数的入口
  • ret从栈顶弹出原来保存在这里的cs:eip的值,放入cs:eip中

函数调用框架

  • 调用者:call func
    • call把call func指令的下一条指令保存在栈顶,eip指向函数开始处
  • 被调用者:储存在某块地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 建立被调用者函数的堆栈框架
    push %ebp
    mov %esp,%ebp

    函数体

    // 拆除被调用者函数的堆栈框架
    movl %ebp,%esp
    pop %ebp

    ret
    • call被调用前程序可能就有使用堆栈,esp到ebp之间的内容被占用
    • call执行之后
      • 当前cs:eip入栈,然后cs:eip指向函数开始,然后向下执行
      • pushmov把ebp(原栈底)入栈,然后ebp指向esp,即开辟一段新的栈,供函数使用
      • 函数调用结束后movpop恢复原来的栈顶和栈底
      • ret把栈顶元素(cs:eip)出栈,赋给cs:eip,即恢复原cs:eip

参数传入

如调用一个两个参数的函数func(x, y);

1
2
3
4
5
6
7
8
pushl 0xfffffff8(%ebp)
pushl 0xfffffff4(%ebp)
// 先通过栈底+偏移的方式把局部变量压栈,方便在函数框架建立后使用
// 然后再call func建立函数框架

call funn
add $0x8,%esp
// 由于压入了两个参数,所以通过这样的方式来清理掉

一个函数也会有自己的局部变量,所以函数框架创建后,编译器会扫描整个函数,然后计算需要的空间,位局部变量预留空间。以main函数为例

1
2
3
4
5
6
7
8
9
10
int main(){
push %ebp
mov %esp,%ebp
sub 立即数,%esp // 为声明的变量预留空间
...

movl %ebp,%esp
pop %ebp
ret
}

中断

中断信号发生时,cpu把当前的eip、ebp等都压到 内核堆栈 保存现场。然后把eip指向中断处理程序入口,执行中断处理程序。

嵌入汇编语法

内嵌汇编语法

1
2
3
4
5
__asm__(
汇编语句模板:
输出部分:
输入部分:
破坏描述部分:); // 表示这代码可能会破坏哪个寄存器的内容等

看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>

int main(){
// val1+val2=val3
unsigned int val1 = 1;
unsigned int val2 = 2;
unsigned int val3 = 3;

asm volatile(
"movl$0,%%eax\n\t" // 把eax清0,%%转意
"addl%1,%%eax\n\t" // %eax+=val1
// %1,%2等指下面输出和输入部分从0开始编号
// 所以%1指val1
"addl%2,%%eax\n\t" // %eax+=val2
"movl%%eax,%0\n\t" // val2 = %eax
:"=m"(val3) // =m指写到内存变量
:"c"(val1),"d"(val2)// c,d做修饰,指%eax,%edx,用ecx存val1,edx存val2
);
printf("val1:%d+val2:%d=val3:%d\n", val1, val2, val3);

return 0;
}

内嵌汇编常用修饰符(限定符)

限定符 描述
寄存器 “a” 将输入变量放入eax
“b” 将输入变量放入ebx
“c” 将输入变量放入ecx
“d” 将输入变量放入edx
“s” 将输入变量放入esi
“D” 将输入变量放入edi
“q” 将输入变量放入eax、ebx、ecx、edx中的一个
“r” 将输入变量放入通用寄存器中的一个
——- “A” 放入eax和edx,把eax和edx,合成一个64位的寄存器
内存 “m” 内存变量
“o” 操作数为内存变量,但其寻址方式是偏移量类型
“V” 操作数为内存变量,但其寻址方式不是偏移量类型
——- “.” 操作数为内存变量,但其寻址方式为自增量
寄存器或内存 “g” 将输入放入eax、ebx、ecx、edx中的一个或者作为内存变量
——- “X” 操作数可以是任何类型
立即数 “I” 0-31之间的立即数(用于32位移位指令)
“J” 0-63之间的立即数(用于64位移位指令)
“N” 0-255之间的立即数(用于out指令)
“i” 立即数
——- “n” 立即数,有些系统不支持除字以外的立即数,这些系统应该使用”n”
操作数类型 “=” 操作数在指令中是只写的(输出操作数)
——- “+” 操作数在指令中是读写的(输入输出操作数)
浮点数 “f” 浮点数
“t” 第一个浮点数寄存器
“u” 第二个浮点数寄存器
“G” 标准的80387
% 该操作数可以和下一个操作数交换位置
# 部分注释,注释从该字符到气候的逗号之间的内容
* 表述如果选中寄存器,则其后的字母被忽略

用户态、内核态和中断处理过程

CPU一般都有几种不同的指令执行级别。在高级级别下,代码可以执行 特权指令,访问访问任意物理地址 ,这种CPU执行级别对应内核态,类似的用户态。

linux中如何区分用户态和内核态:一般来说地址空间是一个显著特征(cs:eip),0xc0000000以上的地址空间只有在内核态下可以访问,0x00000000-0xbfffffff的地址空间两种状态都可以访问。

用户态可以传递系统调用号,系统调用根据系统调用号跳到内核态进行相应的处理。

评论