基于MIT6.828 分析 linux 从用户态到内核态用户栈和内核栈切换过程

基于MIT6.828课程的Lab3,我们来分析一下程序从用户态到内核态中用户栈内核栈切换的过程。

你需要具备:

  1. MIT6.828 课程的基本了解,环境搭建和代码运行
  2. 汇编基本知识
  3. gdb 基本调试

首先函数调用栈是这样的:

i386_init --> env_run --> env_pop_tf,env_po_tf 中 iret 模拟中断返回进入用户态,执行 hello.c 代码,

umain --> lib/cprintf --> vcprintf --> lib/systemcall/sys_cputs --> syscall,systemcall 中使用 int 0x30 陷入内核态,注意这里系统调用号使用的是 0x30 而不是linux中的 0x80,这里对内存管理,程序加载不做分析。

查看 hello.asm 反汇编代码,找到中断最后一条指令位置 0x800aae , 并记住下一条指令的位置 0x800ab0,等会会用到这个值,使用gdb打断点

(gdb) b *0x800aae 然后 (gdb) c 继续运行到断点位置。

void
sys_cputs(const char *s, size_t len)
{
  800a97:   55                      push   %ebp
  800a98:   89 e5                   mov    %esp,%ebp
  800a9a:   57                      push   %edi
  800a9b:   56                      push   %esi
  800a9c:   53                      push   %ebx
    //
    // The last clause tells the assembler that this can
    // potentially change the condition codes and arbitrary
    // memory locations.

    asm volatile("int %1\n"
  800a9d:   b8 00 00 00 00          mov    $0x0,%eax
  800aa2:   8b 4d 0c                mov    0xc(%ebp),%ecx
  800aa5:   8b 55 08                mov    0x8(%ebp),%edx
  800aa8:   89 c3                   mov    %eax,%ebx
  800aaa:   89 c7                   mov    %eax,%edi
  800aac:   89 c6                   mov    %eax,%esi
  800aae:   cd 30                   int    $0x30

void
sys_cputs(const char *s, size_t len)
{
    syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
}
  800ab0:   5b                      pop    %ebx
  800ab1:   5e                      pop    %esi
  800ab2:   5f                      pop    %edi
  800ab3:   5d                      pop    %ebp
  800ab4:   c3                      ret    

00800ab5 <sys_cgetc>:

此时使用 (gdb) i r 查看各寄存器的值

eax            0x0  0
ecx            0xd  13                                                  <-- 此时保存的是 “hello,world\n” 字符串长度
edx            0xeebfde88   -289415544                      <-- 此时保存的是 “hello,world\n” 字符串
ebx            0x0  0
esp            0xeebfde54   0xeebfde54            <-- 用户栈
ebp            0xeebfde60   0xeebfde60                    <-- 用户栈
esi            0x0  0
edi            0x0  0
eip            0x800aae 0x800aae                 <-- eip 在用户代码
eflags         0x92 [ AF SF ]
cs             0x1b 27                                                   <--- 用户态 代码段
ss             0x23 35                                                   <--- 用户态 数据段
ds             0x23 35                                                   <--- 用户态 数据段
es             0x23 35
fs             0x23 35
gs             0x23 35

系统调用最多能够传递5个参数,分别是 ecx,edx,ebx,edi,esi,因为函数调用参数是从右到左压栈的,所以 sys_cputs 中 0x8(%ebp) 保存的是 “hello,world\n” 字符串,0xc(%ebp) 保存了字符串的长度 13,通过 gdb 可以验证

(gdb) p (char *)0xeebfde88 $1 = 0xeebfde88 "hello, world\n"

使用 (gdb) si 继续运行 int 0x30 指令,进入内核态。再使用 (gdb) i r 查看各寄存器的值,然后我们一个一个分析各寄存器变化情况。

=> 0xf01036aa <t_syscall+2>:    push   $0x30
0xf01036aa in t_syscall () at kern/trapentry.S:68
68  TRAPHANDLER_NOEC(t_syscall, T_SYSCALL)

eax            0x0  0
ecx            0x1a 26                                                              
edx            0xeebfde88   -289415544
ebx            0x0  0
esp            0xefffffe8   0xefffffe8                  <——-    内核栈
ebp            0xeebfde60   0xeebfde60
esi            0x0  0
edi            0x0  0
eip            0xf01036aa   0xf01036aa <t_syscall+2>        <———    跳转到内核代码
eflags         0x92 [ AF SF ]
cs             0x8  8                                                           <———  内核态数据段    
ss             0x10 16                                                      <———  内核态代码段
ds             0x23 35
es             0x23 35
fs             0x23 35
gs             0x23 35

发现此时栈的地址已经变成内核栈的地址了,可通过 memlayout.h 定义的内存分布查看内核栈使用地址 KSTACKTOP (0xeffffffc) ,栈的地址是向下增长的, (gdb) x/10x 0xefffffe8 查看内核栈的数据

(gdb) x/10x 0xefffffe8
0xefffffe8: 0x00000000  0x00800ab0  0x0000001b  0x00000092
0xeffffff8: 0xeebfde54  0x00000023  0xf000ff53  0xf000ff53
0xf0000008: 0xf000e2c3  0xf000ff53

0xeffffffc 为内核栈的栈底,进入内核后,cpu自动将用户态的ss (0x23) 和用户栈esp (0xeebfde54) 压入内核栈,

再将 eflags (0x92),cs (0x1b) ,eip 的下一条指令(0x800ab0) 入栈(在上文中提到过这个地址)才能实现从内核态返回到用户态会从这个地址继续执行,最后把错误码入栈,系统调用没有错误码,所以入栈 0x0。到此从用户态到内核态的过程已经完成。

接下来分析一下进入内核态后如何保存各寄存器值的。

进入内核后会跳转到 trapentry.S 文件去执行,中断向量的设置这里不做讨论,查看 trapentry.S 关键代码,之后栈的操作都是内核栈

TRAPHANDLER_NOEC(t_syscall, T_SYSCALL)                                          <--- 进入后执行这句代码

#define TRAPHANDLER(name, num)                      \                                           
    .globl name;        /* define global symbol for 'name' */   \
    .type name, @function;  /* symbol type is function */       \
    .align 2;       /* align function definition */     \
    name:           /* function starts here */      \
    pushl $(num);                           \                                                                   <--- 1. 将调用号 0x30 压入内核栈
    jmp _alltraps
    
    
/*
 * Lab 3: Your code here for _alltraps
 */
 // tf_ss,tf_esp,tf_eflags,tf_cs,tf_eip,tf_err 在中断发生时由处理器压入,所以现在只需要压入剩下寄存器(%ds,%es,通用寄存器)
_alltraps:
    pushl %ds                                   
    pushl %es
    pushal                                  

    movl $GD_KD, %eax                       <-- 修改内核数据段
    movw %ax, %ds
    movw %ax, %es

    push %esp           // 压入trap()的参数tf,%esp 指向 Trapframe 结构的起始地址
    call trap           // 去统一处理

首先调用c函数,参数入栈所以压入 %esp ,为什么 %esp 指向 Trapframe 结构的起始地址呢。查看 Trampframe 定义的结构

struct PushRegs {
    /* registers as pushed by pusha */
    uint32_t reg_edi;
    uint32_t reg_esi;
    uint32_t reg_ebp;
    uint32_t reg_oesp;      /* Useless */
    uint32_t reg_ebx;
    uint32_t reg_edx;
    uint32_t reg_ecx;
    uint32_t reg_eax;
} __attribute__((packed));

struct Trapframe {
    struct PushRegs tf_regs;
    uint16_t tf_es;
    uint16_t tf_padding1;
    uint16_t tf_ds;
    uint16_t tf_padding2;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;                                   
    uint16_t tf_cs;
    uint16_t tf_padding3;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding4;
} __attribute__((packed));

我们从 Trapframe 结构体最后一个变量 uint16_t tf_padding4; 从后往前看,可以发现从后往前看其实就是程序进入内核态压栈的顺序,因为栈是向下增长的,所以压栈完后 %esp 是较低的地址,将这个地址指针转换为 Trapframe 结构体的指针,Tramfram 各变量即从栈顶到栈底的顺序,之后就可以使用 Trapframe 方便的使用各个参数了。

之后 trap() 函数中会把 Tramframe 中的值保存在 curenv->env_tf = *tf;并且 last_tf 指向 curenv->env_tf ,当trap_dispatch()返回后,trap() 会调用 env_run(curenv); 将 curenv->env_tf 结构中保存的寄存器快照重新恢复到寄存器中,这样又会回到用户程序系统调用之后的那条指令运行,并且寄存器 eax 中保存了系统调用返回值。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容