【问题标题】:Why does switch_to use push+jmp+ret to change EIP, instead of jmp directly?为什么switch_to使用push+jmp+ret来改变EIP,而不是直接使用jmp?
【发布时间】:2013-02-22 08:41:21
【问题描述】:

linux/arch/x86/include/asm/switch_to.h,有宏switch_to的定义,真正线程切换奇迹的关键行是这样写的(直到Linux 4.7改变时):

asm volatile("pushfl\n\t"       /* save    flags */ \
              pushl %%ebp\n\t"      /* save    EBP   */ \
              "movl %%esp,%[prev_sp]\n\t"   /* save    ESP   */ \
              "movl %[next_sp],%%esp\n\t"   /* restore ESP   */ \
              "movl $1f,%[prev_ip]\n\t" /* save    EIP   */ \
              "pushl %[next_ip]\n\t"    /* restore EIP   */ \
              __switch_canary                   \
              "jmp __switch_to\n"   /* regparm call  */ \
              "1:\t"                        \
              "popl %%ebp\n\t"      /* restore EBP   */ \
              "popfl\n"         /* restore flags */ \

命名操作数具有内存限制,例如[prev_sp] "=m" (prev->thread.sp)__switch_canary 被定义为空,除非 CONFIG_CC_STACKPROTECTOR 被定义(然后它是使用 %ebx 的加载和存储)。

我了解它是如何工作的,例如内核堆栈指针备份/恢复,以及 push next->eipjmp __switch_to 如何在函数末尾加上 ret 指令,这实际上是一个“假”调用指令匹配真正的ret指令,有效地使next->eip成为下一个线程的返回点。

我不明白的是,为什么要破解?为什么不只是call __switch_to,然后是retjmpnext->eip,这样更干净、更易于阅读。

【问题讨论】:

    标签: assembly linux-kernel x86


    【解决方案1】:

    这样做有两个原因。

    一个是为[next_ip] 提供操作数/寄存器分配的完全灵活性。如果您希望能够在call __switch_to 之后执行jmp %[next_ip] ,那么有必要将%[next_ip] 分配给一个非易失性寄存器(即,根据 ABI 定义,在进行函数调用时将保留其值)。

    这对编译器的优化能力造成了限制,context_switch()(“调用者” - 使用 switch_to() 的地方)的结果代码可能不如预期的那么好。但是有什么好处呢?

    嗯 - 这就是第二个原因,没有,真的,因为call __switch_to 相当于:

    pushl 1f
    jmp __switch_to
    1: jmp %[next_ip]
    

    即它推送返回地址;你最终会得到一个序列push/jmp (== call)/ret/jmp 而如果你不想返回这个地方(并且这段代码不想),你保存通过“伪造”调用在代码分支上,因为您只需要做push/jmp/ret。代码在这里使自己尾递归

    是的,这是一个小的优化,但避免分支会减少延迟,而延迟对于上下文切换至关重要。

    【讨论】:

    • 但它不会有效地杀死返回预测堆栈吗?
    • 是的 - 但是注册目标的jmp 也可以做到这一点,因为你永远想要返回switch_to(),真的(直到下一次上下文切换)。就这一点而言,两者之间没有区别。
    • @harold:好点;在从context_switch()in kernel/sched/core.c)返回的上下文切换之后,返回地址预测器堆栈的当前内容是有价值的,并且可能有几个级别将调用堆栈备份到调度程序中(直到它们的回溯发生分歧)。但这只有在 %[next_ip] 总是/通常在 switch_to 内时才是正确的;它确实以这种方式设置了prev_ip,但也许这不是最常见的值(内核抢占可能会将其留在其他地方?)
    • 但是返回预测堆栈的主要值是用于在中型循环内重复调用的调用树底部附近的函数。不匹配的 call/ret 在返回调用树的过程中最多会导致 16 个未来的错误预测,如果更多 call 指令在返回那么远之前将一些不匹配的条目从堆栈中弹出,则更少。
    猜你喜欢
    • 2020-06-11
    • 2017-10-09
    • 2016-11-27
    • 1970-01-01
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    • 1970-01-01
    • 2021-12-07
    相关资源
    最近更新 更多