其他
只有170字节,最小的64位Hello World程序这样写成
责编:中文妹 | 作者:CJ Ting
链接:cjting.me/2020/12/10/tiny-x64-helloworld/
最简单的 C 语言 Hello World 程序,底层到底发生了什么?如何编写出最小的 64 位 Hello World 程序?
// hello.c
#include <stdio.h>
int main() {
printf("hello, world\n");
return 0;
}
#include <stdio.h> 和 #include "stdio.h" 有什么区别?
stdio.h 文件在哪里?里面是什么内容?
为什么入口是 main 函数?可以写一个程序入口不是 main 吗?
main 的 int 返回值有什么用?是谁在处理 main 的返回值?
printf 是谁实现的?如果不用 printf 可以做到在终端中打印字符吗?
上面这些问题其实涉及到程序的编译、链接和装载,日常工作中也许大家并不会在意。
Tip: 关于编译、链接和装载,这里想推荐一本书《程序员的自我修养》。不得不说,这个名字起得非常不好,很有哗众取宠的味道,但是书的内容是不错的,值得一看。
$ gcc hello.c -o hello
$ ./hello
hello, world
$ ll hello
-rwxr-xr-x 1 root root 16712 Nov 24 10:45 hello
Tip: 后续所有的讨论都是基于 64 位 CentOS7 操作系统。
Tip:
说起 C 语言,我想顺带提一下 UNIX。没有 C 就没有 UNIX 的成功,没有 UNIX 的成功也就没有 C 的今天。诞生于上个世纪 70 年代的 UNIX 不得不说是一项了不起的创造。
这里推荐两份关于 UNIX 的资料:
The UNIX Time-Sharing System 是1974 年由 Dennis Ritchie 和 Ken Thompson 联合发表的介绍 UNIX 的论文。不要被「论文」二字所吓到,实际上,这篇文章写得非常通俗易懂,由 UNIX 的作者们向你娓娓道来 UNIX 的核心设计理念。
The UNIX Operating System 是一段视频,看身着蓝色时尚毛衣的 Kernighan 演示 UNIX 的特性,不得不说,Kernighan 简直太帅了。
$ cat > hello.sh <<EOF
#!/bin/bash
echo "hello, world"
EOF
$ chmod +x hello.sh
$ ./helo.sh
hello, world
$ cat <<EOF > test.js
#!/usr/bin/env node
console.log("hello world")
EOF
$ chmod +x test.js
$ ./test.js
hello world
$ export name=shell
$ node
> process.env.name
'shell'
$ env name=env node
> process.env.name
'env'
# 运行 Go 文件的指令是 `go run`,不是一个独立的程序
# 所以,我们先要写一个脚本包装一下
$ cat <<EOF > /usr/local/bin/rungo
#!/bin/bash
go run $1
EOF
# 接下来写入规则告诉 binfmt_misc 使用上面的程序来加载所有
# 以 .go 结尾的文件
$ echo ':golang:E::go::/usr/local/bin/rungo:' > /proc/sys/fs/binfmt_misc/register
# 现在我们就可以直接运行 Go 文件了
$ cat << EOF > test.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
EOF
$ chmod +x test.go
$ ./test.go
hello, world
解释器路径要尽量短;
脚本本身用于打印的代码要尽量短。
# 假设 php 在 /usr/local/bin/php
$ cd /
$ ln -s /usr/local/bin/php p
$ cat <<EOF > final.php
#!/p
hello, world
EOF
$ chmod +x final.php
$ ./final.php
hello, world
$ ll final.php
-rwxr-xr-x 1 root root 18 Dec 2 22:32 final.php
Tip: 64 位机器可以执行 32 位的程序,比如我们可以使用 gcc -m32 来编译 32 位程序。但这只是一个后向兼容,并没有充分利用 64 位机器的能力。
// hello.c
#include <stdio.h>
int main() {
printf("hello, world\n");
return 0;
}
Tip:nm 是「窥探」二进制的一个有力工具。记得之前有一次苹果调整了 iOS 的审核策略,不再允许使用了 UIWebView 的 App 提交。我们的 IPA 里面不知道哪个依赖使用了 UIWebView,导致苹果一直审核不过,每次都要二分注释、打包、提交审核,然后等待苹果的自动检查邮件告知结果,非常痛苦。
后来我想到了一个办法,就是使用 nm 查看编译出来的可执行程序,看看里面是否有 UIWebView 相关的 symbol,这大大简化了调试流程,很快就定位到问题了。
#include <stdio.h>#include <unistd.h>
int
nomain()
{
printf("hello, world\n");
_exit(0);
}
write: 向终端打印字符实际上就是向终端对应的文件写入数据
exit: 退出程序
char *str = "hello, world\n";
void
myprint()
{
asm("movq $1, %%rax \n"
"movq $1, %%rdi \n"
"movq %0, %%rsi \n"
"movq $13, %%rdx \n"
"syscall \n"
: // no output
: "r"(str)
: "rax", "rdi", "rsi", "rdx");
}
void
myexit()
{
asm("movq $60, %rax \n"
"xor %rdi, %rdi \n"
"syscall \n");
}
int
nomain()
{
myprint();
myexit();
}
$ readelf -S -W step4/hello.out
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000000401000 001000 00006e 00 AX 0 0 16
[ 2] .rodata PROGBITS 0000000000402000 002000 00000e 01 AMS 0 0 1
[ 3] .eh_frame_hdr PROGBITS 0000000000402010 002010 000024 00 A 0 0 4
[ 4] .eh_frame PROGBITS 0000000000402038 002038 000054 00 A 0 0 8
[ 5] .data PROGBITS 0000000000404000 003000 000008 00 WA 0 0 8
[ 6] .comment PROGBITS 0000000000000000 003008 000022 01 MS 0 0 1
[ 7] .shstrtab STRTAB 0000000000000000 00302a 000040 00 0 0 1
$ ld --verbose
GNU ld (GNU Binutils) 2.34
...
. = ALIGN(CONSTANT (MAXPAGESIZE));
...
. = ALIGN(CONSTANT (MAXPAGESIZE));
...
$ cat > link.lds <<EOF
ENTRY(nomain)
SECTIONS
{
. = 0x8048000 + SIZEOF_HEADERS;
tiny : { *(.text) *(.data) *(.rodata*) }
/DISCARD/ : { *(*) }
}
EOF
section .data
message: db "hello, world", 0xa
section .text
global nomain
nomain:
mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
mov rax, 60
xor rdi, rdi
syscall
BITS 64
org 0x400000
ehdr: ; Elf64_Ehdr
db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident
times 8 db 0
dw 2 ; e_type
dw 0x3e ; e_machine
dd 1 ; e_version
dq _start ; e_entry
dq phdr - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsize ; e_ehsize
dw phdrsize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
ehdrsize equ $ - ehdr
phdr: ; Elf64_Phdr
dd 1 ; p_type
dd 5 ; p_flags
dq 0 ; p_offset
dq $$ ; p_vaddr
dq $$ ; p_paddr
dq filesize ; p_filesz
dq filesize ; p_memsz
dq 0x1000 ; p_align
phdrsize equ $ - phdr
_start:
mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
mov rax, 60
xor rdi, rdi
syscall
message: db "hello, world", 0xa
filesize equ $ - $$
Tip: 其实还可以继续,还有一些技巧可以进一步减小体积,因为非常的「Hack」,这里不打算说明了。有兴趣的朋友可以参考《A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux》。
# ELF Header
00: 7f 45 4c 46 02 01 01 00 # e_ident
08: 00 00 00 00 00 00 00 00 # reserved
10: 02 00 # e_type
12: 3e 00 # e_machine
14: 01 00 00 00 # e_version
18: 78 00 40 00 00 00 00 00 # e_entry
20: 40 00 00 00 00 00 00 00 # e_phoff
28: 00 00 00 00 00 00 00 00 # e_shoff
30: 00 00 00 00 # e_flags
34: 40 00 # e_ehsize
36: 38 00 # e_phentsize
38: 01 00 # e_phnum
3a: 00 00 # e_shentsize
3c: 00 00 # e_shnum
3e: 00 00 # e_shstrndx
# Program Header
40: 01 00 00 00 # p_type
44: 05 00 00 00 # p_flags
48: 00 00 00 00 00 00 00 00 # p_offset
50: 00 00 40 00 00 00 00 00 # p_vaddr
58: 00 00 40 00 00 00 00 00 # p_paddr
60: aa 00 00 00 00 00 00 00 # p_filesz
68: aa 00 00 00 00 00 00 00 # p_memsz
70: 00 10 00 00 00 00 00 00 # p_align
# Code
78: b8 01 00 00 00 # mov $0x1,%eax
7d: bf 01 00 00 00 # mov $0x1,%edi
82: 48 be 9d 00 40 00 00 00 00 00 # movabs $0x40009d,%rsi
8c: ba 0d 00 00 00 # mov $0xd,%edx
91: 0f 05 # syscall
93: b8 3c 00 00 00 # mov $0x3c,%eax
98: 48 31 ff # xor %rdi,%rdi
9b: 0f 05 # syscall
9d: 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 0a # "hello, world\n"
可以发现 ELF Header 是 64 个字节,Program Header 是 56 字节,代码 37 个字节,最后 13 个字节是 hello, world\n 这个字符串数据。
--END--
往期精彩
又一起“删库”跑路!程序员怒删公司9TB数据,被判7年!因Git服务器配置错误,日产汽车源码泄露;雷军给工程师团队发百万美金大奖
喜欢本文的朋友们,欢迎长按下图,关注订阅号Linux中文社区
收看更多精彩内容