编写一个最小的 64 位 Hello World

Hello World 应该是每一位程序员的启蒙程序,出自于 Brian KernighanDennis Ritchie 的一代经典著作 The C Programming Language

// 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 可以做到在终端中打印字符吗?

上面这些问题其实涉及到程序的编译、链接和装载,日常工作中也许大家并不会在意。

现代 IDE 在方便我们开发的同时,也将很多底层的细节隐藏了起来。往往写完代码以后,点击「构建」就行了,至于构建在发生什么,具体是怎么构建的,很多人并不关心,甚至根本不知道从源代码到可执行程序这中间经历了什么。

编译、链接和装载是一个巨大的话题,不是一篇博客可以覆盖的。在这篇博客中,我想使用「文件尺寸」作为线索,来介绍从 C 源代码到可执行程序这个过程中,所经历的一系列过程。

关于编译、链接和装载,这里想推荐一本书《程序员的自我修养》。不得不说,这个名字起得非常不好,很有哗众取宠的味道,但是书的内容是不错的,值得一看。

我们先来编译上面的程序:

$ gcc hello.c -o hello
$ ./hello
hello, world
$ ll hello
-rwxr-xr-x 1 root root 16712 Nov 24 10:45 hello

后续所有的讨论都是基于 64 位 CentOS7 操作系统。

我们会发现这个简单的 hello 程序大小为 16K。在今天看来,16K 真的没什么,但是考虑到这个程序所做的事情,它真的需要 16K 吗?

在 C 诞生的上个世纪 70 年代,PDP-11 的内存为 144K,如果一个 hello world 就要占 16K,那显然是不合理的,一定有办法可以缩减体积。

说起 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 简直太帅了。

接下来我们来玩一个游戏,目标是:在 CentOS7 64 位操作系统上,编写一个体积最小的打印 hello world 的可执行程序。

Executable

我们先来看「可执行程序」这个概念。

什么是可执行程序?按照字面意思来理解,那就是:可以执行的程序。

ELF

上面用 C 编写的 hello 当然是可执行程序,毫无疑问。

实际上,我们可以说它是真正的“可执行”程序(区别于后文的脚本),或者说“原生”程序。

因为它里面包含了可以直接用于 CPU 执行的机器代码,它的执行无需借助外部。

hello 的存储格式叫做 ELF,全称为 Executable and Linkable Format,看名称可以知道,它既可以用于存储目标文件,又可以用于存储可执行文件。

ELF 本身并不难理解,/usr/include/elf.h 中含有 ELF 结构的详细信息。难理解的是由 ELF 所掀开的底层世界,目标文件是什么?和执行文件有什么区别?链接在干什么?目标文件怎样变成可执行文件等等等等。

Shebang

接下来我们来看另外一种形式的可执行程序,脚本。

$ cat > hello.sh <<EOF
#!/bin/bash
echo "hello, world"
EOF
$ chmod +x hello.sh
$ ./helo.sh
hello, world

按照定义,因为这个脚本可以直接从命令行执行,所以它是可执行程序。

那么 hello 和 hello.sh 的区别在哪里?

可以发现 hello.sh 的第一行比较奇怪,这是一个叫做 Shebang 的东西 #!/bin/bash,这个东西表明当前文件需要 /bin/bash 程序来执行。

所以,hello 和 hello.sh 的区别就在于:一个可以直接执行不依赖于外部程序,而另一个需要依赖外部程序。

我曾经有一个误解,认为 Shebang 是 Shell 在处理,当 Shell 执行脚本时,发现第一行是 Shebang,然后调用相应的程序来执行该脚本。

实际上并不是这样,对 Shebang 的处理是内核在进行。当内核加载一个文件时,会首先读取文件的前 128 个字节,根据这 128 个字节判断文件的类型,然后调用相应的加载器来加载。

比如说,内核发现当前是一个 ELF 文件(ELF 文件前四个字节为固定值,称为魔数),那么就调用 ELF 加载器。

而内核发现当前文件含有 Shebang,那么就会启动 Shebang 指定的程序,将当前路径作为第一个参数传入。所以当我们执行 ./hello.sh 时,在内核中会被变为 /bin/bash ./hello.sh

这里其实有一个小问题,如果要脚本可以从命令行直接执行,那么第一行必须是 Shebang。Shebang 的形式固定为 #! 开头,对于使用 # 字符作为注释的语言比如 Python, Ruby, Elixir 来说,这自然不是问题。但是对于 # 字符不是注释字符的语言来说,这一行就是一个非法语句,必然带来解释错误。

比如 JavaScript,它就不使用 # 作为注释,我们来写一个带 Shebang 的 JS 脚本看看会怎么样。

$ cat <<EOF > test.js
#!/usr/bin/env node
console.log("hello world")
EOF
$ chmod +x test.js
$ ./test.js
hello world

并没有出错,所以这里是怎么回事?按道理来说第一行是非法的 JS 语句,解释器应该要报错才对。

如果把第一行的 Shebang 拷贝一份到第二行,会发现报了 SyntaxError,这才是符合预期的。所以必然是 Node 什么地方对第一行的 Shebang 做了特别处理,否则不可能不报错。

大家可以在 Node 的代码里面找一找,看看在什么地方😉

答案是什么地方都没有,或者说在最新的 Node 中,已经没有地方在处理 Shebang 了。

在 Node v11 中,我们可以看到相应的代码在

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值