GNU as, puts 有效但 printf 无效
GNU as, puts works but printf does not
这是我现在正在玩的代码:
# file-name: test.s
# 64-bit GNU as source code.
.global main
.section .text
main:
lea message, %rdi
push %rdi
call puts
lea message, %rdi
push %rdi
call printf
push [=10=]
call _exit
.section .data
message: .asciz "Hello, World!"
Compilation instructions: gcc test.s -o test
修订版 1:
.global main
.section .text
main:
lea message, %rdi
call puts
lea message, %rdi
call printf
mov [=11=], %rdi
call _exit
.section .data
message: .asciz "Hello, World!"
最终修订版(作品):
.global main
.section .text
main:
lea message, %rdi
call puts
mov [=12=], %rax
lea message, %rdi
call printf
# flush stdout buffer.
mov [=12=], %rdi
call fflush
# put newline to offset PS1 prompt when the program ends.
# - ironically, doing this makes the flush above redundant and can be removed.
# - The call to fflush is retained for display and
# to keep the block self contained.
mov $'\n', %rdi
call putchar
mov [=12=], %rdi
call _exit
.section .data
message: .asciz "Hello, World!"
我很难理解为什么调用 puts 成功但调用 printf 导致分段错误。
有人可以解释这种行为以及 printf 的调用方式吗?
提前致谢。
总结:
- printf 从 %rdi 中获取打印字符串和 %rax 的低位 DWORD 中的附加参数数量。
- 在将换行符放入标准输出或调用 fflush(0) 之前,无法看到 printf 结果。
puts
隐式附加一个换行符,标准输出是行缓冲的(默认情况下在终端上)。所以来自 printf
的文本可能只是坐在缓冲区中。你的call to _exit(2)
doesn't flush buffers, because it's the exit_group(2)
system call, not the exit(3)
library function。 (请参阅下面我的代码版本)。
您对 printf(3)
的调用也不完全正确,因为您在调用不带 FP 参数的 var-args 函数之前没有将 %al
归零。 (好消息@RossRidge,我错过了)。 。 %al
将是非零的(来自 puts()
的 return 值),这大概就是 printf 段错误的原因。我在我的系统上进行了测试,当堆栈未对齐时 printf 似乎并不介意(确实是这样,因为你在调用它之前按了两次,这与 puts 不同)。
此外,您不需要该代码中的任何 push
指令。第一个参数进入 %rdi
。前 6 个整数参数进入寄存器,第 7 个和后面进入堆栈。您还忽略了在函数 return 之后弹出堆栈,这只有效,因为您的函数在弄乱堆栈后从不尝试 return。
ABI 确实需要按 16B 对齐堆栈,push
是一种方法,在最近带有堆栈引擎的 Intel CPU 上,这实际上比 sub , %rsp
更有效,而且它需要更少的字节。 (参见 the x86-64 SysV ABI, and other links in the x86 标签 wiki)。
改进代码:
.text
.global main
main:
lea message, %rdi # or mov $message, %edi if you don't need the code to be position-independent: default code model has all labels in the low 2G, so you can use shorter 32bit instructions
push %rbx # align the stack for another call
mov %rdi, %rbx # save for later
call puts
xor %eax,%eax # %al = 0 = number of FP args for var-args functions
mov %rbx, %rdi # or mov %ebx, %edi will normally be safe, since the pointer is known to be pointing to static storage, which will be in the low 2G
call printf
# optionally putchar a '\n', or include it in the string you pass to printf
#xor %edi,%edi # exit with 0 status
#call exit # exit(3) does an fflush and other cleanup
pop %rbx # restore caller's rbx, and restore the stack
xor %eax,%eax # return 0
ret
.section .rodata # constants should go in .rodata
message: .asciz "Hello, World!"
lea message, %rdi
很便宜,并且执行两次比使用 %rbx
的两个 mov
指令更少的指令。但由于我们需要将堆栈调整 8B 以严格遵循 ABI 的 16B 对齐保证,我们不妨通过保存调用保留寄存器来实现。 mov reg,reg
非常便宜而且体积小,因此利用保留调用的 reg 是很自然的。
使用 mov %edi, %ebx
之类的东西可以在机器代码编码中保存 REX 前缀。如果您不确定/不明白为什么只复制低 32 位是安全的,将高 32b 清零,然后使用 64 位寄存器。一旦理解了发生了什么,您就会知道何时可以使用 32 位操作数大小来节省机器代码字节。
这是我现在正在玩的代码:
# file-name: test.s
# 64-bit GNU as source code.
.global main
.section .text
main:
lea message, %rdi
push %rdi
call puts
lea message, %rdi
push %rdi
call printf
push [=10=]
call _exit
.section .data
message: .asciz "Hello, World!"
Compilation instructions: gcc test.s -o test
修订版 1:
.global main
.section .text
main:
lea message, %rdi
call puts
lea message, %rdi
call printf
mov [=11=], %rdi
call _exit
.section .data
message: .asciz "Hello, World!"
最终修订版(作品):
.global main
.section .text
main:
lea message, %rdi
call puts
mov [=12=], %rax
lea message, %rdi
call printf
# flush stdout buffer.
mov [=12=], %rdi
call fflush
# put newline to offset PS1 prompt when the program ends.
# - ironically, doing this makes the flush above redundant and can be removed.
# - The call to fflush is retained for display and
# to keep the block self contained.
mov $'\n', %rdi
call putchar
mov [=12=], %rdi
call _exit
.section .data
message: .asciz "Hello, World!"
我很难理解为什么调用 puts 成功但调用 printf 导致分段错误。
有人可以解释这种行为以及 printf 的调用方式吗?
提前致谢。
总结:
- printf 从 %rdi 中获取打印字符串和 %rax 的低位 DWORD 中的附加参数数量。
- 在将换行符放入标准输出或调用 fflush(0) 之前,无法看到 printf 结果。
puts
隐式附加一个换行符,标准输出是行缓冲的(默认情况下在终端上)。所以来自 printf
的文本可能只是坐在缓冲区中。你的call to _exit(2)
doesn't flush buffers, because it's the exit_group(2)
system call, not the exit(3)
library function。 (请参阅下面我的代码版本)。
您对 printf(3)
的调用也不完全正确,因为您在调用不带 FP 参数的 var-args 函数之前没有将 %al
归零。 (好消息@RossRidge,我错过了)。 %al
将是非零的(来自 puts()
的 return 值),这大概就是 printf 段错误的原因。我在我的系统上进行了测试,当堆栈未对齐时 printf 似乎并不介意(确实是这样,因为你在调用它之前按了两次,这与 puts 不同)。
此外,您不需要该代码中的任何 push
指令。第一个参数进入 %rdi
。前 6 个整数参数进入寄存器,第 7 个和后面进入堆栈。您还忽略了在函数 return 之后弹出堆栈,这只有效,因为您的函数在弄乱堆栈后从不尝试 return。
ABI 确实需要按 16B 对齐堆栈,push
是一种方法,在最近带有堆栈引擎的 Intel CPU 上,这实际上比 sub , %rsp
更有效,而且它需要更少的字节。 (参见 the x86-64 SysV ABI, and other links in the x86 标签 wiki)。
改进代码:
.text
.global main
main:
lea message, %rdi # or mov $message, %edi if you don't need the code to be position-independent: default code model has all labels in the low 2G, so you can use shorter 32bit instructions
push %rbx # align the stack for another call
mov %rdi, %rbx # save for later
call puts
xor %eax,%eax # %al = 0 = number of FP args for var-args functions
mov %rbx, %rdi # or mov %ebx, %edi will normally be safe, since the pointer is known to be pointing to static storage, which will be in the low 2G
call printf
# optionally putchar a '\n', or include it in the string you pass to printf
#xor %edi,%edi # exit with 0 status
#call exit # exit(3) does an fflush and other cleanup
pop %rbx # restore caller's rbx, and restore the stack
xor %eax,%eax # return 0
ret
.section .rodata # constants should go in .rodata
message: .asciz "Hello, World!"
lea message, %rdi
很便宜,并且执行两次比使用 %rbx
的两个 mov
指令更少的指令。但由于我们需要将堆栈调整 8B 以严格遵循 ABI 的 16B 对齐保证,我们不妨通过保存调用保留寄存器来实现。 mov reg,reg
非常便宜而且体积小,因此利用保留调用的 reg 是很自然的。
使用 mov %edi, %ebx
之类的东西可以在机器代码编码中保存 REX 前缀。如果您不确定/不明白为什么只复制低 32 位是安全的,将高 32b 清零,然后使用 64 位寄存器。一旦理解了发生了什么,您就会知道何时可以使用 32 位操作数大小来节省机器代码字节。