Recursion 额外的推送会对装配程序产生什么影响?
鉴于: 以及以下在树中搜索值的汇编代码。(与代码相同) 我想知道这一变化会产生什么影响,以及它是否会导致计划无法按预期工作: 继续后立即添加Recursion 额外的推送会对装配程序产生什么影响?,recursion,assembly,x86-64,callstack,att,Recursion,Assembly,X86 64,Callstack,Att,鉴于: 以及以下在树中搜索值的汇编代码。(与代码相同) 我想知道这一变化会产生什么影响,以及它是否会导致计划无法按预期工作: 继续后立即添加推送%rdi 据我所知,这将导致一个问题,因为我们正在将一些额外的值推送到堆栈中,因此此迭代的调用方可能会弹出错误的%rdi值。例如,在这种情况下,调用方: .section .text .global _start _start: mov $8, %esi mov $A, %rdi call func movq $60,
推送%rdi
据我所知,这将导致一个问题,因为我们正在将一些额外的值推送到堆栈中,因此此迭代的调用方可能会弹出错误的%rdi值。例如,在这种情况下,调用方:
.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
可能会弹出12+%rdi而不是弹出%rdi,但我运行了许多测试,它们似乎都在RAX中返回了正确的值,这是为什么
注意:此行是否也会导致堆栈溢出?我想答案可能是肯定的。请注意尾声中的提示:它在
ret
之前恢复RSP,撤消任何额外的推送。如果没有,额外的推送会使函数崩溃(例如,将RDI的副本弹出到RIP中),无法成功返回,但RSP指向错误的位置
可能会弹出12+%rdi而不是弹出%rdi 不,那是不可能的。推送发生在RDI被修改之前,因此您正在推送原始值,而pop会将其读回<代码>呼叫对RSP没有净影响,即RSP保留呼叫。(如果你破坏了你的堆栈,
ret
会弹出错误的东西,而不是返回地址,因此你会崩溃而不是返回。因此,除非你有意将返回地址复制到其他地方,否则你无法在修改RSP的情况下返回)
此外,RDI+12处的内存内容与RDI+12不同。如果你的字面意思是pop12(%rdi)
(即弹出到内存中),那么显然不是pop%rdi
始终写入寄存器,无论弹出什么数据
正如我刚才在你发布这篇文章时所评论的那样,在结束语中掩盖了任何不平衡推送的问题(无论是添加额外推送,还是删除pop),只要实际需要将正确的值放入正确寄存器的推送/弹出操作仍然存在。(在第一次
呼叫时保存RDI
)。您的函数在第二次调用之后不需要再次使用节点指针,因此这是毫无意义的,最好是离开/mov 12(%rdi),%rdi
/jmp func
。但是在第二次呼叫之前的任何额外推送都无关紧要
唯一会出现问题的是一个额外的pop
,它会在最后一次调用之前从堆栈中删除您的返回地址,因此它会被覆盖。(最后一次调用后的不平衡pop
将使您的返回地址位于RSP下方,但在x86-64 System V ABI的用户空间中这是安全的,因为它保证RSP下方有一个红色区域,可以安全地避免被信号处理程序等异步攻击。因此leave
仍将RSP指向它应该位于的位置,让ret
弹出返回地址
记住,leave
相当于mov%rbp,%rsp
/pop%rbp
,因此它会为自身和以后的堆栈操作重置rsp。
递归调用之前的任何额外推送都意味着每个堆栈深度使用8个额外字节的堆栈空间,但Linux用户空间堆栈默认为8MiB,因此需要相当深的树才能接近实际溢出
在调试器(如GDB)中单步执行代码,以查看其运行方式
使用display/x*(long(*)[5])$rsp
在每一步之后从堆栈中转储前5个qwords(通过将其转换为指向数组的指针并取消引用)。然后stepi
浏览您的代码,看看它是如何变化的,尤其是在离开时。请注意尾声中的注意:它在ret
之前恢复RSP,撤消任何额外的推送。如果没有,额外的推送会使您的函数崩溃(例如,将RDI的副本弹出到RIP中),未成功返回,但RSP指向错误的位置
可能会弹出12+%rdi而不是弹出%rdi
不,这是不可能的。推送发生在修改RDI之前,因此您正在推送原始值,然后pop将其读回。调用
对RSP没有净影响,即RSP保留调用。(如果你破坏了你的堆栈,ret
会弹出错误的东西,而不是返回地址,因此你会崩溃而不是返回。因此,除非你有意将返回地址复制到其他地方,否则你无法在修改RSP的情况下返回)
此外,RDI+12处的内存内容与RDI+12不同。如果您的字面意思是pop 12(%RDI)
(即pop into memory),则显然不是这样;pop%RDI
总是写入寄存器,而不管弹出什么数据
正如我刚才在你发布这篇文章时所评论的那样,在结束语中掩盖了任何不平衡推送的问题(无论是添加额外推送,还是删除pop),只要实际需要将正确的值放入正确寄存器的推送/弹出操作仍然存在。(在第一次调用前后保存RDI
)。在第二次调用之后,您的函数不需要再次使用节点指针,因此这是毫无意义的,最好只离开/mov 12(%RDI),%RDI
/jmp func
。但在第二次调用之前的任何额外推送都无关紧要
唯一会出现问题的是一个额外的pop
,它会在最后一次调用之前从堆栈中删除您的返回地址,因此它会被覆盖。(最后一次呼叫后的不平衡pop
会使您的回信地址低于RSP,但在x86-64系统V ABI的用户空间中这是安全的,因为它保证RSP下方的红色区域是安全的。)
.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
pushq %rdi # 1
mov 12(%rdi), %rdi
call func
pop %rdi # 2