相关文章: 如何从GCC/clang汇编输出中去除“噪音”? .cfi
指令对您来说并不直接有用,程序可以在没有它们的情况下工作。(这是异常处理和回溯所需的堆栈展开信息,因此可以默认启用 -fomit-frame-pointer
。是的,即使是C语言,gcc也会发出这个指令。)
对于生成Hello World程序所需的汇编源代码行数,显然我们希望使用libc函数来为我们完成更多的工作。
@Zwol的答案具有您原始C代码的最短实现。
如果您不关心程序的退出状态,只想让它打印出您的字符串,那么可以手动执行以下操作:
# Hand-optimized asm, not compiler output
.globl main # necessary for the linker to see this symbol
main:
# main gets two args: argv and argc, so we know we can modify 8 bytes above our return address.
movl $.LC0, 4(%esp) # replace our first arg with the string
jmp puts # tail-call puts.
# you would normally put the string in .rodata, not leave it in .text where the linker will mix it with other functions.
.section .rodata
.LC0:
.asciz "Hello world" # asciz zero-terminates
等效的C代码(你只是要求最短的Hello World,而不是具有相同语义的代码):
int main(int argc, char **argv) {
return puts("Hello world");
}
它的退出状态是实现定义的,但它肯定会打印。
puts(3)
返回“非负数”,可能超出0..255范围,因此我们不能确定程序在Linux中的退出状态为0 /非零(在这种情况下,进程的退出状态是传递给
exit_group()
系统调用的整数的低8位(由调用main()的CRT启动代码传递)。
使用JMP实现尾调用是一种标准实践, 在函数不需要在另一个函数返回后执行任何操作时常用。 puts()
最终会返回到调用 main()
的函数中,就像如果 puts() 返回到 main(),然后 main() 返回一样。 main() 的调用者仍然必须处理它为 main() 放在堆栈上的参数,因为它们仍然存在(但已经被修改了,我们可以这样做)。
gcc 和 clang 不会生成修改堆栈上传递参数空间的代码。虽然如此,它仍然是完全安全和ABI兼容的:函数“拥有”它们在堆栈上的参数,即使它们是const
类型的。如果你调用一个函数,你不能假设你放在堆栈上的参数仍然存在。要使用相同或类似的参数进行另一个调用,你需要再次存储它们所有。
还要注意,这将以与进入main()
时相同的堆栈对齐方式调用puts()
,因此我们在保留16B对齐时符合现代x86-32(也称为i386 System V ABI,用于Linux)的ABI。
.string
会在字符串末尾加上空字符,与.asciz
相同,但我不得不查一下。建议使用.ascii
或.asciz
以确保数据是否有终止字节。(如果与显式长度函数一起使用,如write()
,则不需要终止字节)
在x86-64 System V ABI(和Windows)中,参数通过寄存器传递。这使得尾调用优化变得更加容易,因为您可以重新排列参数或传递更多的参数(只要不用完寄存器)。这使得编译器愿意在实践中进行优化。(因为正如我所说,它们目前不喜欢生成修改堆栈中传入参数空间的代码,即使ABI明确允许他们这样做,并且编译器生成的函数确实假设被调用者会破坏其堆栈参数。)
对于x86-64,clang或gcc -O3将进行此优化,您可以在Godbolt编译器浏览器上看到:
#include <stdio.h>
int main() { return puts("Hello World")
# clang -O3 output
main: # @main
movl $.L.str, %edi
jmp puts # TAILCALL
# Godbolt strips out comment-only lines and directives
.L.str:
.asciz "Hello World"
静态数据地址总是适合于地址空间的低31位,且可执行文件不需要位置无关代码,否则
mov
将会是
lea .LC0(%rip), %rdi
。(如果使用了
--enable-default-pie
进行配置以生成位置无关可执行文件,则可从gcc中获得此信息。)
如何在GNU Assembler中将函数或标签的地址加载到寄存器中,请参阅
此文。
使用32位x86 Linux int 0x80
系统调用直接输出Hello World,不使用libc库
参见使用Linux系统调用编写汇编语言的Hello World程序? 我在那里的回答最初是为SO文档编写的,后来由于SO文档关闭而转移到这里。它并不真正属于这里,所以我将其移动到另一个问题。
相关链接: Linux下创建微小的ELF可执行文件龙卷风教程。你能运行的最小二进制文件只包含一个退出(exit())系统调用。这是关于最小化二进制文件大小,而不是源文件大小甚至只是实际运行的指令数量。
.cfi
开头的行和所有以.type
开头的行,以及以.file
开头的行。.LFE0
以下的所有内容都可以删除。 - Michael Petch-fno-asynchronous-unwind-tables
进行编译,您可能会看到以.cfi
开头的那些行被删除。 - Michael Petch