美文网首页
Linux内核原理初窥

Linux内核原理初窥

作者: 谭英智 | 来源:发表于2022-02-27 00:04 被阅读0次

    函数堆栈

    • 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

    • 把寄存器内容压栈

    kernel-save

    函数调用

    • 压入函数调用参数

    • 保存函数返回后的下一行代码的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:

    https://github.com/mengning/linuxkernel/

    相关文章

      网友评论

          本文标题:Linux内核原理初窥

          本文链接:https://www.haomeiwen.com/subject/mawcrrtx.html