如果我要在程序集中编写程序,这个HelloWorld汇编代码的哪些部分是必须的?

我有这个简短的你好世界计划:

#include <stdio.h> static const char* msg = "Hello world"; int main(){ printf("%s\n", msg); return 0; } 

我用gcc把它编译成下面的汇编代码:

  .file "hello_world.c" .section .rodata .LC0: .string "Hello world" .data .align 4 .type msg, @object .size msg, 4 msg: .long .LC0 .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 andl $-16, %esp subl $16, %esp movl msg, %eax movl %eax, (%esp) call puts movl $0, %eax leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4" .section .note.GNU-stack,"",@progbits 

我的问题是:如果我要在汇编中编写这个程序(而不是用C编写,然后编译为汇编),那么这些代码的所有部分都是必不可less的? 我了解汇编指令,但有一些我不明白的部分。 例如,我不知道.cfi *是什么,我想知道是否需要包含这个程序来编写程序。

Solutions Collecting From Web of "如果我要在程序集中编写程序,这个HelloWorld汇编代码的哪些部分是必须的?"

在这个似乎是平台上工作的绝对最低限度是

  .globl main main: pushl $.LC0 call puts addl $4, %esp xorl %eax, %eax ret .LC0: .string "Hello world" 

但是这打破了一些ABI的要求。 ABI兼容计划的最低要求是

  .globl main .type main, @function main: subl $24, %esp pushl $.LC0 call puts xorl %eax, %eax addl $28, %esp ret .size main, .-main .section .rodata .LC0: .string "Hello world" 

对象文件中的其他内容是编译器不尽可能地优化代码,或者将可选注释写入目标文件。

.cfi_*指令尤其是可选的注释。 当且仅当函数可能在C ++异常抛出时调用堆栈上时,它们才是必需的 ,但是它们在任何可能需要提取堆栈跟踪的程序中都是有用的 。 如果你要用汇编语言手工编写非常规的代码,可能值得学习如何编写它们。 不幸的是,他们的记录很差, 我目前没有找到任何我认为值得链接的东西。

该线

 .section .note.GNU-stack,"",@progbits 

了解你是否手工编写汇编语言也很重要; 它是另一个可选的注释,但却是有价值的注释,因为它的意思是“这个对象文件中没有任何东西需要堆栈可执行”。 如果一个程序中的所有目标文件都有这个注解,那么内核将不会使堆栈可执行,这会提高安全性。

