Simple c vs 汇编
你贴出的程序是一个典型的 Linux x86-64 NASM 无 libc Hello World 示例。整体逻辑正确,不过有几个更深入的点值得补充:
1. 程序执行流程
这个程序并不是从 main() 开始运行,而是:
Linux Kernel
|
v
_start
|
+--> syscall write()
|
+--> syscall exit()
|
v
Kernel terminates process
因为使用:
global _start
并直接链接:
ld hello.o -o hello
所以没有经过 C Runtime(crt)启动代码。
普通 C 程序实际上类似:
_start
|
v
__libc_start_main()
|
v
main()
|
v
exit()
而你的汇编跳过了 libc。
2. len equ $ - msg 的含义
这一行:
len equ $ - msg
是 NASM 的汇编期计算。
假设:
msg db 'Hello, world!', 0xA
占用:
H e l l o , _ w o r l d ! \n
| |
msg $
当前地址 $ 减去起始地址 msg:
len = 当前地址 - msg地址
得到:
13 个字符 + 1 个换行 = 14 bytes
所以等价于:
write(1, "Hello, world!\n", 14);
3. Linux x86-64 syscall ABI
你的寄存器使用符合 Linux System V ABI:
| 参数 | 寄存器 |
|---|---|
| syscall number | rax |
| 第1参数 | rdi |
| 第2参数 | rsi |
| 第3参数 | rdx |
| 第4参数 | r10 |
| 第5参数 | r8 |
| 第6参数 | r9 |
因此:
write
C:
write(fd, buffer, count);
汇编:
mov rax,1 ; SYS_write
mov rdi,1 ; stdout
mov rsi,msg
mov rdx,len
syscall
对应:
rax = 1
rdi = 1
rsi = msg地址
rdx = 长度
exit
C:
exit(0);
汇编:
mov rax,60
xor rdi,rdi
syscall
对应:
rax = SYS_exit
rdi = 返回值
4. 可以进一步优化
你的代码已经很小,但还能略微调整:
section .data
msg db "Hello, world!", 10
len equ $-msg
section .text
global _start
_start:
mov eax,1 ; SYS_write
mov edi,1 ; stdout
mov rsi,msg
mov edx,len
syscall
mov eax,60 ; SYS_exit
xor edi,edi
syscall
原因:
-
x86-64 中写
eax/edi/edx会自动清零高 32 位; -
指令编码更短;
-
不需要写完整
rax/rdi/rdx。
5. 查看生成的 ELF
可以观察它没有 libc:
ldd hello
输出:
not a dynamic executable
查看入口:
readelf -h hello
类似:
Entry point address: 0x401000
查看 syscall:
strace ./hello
输出类似:
write(1, "Hello, world!\n", 14) = 14
exit(0)
+++ exited with 0 +++
6. 与 C 编译结果比较
如果写:
int main()
{
write(1,"Hello, world!\n",14);
}
然后:
gcc hello.c -o hello
程序会包含:
-
ELF 文件
-
动态链接器
-
libc
-
crt 启动代码
而 NASM 版本:
ELF
|
+-- .text
|
+-- .data
|
+-- syscall
没有额外运行时。
这也是学习操作系统、编译器、逆向工程时常用的第一个实验。你给出的版本就是 Linux x86-64 汇编直接调用内核接口的最小经典示例。
浙公网安备 33010602011771号