【问题标题】:What impact could extra push do to assembly program?额外的推动会对装配程序产生什么影响?
【发布时间】:2021-07-25 19:54:00
【问题描述】:

给定:

typedef struct __attribute__((packed)) _Node{
    int data;
    struct _Node *left;
    struct _Node *right;
} Node;

以及以下在树中搜索值的汇编代码。 (与How this assembly code will be translated into c?相同的代码)

.section .text
.global _start

_start:
    mov $8, %esi
    mov $A, %rdi
    call func
    movq $60, %rax
    movq $0, %rdi
    syscall
    
func:
    pushq %rbp
    movq %rsp, %rbp
    cmp (%rdi), %esi
    jne continue
    mov $1, %eax
    jmp finish
    
continue: # go left
    cmpq $0, 4(%rdi)
    je next
    pushq %rdi # 3
    mov 4(%rdi), %rdi
    call func
    pop %rdi # 4
    cmp $1, %eax
    je finish

next: # go right
    cmpq $0, 12(%rdi)
    je fail
    pushq %rdi # 1
    mov 12(%rdi), %rdi
    call func
    pop %rdi # 2
    cmp $1, %eax
    je finish
   
fail:
    mov $0, %rax

finish:
    leave
    ret

我想知道此更改会产生什么影响以及是否会导致程序无法按预期运行:

在继续后立即添加push %rdi

据我了解,这会导致问题,因为我们将一些额外的值推送到堆栈中,因此此迭代的调用者可能会弹出错误的 %rdi 值,例如本例中的调用者:

    pushq %rdi # 1
    mov 12(%rdi), %rdi
    call func
    pop %rdi # 2

可能会弹出 12+%rdi 而不是弹出 %rdi,但是我运行了很多测试,所有测试似乎都在 RAX 中返回了正确的值,这是为什么呢?


注意:这条线也会导致堆栈溢出吗?我认为答案可能是肯定的。

【问题讨论】:

    标签: recursion assembly x86-64 callstack att


    【解决方案1】:

    注意结尾处的leave:它在ret 之前恢复RSP,撤消任何额外的推送。如果它不存在,额外的推送会使你的函数崩溃(例如,将 RDI 的副本弹出到 RIP 中),不会成功返回,但 RSP 指向错误的位置。


    可能会弹出 12+%rdi 而不是弹出 %rdi

    不,这是不可能的。推送发生在修改 RDI 之前,因此您推送原始值,然后 pop 将其读回。 call 对 RSP 没有净影响,即 RSP 是呼叫保留的。 (如果你破坏了你的堆栈,ret 会弹出错误的东西而不是返回地址,所以你会崩溃而不是返回。所以除非你故意将你的返回地址复制到其他地方,否则你不能使用 RSP 返回修改)。

    此外,RDI+12 处的内存内容与 RDI+12 相同。如果您的字面意思是pop 12(%rdi)(即弹出内存),显然不是; pop %rdi 总是写入寄存器,无论弹出什么数据。


    就像我刚刚在您发布此内容时对 your last question 发表评论一样,尾声中的 leave 掩盖了不平衡推送的任何问题(无论是添加额外推送,还是删除pop) 只要实际需要将正确的值传送到正确的寄存器 的 push/pop 操作仍然存在。 (在 first call 周围保存 RDI)。您的函数在第二个call 之后不再需要节点指针,所以它没有意义,最好只使用leave / mov 12(%rdi), %rdi / jmp func。但是在第二个call 之前的任何额外推送都无关紧要。

    唯一会出现问题的是一个额外的pop,它会在最后一个call 之前从堆栈中删除您的返回地址,因此它会被覆盖。 (不平衡pop 最后一次调用之后会使您的返回地址低于 RSP,但在 x86-64 System V ABI 的用户空间中这是安全的,因为它保证了低于 RSP 的红色区域这是安全的,不会被信号处理程序和诸如此类的异步破坏。所以leave 仍然会将 RSP 指向它应该在的位置,让ret 弹出返回地址。

    请记住,leave 等价于 mov %rbp, %rsp / pop %rbp,因此它会为自身和以后的堆栈操作重置 RSP。

    递归 call 之前的任何额外推送意味着每个堆栈深度使用 8 个额外字节的堆栈空间,但 Linux 用户空间堆栈默认为 8MiB,因此需要相当深的树才能接近实际溢出它。


    在调试器(例如 GDB)中单步执行您的代码以查看其运行方式

    使用display /x *(long (*)[5])$rsp 在每一步之后从堆栈中转储前 5 个 qwords(通过将其转换为指向数组的指针并取消引用)。然后stepi 通过你的代码,看看它是如何变化的,尤其是leave

    【讨论】:

    • “不,这不可能。推送发生在修改 RDI 之前”我的意思是它可能会读取从添加的新行中保存的错误值
    • 再次证明我的解释是错误的,当调用 pop 时,我们会将错误的值加载到寄存器 rdi 中
    • @stacker:这是不可能的,因为将值写入堆栈的推送是在 RDI 更新之前完成的。所以推送的值是原始值。所以当你弹出它时,它会读回你推送的值。这就是重点。如果您想看到实际效果,请务必使用 display /x 命令并单步执行代码。
    • " 将值写入堆栈的推送是在更新到 RDI 之前完成的" 我不同意,我们保存 rdi 然后更改 rdi 然后我们再次调用该函数并弹出对吗?当我们再次调用该函数时,它可能会向堆栈中插入错误的值,所以当我们在它完成后弹出时,我们会返回错误的值
    • @stacker:如果你愿意,可以选择任何其他调试器,并观察其中的内存。 onlinegdb.com 如果由于某种原因您不能在自己的 GNU/Linux 设置上代替 GDB,它支持 x86-64 GAS 程序集。拥有调试器对于学习汇编非常很有帮助,不使用它基本上是在浪费你自己和其他人的时间。这就像尝试构建一个机器人和/或通过声音而不是视觉来了解它是如何工作的。
    猜你喜欢
    • 1970-01-01
    • 2010-11-28
    • 1970-01-01
    • 2014-07-29
    • 2011-03-15
    • 2012-08-27
    • 2019-06-13
    • 1970-01-01
    • 2019-06-24
    相关资源
    最近更新 更多