(为了表明你需要栈是可执行的,你把"x"代替"" 。如果你使用它的“嵌套函数”扩展名,GCC可以这样做(不这样做)。

可能值得一提的是,在GCC和GNU binutils使用的“AT&T”汇编语法中(默认情况下),有三种类型的行:带有单个令牌的行,以冒号结尾,是一个标签。 (我不记得哪些字符可以出现在标签中的规则。) 第一个标记以点开头,不以冒号结尾的行是汇编程序的某种指令。 其他任何东西都是汇编指令。

相关: 如何从GCC / clang程序集输出中删除“噪音”? .cfi指令对你没有直接的用处,程序在没有它们的情况下工作。 (这是异常处理和回溯所需的堆栈展开信息,因此-fomit-frame-pointer可以默认启用。是的,gcc甚至可以为C发出这个消息。


就产生一个Hello World程序所需要的asm源代码行数而言,显然我们希望使用libc函数为我们做更多的工作。

@ Zwol的答案有你最初的C代码的最短实现。

如果你不关心程序的退出状态,只需要打印你的字符串就可以手工完成。

  .globl main main: # main gets two args: argv and argc, so we know we can modify the 8 bytes above our return address. mov $.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. .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来实现tail-call是一个标准的做法 ,当一个函数在另一个函数返回后不需要做任何事情时通常使用。 puts()将最终返回到调用main()的函数,就像puts()返回到main(),然后main()返回一样。 main()的调用者仍然需要处理它放在main()的堆栈上的参数,因为他们仍然在那里(但是被修改了,我们被允许这么做)。

gcc和clang不会生成修改堆栈上arg传递空间的代码。 尽管它是完全安全的并且符合ABI标准:即使它们是const ,函数仍然“拥有”它们的参数。 如果你调用一个函数,你不能认为你放在堆栈上的参数仍然存在。 要使用相同或相似的参数进行另一个呼叫,则需要再次存储它们。

还要注意的是,这调用puts()与我们在进入main() puts()使用的堆栈对齐方式,所以我们再次符合ABI标准,以保持现代版本的x86-32又名i386系统V ABI (由Linux使用)。

.string零终止字符串,与.asciz相同,但是我必须查找它 。 我建议只使用.ascii.asciz来确保你清楚你的数据是否有终止字节。 (如果使用write()等显式长度函数,则不需要其中一个)


在x86-64 System V ABI(和Windows)中,参数在寄存器中传递。 这使得尾部调用优化更容易,因为您可以重新排列参数或传递更多的参数(只要您没有用完寄存器)。 这使编译器愿意在实践中做到这一点。 (因为正如我所说的,他们目前不会生成修改栈上传入的arg空间的代码,尽管ABI清楚地允许它们,而编译器生成的函数则假定函数会打断它们的栈参数。)

铿锵声或gcc -O3将为x86-64做这种优化, 你可以在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; there's actually a .section .rodata before this .L.str: .asciz "Hello World" 

静态数据地址总是适合地址空间的低31位,可执行文件不需要与位置无关的代码,否则mov将是lea .LC0(%rip), %rdi 。 (如果使用--enable-default-pie来配置位置独立的可执行文件,您将从gcc获得此文件。)


Hello World直接使用32位x86 Linux系统调用,没有libc

我最初为SO Docs(主题ID:1164,示例ID:19078)写了这个,用@runner重写了一个基本的不太好评论的例子。 这是在NASM的语法,所以这不是一个完美的适合这个问题。


如果你还不知道底层的Unix系统编程,你可能只需要在asm中编写函数,然后返回一个值(或者通过指针arg更新数组)并从C或C ++程序中调用它们。 那么你可以担心学习如何处理寄存器和内存,而不用学习POSIX系统调用API和ABI来使用它。 这也使得您可以很容易地将您的代码与C实现的编译器输出进行比较。 编译器通常在编写高效的代码方面做得非常好,但是很少完美 。

libc为系统调用提供封装函数,所以编译器生成的代码会call write而不是直接用int 0x80 (或者如果你关心性能, sysenter )调用它。 (在x86-64代码中, 对64位ABI使用syscall 。)另请参阅syscalls(2)

系统调用记录在第2章手册页中,如write(2) 。 有关libc包装函数和底层Linux系统调用之间的区别,请参见NOTES部分。 请注意, sys_exit的包装是_exit(2) ,而不是exit(3) ISO C函数,首先刷新stdio缓冲区和其他清理。 还有一个exit_group系统调用可以结束所有的线程 。 exit(3)实际上使用它,因为在单线程的过程中没有任何缺点。

这段代码使2个系统调用:

  • sys_write(1, "Hello, World!\n", sizeof(...));
  • sys_exit(0);

我大量地评论它(在没有颜色语法高亮的情况下,它开始模糊实际的代码)。 这是一个尝试,指出事情总的初学者,而不是你应该如何评论你的代码正常。

 section .text ; Executable code goes in the .text section global _start ; The linker looks for this symbol to set the process entry point, so execution start here ;;;a name followed by a colon defines a symbol. The global _start directive modifies it so it's a global symbol, not just one that we can CALL or JMP to from inside the asm. ;;; note that _start isn't really a "function". You can't return from it, and the kernel passes argc, argv, and env differently than main() would expect. _start: ;;; write(1, msg, len); ; Start by moving the arguments into registers, where the kernel will look for them mov edx,len ; 3rd arg goes in edx: buffer length mov ecx,msg ; 2nd arg goes in ecx: pointer to the buffer ;Set output to stdout (goes to your terminal, or wherever you redirect or pipe) mov ebx,1 ; 1st arg goes in ebx: Unix file descriptor. 1 = stdout, which is normally connected to the terminal. mov eax,4 ; system call number (from SYS_write / __NR_write from unistd_32.h). int 0x80 ; generate an interrupt, activating the kernel's system-call handling code. 64-bit code uses a different instruction, different registers, and different call numbers. ;; eax = return value, all other registers unchanged. ;;;Second, exit the process. There's nothing to return to, so we can't use a ret instruction (like we could if this was main() or any function with a caller) ;;; If we don't exit, execution continues into whatever bytes are next in the memory page, ;;; typically leading to a segmentation fault because the padding 00 00 decodes to add [eax],al. ;;; _exit(0); xor ebx,ebx ; first arg = exit status = 0. (will be truncated to 8 bits). Zeroing registers is a special case on x86, and mov ebx,0 would be less efficient. ;; leaving out the zeroing of ebx would mean we exit(1), ie with an error status, since ebx still holds 1 from earlier. mov eax,1 ; put __NR_exit into eax int 0x80 ;Execute the Linux function section .rodata ; Section for read-only constants ;; msg is a label, and in this context doesn't need to be msg:. It could be on a separate line. ;; db = Data Bytes: assemble some literal bytes into the output file. msg db 'Hello, world!',0xa ; ASCII string constant plus a newline (0x10) ;; No terminating zero byte is needed, because we're using write(), which takes a buffer + length instead of an implicit-length string. ;; To make this a C string that we could pass to puts or strlen, we'd need a terminating 0 byte. (eg "...", 0x10, 0) len equ $ - msg ; Define an assemble-time constant (not stored by itself in the output file, but will appear as an immediate operand in insns that use it) ; Calculate len = string length. subtract the address of the start ; of the string from the current position ($) ;; equivalently, we could have put a str_end: label after the string and done len equ str_end - str 

注意我们把字符串长度存储在任何地方的数据存储器中。 这是一个汇编时间常量,所以将其作为一个立即操作数比一个负载更高效。 我们也可以用三个push imm32指令将字符串数据压入堆栈,但是过多的代码大小并不是一件好事。


在Linux上,您可以将此文件保存为Hello.asm使用以下命令从中构建一个32位可执行文件

 nasm -felf32 Hello.asm # assemble as 32-bit code. Add -Worphan-labels -g -Fdwarf for debug symbols and warnings gcc -static -nostdlib -m32 Hello.o -o Hello # link without CRT startup code or libc, making a static binary 

请参阅此答案,以获取有关将程序​​集构建到32位或64位静态或动态链接的Linux可执行文件,NASM / YASM语法或GNU AT&T语法以及GNU as指令的更多详细信息。 (关键点:在64位主机上构建32位代码时,请确保使用-m32或等效代码,否则在运行时会出现混淆问题。)


你可以用strace跟踪它的执行情况,看看它所做的系统调用

 $ strace ./Hello execve("./Hello", ["./Hello"], [/* 72 vars */]) = 0 [ Process PID=4019 runs in 32 bit mode. ] write(1, "Hello, world!\n", 14Hello, world! ) = 14 _exit(0) = ? +++ exited with 0 +++ 

比较这与动态链接过程的跟踪(如gcc从hello.c或从运行strace /bin/ls )来跟踪,以了解在动态链接和C库启动时发生了多少事情。

stderr上的跟踪和stdout上的常规输出都在这里到达终端,所以它们会干扰write系统调用的线路。 重定向或跟踪文件,如果你在意的话。 注意,这样可以让我们轻松地看到系统调用的返回值,而不必添加代码来打印它们,而且实际上比使用常规调试器(如gdb)更简单,只需单步执行并查看eax 。 有关gdb asm的提示,请参阅x86标记wiki的底部。 (标签wiki的其余部分充满了良好资源的链接。)

这个程序的x86-64版本是非常相似的,将相同的参数传递给相同的系统调用,只是在不同的寄存器和syscall而不是int 0x80 。 查看底部如果在64位代码中使用32位int 0x80 Linux ABI会发生什么情况? 作为编写一个字符串并退出64位代码的工作示例。


相关: 一个Whirlwind教程创建真正的Teensy ELF可执行文件的Linux 。 你可以运行的最小的二进制文件只是做一个exit()系统调用。 这是关于最小化二进制大小,而不是源大小,甚至只是实际运行的指令数量。