retq 期间的程序集分段错误
Assembly segmentation fault during retq
我有一些使用 callq
调用另一个的汇编代码。调用 retq
后,程序因分段错误而崩溃。
.globl main
main: # def main():
pushq %rbp #
movq %rsp, %rbp #
callq input # get input
movq %rax, %r8
callq r8_digits_to_stack
# program is not getting here before the segmentation fault
jmp exit_0
# put the binary digits of r8 on the stack, last digit first (lowest)
# uses: rcx, rbx
r8_digits_to_stack:
movq %r8, %rax # copy for popping digits off
loop_digits_to_stack:
cmpq [=10=], %rax # if our copy is zero, we're done!
jle return
movq %rax, %rcx # make another copy to extract digit with
andq , %rcx # get last digit
pushq %rcx # push last digit to stack
sarq %rax # knock off last digit for next loop
jmp loop_digits_to_stack
# return from wherever we were last called
return:
retq
# exit with code 0
exit_0:
movq [=10=], %rax # return 0
popq %rbp
retq
其中input
是returns键盘输入到%rax
的C函数。
我认为这可能与我正在操纵堆栈这一事实有关,是这样吗?
我认为您的 return 路径之一不会弹出 rbp。只需省略
pushq %rbp
movq %rsp, %rbp
pop %rbp
一共。 gcc 的默认值是 -fomit-frame-pointer
.
或者修正你的非return-零路径也弹出 rbp。
实际上,您被搞砸了,因为您的函数似乎旨在将东西放在堆栈上并且永远不会将其取下。如果您想发明自己的 ABI,其中堆栈指针下方的 space 可用于 return 数组,这很有趣,但您必须跟踪它们有多大,以便您可以调整 rsp
回到指向 ret
之前的 return 地址。
我建议不要将 return 地址加载到寄存器中,然后将后面的 ret
替换为 jmp *%rdx
或其他内容。这会在现代 CPUs 中抛出 call/return 地址预测逻辑,并导致与 b运行ch 错误预测相同的停顿。 (参见 http://agner.org/optimize/)。 CPU 讨厌不匹配 call/ret。我现在找不到关于 link 的特定页面。
请参阅 https://whosebug.com/tags/x86/info 了解其他有用的资源,包括有关函数通常如何获取 args 的 ABI 文档。
您可以将return地址复制到您刚刚推送的数组下方,然后将运行ret
复制到return并修改%rsp。但是除非你需要从多个调用点调用一个长函数,否则最好将它内联到一个或两个调用点。
如果它太大而无法在太多调用站点内联,最好的办法是模拟 call
并将 return 地址复制到新位置,而不是使用
=19=] 和 ret
。来电者
put args in some registers
lea .ret_location(%rip), %rbx
jmp my_weird_helper_function
.ret_location: # in NASM/YASM, labels starting with . are local labels, and don't show up in the object file.
# GNU assembler might only treat symbols starting with .L that way.
...
my_weird_helper_function:
use args, potentially modifying the stack
jmp *%rbx # return
你需要一个很好的理由来使用这样的东西。而且您必须用大量评论来证明/解释它,因为它 不是 读者所期待的。首先,你打算如何处理这个压入堆栈的数组?是不是要通过减去rsp和rbp之类的方法求出它的长度?
有趣的是,即使 push
必须修改 rsp 以及进行存储,它在所有最近的 CPU 上都有一个每个时钟的吞吐量。英特尔 CPUs 有一个堆栈引擎,当它仅被 push/pop/call/ret 更改时,堆栈操作不必等待在乱序引擎中计算 rsp。 (将 push/pop 与 mov 4(%rsp), %rax
或任何导致插入额外微指令以同步 OOO 引擎的 rsp 与堆栈引擎的偏移量的结果。) Intel/AMD CPUs 只能做无论如何每个时钟存储一个,但英特尔 SnB 和更高版本可以每个时钟弹出两次。
所以push/pop实际上并不是实现堆栈数据结构的糟糕方法,尤其是。在英特尔上。
此外,您的代码结构很奇怪。 main()
分为 r8_digits_to_stack
。这很好,但你并没有利用从一个块掉落到另一个块的优势,所以它只会让你在 main
中额外花费 jmp
,没有任何好处,而且会带来巨大的可读性缺点。
让我们假设您的循环是 main
的一部分,因为我已经谈到了修改 %rsp 的函数 return 是多么的奇怪。
您的循环也可以更简单。在可能的情况下,使用 jcc 返回顶部来构造事物。
避免使用高 16 位寄存器有一个小好处:具有经典寄存器的 32 位 insn 不需要 REX 前缀字节。所以让我们假装我们的起始值在 %rax.
digits_to_stack:
# put each bit of %rax into its own 8 byte element on the stack for maximum space-inefficiency
movq %rax, %rdx # save a copy
xor %ecx, %ecx # setcc is only available for byte operands, so zero %rcx
# need a test at the top after transforming while() into do{}while
test %rax, %rax # fewer insn bytes to test for zero this way
jz .Lend
# Another option can be to jmp to the test at the end of the loop, to begin the first iteration there.
.align 16
.Lpush_loop:
shr , %rax # shift the low bit into CF, set ZF based on the result
setc %cl # set %cl to 0 or 1, based on the carry flag
# movzbl %cl, %ecx # zero-extend
pushq %rcx
#.Lfirst_iter_entry
# test %rax, %rax # not needed, flags still set from shr
jnz .Lpush_loop
.Lend:
这个版本仍然有点糟糕,因为在 Intel P6 / SnB CPU 系列上,在写入较小的部分后使用较宽的寄存器会导致速度变慢。 (在 pre-SnB 上停止,或在 SnB 和更高版本上停止)。其他公司,包括 AMD 和 Silvermont,不单独跟踪部分寄存器,因此写入 %cl 依赖于 %rcx 的先前值。 (写入 32 位 reg 会将上部 32 归零,这避免了部分 reg 依赖性问题。)movzx
从 byte 到 long 的零扩展将执行 Sandybridge 隐式执行的操作,并在较旧的 CPUs.
这在 Intel 上不会完全 运行 在每次迭代的单个周期中,但在 AMD 上可能会。 mov/and
还不错,但是 and
会影响标志,使得仅基于 shr
设置标志的循环变得更加困难。
请注意,您的旧版本 sarq %rax
会改变符号位,不一定是零,因此对于负输入,您的旧版本将是一个 inf 循环(当您 运行 出栈时会出现段错误 space(推送将尝试写入未映射的页面))。
我有一些使用 callq
调用另一个的汇编代码。调用 retq
后,程序因分段错误而崩溃。
.globl main
main: # def main():
pushq %rbp #
movq %rsp, %rbp #
callq input # get input
movq %rax, %r8
callq r8_digits_to_stack
# program is not getting here before the segmentation fault
jmp exit_0
# put the binary digits of r8 on the stack, last digit first (lowest)
# uses: rcx, rbx
r8_digits_to_stack:
movq %r8, %rax # copy for popping digits off
loop_digits_to_stack:
cmpq [=10=], %rax # if our copy is zero, we're done!
jle return
movq %rax, %rcx # make another copy to extract digit with
andq , %rcx # get last digit
pushq %rcx # push last digit to stack
sarq %rax # knock off last digit for next loop
jmp loop_digits_to_stack
# return from wherever we were last called
return:
retq
# exit with code 0
exit_0:
movq [=10=], %rax # return 0
popq %rbp
retq
其中input
是returns键盘输入到%rax
的C函数。
我认为这可能与我正在操纵堆栈这一事实有关,是这样吗?
我认为您的 return 路径之一不会弹出 rbp。只需省略
pushq %rbp
movq %rsp, %rbp
pop %rbp
一共。 gcc 的默认值是 -fomit-frame-pointer
.
或者修正你的非return-零路径也弹出 rbp。
实际上,您被搞砸了,因为您的函数似乎旨在将东西放在堆栈上并且永远不会将其取下。如果您想发明自己的 ABI,其中堆栈指针下方的 space 可用于 return 数组,这很有趣,但您必须跟踪它们有多大,以便您可以调整 rsp
回到指向 ret
之前的 return 地址。
我建议不要将 return 地址加载到寄存器中,然后将后面的 ret
替换为 jmp *%rdx
或其他内容。这会在现代 CPUs 中抛出 call/return 地址预测逻辑,并导致与 b运行ch 错误预测相同的停顿。 (参见 http://agner.org/optimize/)。 CPU 讨厌不匹配 call/ret。我现在找不到关于 link 的特定页面。
请参阅 https://whosebug.com/tags/x86/info 了解其他有用的资源,包括有关函数通常如何获取 args 的 ABI 文档。
您可以将return地址复制到您刚刚推送的数组下方,然后将运行ret
复制到return并修改%rsp。但是除非你需要从多个调用点调用一个长函数,否则最好将它内联到一个或两个调用点。
如果它太大而无法在太多调用站点内联,最好的办法是模拟 call
并将 return 地址复制到新位置,而不是使用
=19=] 和 ret
。来电者
put args in some registers
lea .ret_location(%rip), %rbx
jmp my_weird_helper_function
.ret_location: # in NASM/YASM, labels starting with . are local labels, and don't show up in the object file.
# GNU assembler might only treat symbols starting with .L that way.
...
my_weird_helper_function:
use args, potentially modifying the stack
jmp *%rbx # return
你需要一个很好的理由来使用这样的东西。而且您必须用大量评论来证明/解释它,因为它 不是 读者所期待的。首先,你打算如何处理这个压入堆栈的数组?是不是要通过减去rsp和rbp之类的方法求出它的长度?
有趣的是,即使 push
必须修改 rsp 以及进行存储,它在所有最近的 CPU 上都有一个每个时钟的吞吐量。英特尔 CPUs 有一个堆栈引擎,当它仅被 push/pop/call/ret 更改时,堆栈操作不必等待在乱序引擎中计算 rsp。 (将 push/pop 与 mov 4(%rsp), %rax
或任何导致插入额外微指令以同步 OOO 引擎的 rsp 与堆栈引擎的偏移量的结果。) Intel/AMD CPUs 只能做无论如何每个时钟存储一个,但英特尔 SnB 和更高版本可以每个时钟弹出两次。
所以push/pop实际上并不是实现堆栈数据结构的糟糕方法,尤其是。在英特尔上。
此外,您的代码结构很奇怪。 main()
分为 r8_digits_to_stack
。这很好,但你并没有利用从一个块掉落到另一个块的优势,所以它只会让你在 main
中额外花费 jmp
,没有任何好处,而且会带来巨大的可读性缺点。
让我们假设您的循环是 main
的一部分,因为我已经谈到了修改 %rsp 的函数 return 是多么的奇怪。
您的循环也可以更简单。在可能的情况下,使用 jcc 返回顶部来构造事物。
避免使用高 16 位寄存器有一个小好处:具有经典寄存器的 32 位 insn 不需要 REX 前缀字节。所以让我们假装我们的起始值在 %rax.
digits_to_stack:
# put each bit of %rax into its own 8 byte element on the stack for maximum space-inefficiency
movq %rax, %rdx # save a copy
xor %ecx, %ecx # setcc is only available for byte operands, so zero %rcx
# need a test at the top after transforming while() into do{}while
test %rax, %rax # fewer insn bytes to test for zero this way
jz .Lend
# Another option can be to jmp to the test at the end of the loop, to begin the first iteration there.
.align 16
.Lpush_loop:
shr , %rax # shift the low bit into CF, set ZF based on the result
setc %cl # set %cl to 0 or 1, based on the carry flag
# movzbl %cl, %ecx # zero-extend
pushq %rcx
#.Lfirst_iter_entry
# test %rax, %rax # not needed, flags still set from shr
jnz .Lpush_loop
.Lend:
这个版本仍然有点糟糕,因为在 Intel P6 / SnB CPU 系列上,在写入较小的部分后使用较宽的寄存器会导致速度变慢。 (在 pre-SnB 上停止,或在 SnB 和更高版本上停止)。其他公司,包括 AMD 和 Silvermont,不单独跟踪部分寄存器,因此写入 %cl 依赖于 %rcx 的先前值。 (写入 32 位 reg 会将上部 32 归零,这避免了部分 reg 依赖性问题。)movzx
从 byte 到 long 的零扩展将执行 Sandybridge 隐式执行的操作,并在较旧的 CPUs.
这在 Intel 上不会完全 运行 在每次迭代的单个周期中,但在 AMD 上可能会。 mov/and
还不错,但是 and
会影响标志,使得仅基于 shr
设置标志的循环变得更加困难。
请注意,您的旧版本 sarq %rax
会改变符号位,不一定是零,因此对于负输入,您的旧版本将是一个 inf 循环(当您 运行 出栈时会出现段错误 space(推送将尝试写入未映射的页面))。