网易首页 > 网易号 > 正文 申请入驻

第一行代码 Hello World 的背后?开发者:“一言难尽……”

0
分享至


【CSDN 编者按】相信每位程序员都对“Hello World”程序非常熟悉,但你是否了解其背后的抽象世界呢?

原文链接:https://thecoder08.github.io/hello-world.html

未经允许,禁止转载!

作者 | Lennon McLean 责编 | 夏萌

译者 | 弯月

出品 | CSDN(ID:CSDNnews)

在本文中,我们来深入探讨现代Hello World程序背后的抽象世界。



背景介绍

本文主要探讨用C语言编写的Hello World程序。不考虑具体的编程语言在Hello World正式运行之前解释器/编译器/JIT等工作的话,C语言就是高级语言所能达到的最高层次了。

原本我写这篇文章的目的是让所有具备一些编程背景的人都能理解,但现在我认为具备一些C语言或汇编语言的知识会更有帮助。


Hello World代码

每个人都应该很熟悉Hello World程序。学习Python时,你编写的第一个程序可能像下面这样:

非常简单,就是在屏幕上输出文本“Hello World!”。

在本文中,我们来看一看用C语言编写的Hello World程序。你能看懂下面的代码吗?


这个程序执行的操作与上述Python代码完全一样。但与Python不同,你不能直接调用解释器运行这个程序。你必须先运行编译器,将这段代码转换成机器代码,然后才能在计算机的处理器上直接运行。所有现代大型程序都是这样编写的。

因此,我们必须运行以下命令:

这个命令可以将文件hello.c中的C代码转换成机器代码,并生成一个名为hello的程序。然后,我们就可以通过如下命令运行程序了:

结果是:


我们的程序

那么,我们的程序是如何输出这个文本的呢?首先,我们来看看我们的程序,看看里面究竟是什么。


你不用担心看不懂,我会慢慢解释。重点是下面这几个字段:

这几个字段告诉我们,这个程序是x86_64指令集架构上的ELF可执行文件。什么意思?

ELF可执行文件是Linux文件,相当于Windows下的.exe文件,就是一种计算机可以运行的程序。其余信息告诉我们,这是一个在 64位 x86 处理器上运行的机器代码程序,64位 x86 处理器是自1981年以来IBM计算机一直在使用的CPU架构。当然,当时还不是64位的,但现代处理器也可以运行为IBM PC编写的代码。这又是另一个话题了。

我们的程序文件包含的是机器代码,一种语言,也是CPU能理解的唯一语言。那么,CPU从何处开始运行代码呢?


此处的重点是:Entry point address,其值为 0x1060。这是一个十六进制数字,代表了程序加载到计算机内存后,程序中的一个位置。那么,这个位置上究竟有什么呢?


代码

这条命令完整的输出太长了,此处就不贴了。下面是截取的一部分,请注意 1060: 开头的一行:


什么意思?冒号前面的数字是后面的字节的地址,也就是它们在文件中的位置。后面的数字是程序文件中的数据字节,此处表示机器代码。后面的文本是机器代码的反汇编。汇编语言是人类可读的机器代码的表示。请注意,即便左侧的字节不表示代码,反汇编器仍会尝试对它们进行反汇编。由此会产生一些垃圾和毫无意义的汇编代码。

如上,我们找到了一些代码!但不是我们编写的代码。这些代码是编译器(严格来说是链接器)自动添加到程序中的。本质上,这些代码会执行一些初始化,然后运行一个重要的指令:

这条指令告诉计算机去执行其他地方的一些代码,此处即为地址0x2f53,当动态链接器加载我们的程序时,这个地址会被改为0x3fd8。关于这一点,此处不做详细探讨。

但无论你怎么努力寻找,我们的文件中都找不到这两个地址。准确来说,0x3fd8在全局偏移表中,同样相关内容也超出了本文的范围,但此刻它是空的。这是因为这段代码不是在我们的程序中定义的,而是在其他地方。


C 库

那么究竟在哪里?


我们的代码依赖的库有很多,上面只是其中一部分。我们可以看到下面这行:

main函数当然就在我们的程序中。再看看反汇编,你会看到:


终于看到我们的代码了!那它究竟干了什么呢?

  1. 设置了一个栈帧。

  2. 设置了我们的函数调用的参数。

  3. 调用了我们的Hello World函数。

  4. 清理了栈帧。

  5. 从函数中返回,退出代码为0。

这就是我们在源代码中看到的内容。但什么是栈帧呢?它是计算机内存的一部分,我们的程序用栈帧来存储局部变量,即在main函数内声明的变量。幸运的是,我们没有声明任何变量,所以不需要在意。重点是下面这部分:


具体操作为:

  1. 设置 Hello World 字符串的内存地址,将其作为函数调用的第一个参数(间接调用)。

  2. 调用 puts() 函数。

等等,puts()?我们调用的不是 printf() 吗?

没错。但是,编译器进行了一种优化。printf 函数很复杂,因为它能够打印“格式化输出”,这意味着我们可以在输出中嵌入变量。这个函数负责将它们转换为字符串并输出,但我们没有用到这些功能。因此,编译器将 printf() 替换为更为简单的 puts(),后者仅负责打印一串未经格式化的文本。那么,我们的文本在哪里?


字符串

根据反汇编器的显示,我们的字符串位于地址 0x0eac,加载后会转换为地址 0x2004。那么,字符串里面是什么呢?


前面,我说过即使不是代码,反汇编器也会尝试进行反汇编,这就是一个很好的例子。忽略上面这些汇编语言,因为它们毫无意义。我们来看看 0x2004,后面是一串十六进制字节 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 00,翻译过来就是字符串“Hello World!”,最后是一个NULL终止符。

但是我们的字符串中不是还包含一个换行符 \n 吗,不是应该被翻译为 ASCII 0x0a 吗?没错,但这也是编译器优化后的结果。puts() 函数会在字符串后面添加换行符,而 printf() 不会。因此,我们的换行符被移除了,这样输出就只包含一个换行符。

我们还看到了一个NULL字节 0x00,又称作NULL终止符。所有 C 字符串的末尾都有这个字节。在 C 中,字符串不包含任何长度信息。因此,接受任何长度的字符串作为参数的函数会逐字节地对其进行操作,直到遇到NULL终止符。如果内存中有多个字符串,并且它们之间没有NULL终止符,那么 C 函数将一次性操作所有字符串。最终,函数将来到字符串末尾,并开始读取不允许读取的内存,而你的程序将崩溃并显示“Segmentation Fault”错误。


puts()

puts()的地址是0x1050。


又一次调用标准库(严格来说是全局偏移表,但最终是标准库)。

此处,我们还是不想阅读标准库的反汇编代码,但幸运的是 Glibc(我们的 C 标准库)是开源的。我们能从中发现什么呢?

在标准库中,puts() 的别名为 _IO_puts。


可以看到,这个函数获取了字符串的长度,获得了输出流锁,进行了一些检查,并调用了 _IO_sputn。然后,释放锁,并返回打印字符的数量。

我搜索了一下这个函数,但没有找到。很明显,它通过一个名为 _IO_file_jumps 的函数执行了一些操作,并调用了 _IO_new_file_xsputn。


好长的一段代码,我可不打算去分析这段代码究竟在干什么。我知道使用 Glibc 来解释这段代码会很麻烦。因此,此处我决定查看 musl libc,我知道它应该很小。


musl

在 musl 中,puts() 定义如下:


首先,获取输出流锁;然后,调用fputs;最后,释放输出流锁。

那么,fputs又是怎样定义的呢?


获取字符串的长度,然后调用fwrite(),参数为输出流、字符串及其长度。

那么,fwrite()的定义又是什么呢?


获取另一个输出流锁,然后调用__fwritex(),然后释放输出流锁。

那么,__fwritex()的定义又是什么呢?


这段代码有点多,但主要操作是使用输出流的FILE对象调用write()。我们的流被定义为标准输出(stdout),这又是在哪里定义的呢?


此处,write函数被定义为__stdout_write(),那么后者的定义又是什么呢?


针对我们的输出流执行了一次 TIOCGWINSZ ioctl,然后又调用了 __stdio_write(),那么后者的定义又是什么呢?


我们距离终点已经很近了。这个函数执行了很多操作,调用了 syscall(),第一个参数为 SYS_writev。那么,syscall() 是如何定义的呢?


syscall()的第一个参数为系统调用编号,还接受数量可变的额外参数。va_arg()调用将这些参数读入变量a、b、c、d、e和f中。然后,我们使用这些参数调用__syscall(),并将结果放入__syscall_ret()。

不幸的是,我找不到__syscall()的定义。但我觉得这是因为这部分属于平台范畴。Musl是一个多架构的C库,因此从这个深度开始运行的代码取决于我们使用的是什么架构。在深入研究之前,我看了一眼__syscall_ret():


检查__syscall()的返回值是否有效,如果无效,则系统调用失败,因此返回-1。


系统调用

我们的Hello World调用的最后几个阶段涉及到了系统调用。什么是系统调用?无论我们的C库有多大,都无法完成底层的一些工作。其中之一就是与硬件通信。这部分工作预留给了内核,是操作系统的一部分,负责控制并共享IO设备、内存和CPU的访问。在这个例子中,这部分工作由Linux内核负责。在Windows中是ntoskrnl.exe,也就是任务管理器显示的System。

这意味着,向操作系统传达了后面的工作后,puts()调用就功成身退了。在这个例子中,我们要求操作系统向输出流写入一些文本。写入流的工作是系统调用write完成的。Musl使用了一个类似的系统调用,叫做writev,它可以在数组中写入多个缓冲区。下面,我们来看看musl如何进行系统调用。


我们已经追踪到最底层了。在x86_64平台上,musl可以使用7个不同的函数进行系统调用。每个函数接受不同数量的参数。

每个函数都有一个__asm__指令,它可以将内联汇编代码嵌入到编译器的机器语言输出中。我们在向操作系统发出系统调用时设置了一些CPU寄存器并执行了syscall指令。然后,控制权转移到了内核,由后者读取我们的参数并执行系统调用。


内核

接下来,由Linux内核执行系统调用请求的操作。系统调用write告诉内核写入文件系统中的一个已打开的文件,或者写入一个流,而此处我们的操作属于后者。

系统调用write有3个参数:文件描述符、写入的缓冲区以及写入的字节数。musl使用的系统调用write略有不同,但此处我们只讨论write。

那么,我们到底写入到哪里呢?


视具体情况而定。

在这个例子中,我在GNOME终端模拟器中运行了hello程序。这款模拟器是一个图形应用程序,对于内核来说,它是一个伪终端(pty)。所以,内核将我们的消息Hello World保存在缓冲区中,模拟器运行时,读取缓冲区,然后再显示。终于讲完了。

当然,整个旅程还没有完全结束。模拟器必须将文本渲染成一帧(可以使用GPU来渲染),将此帧发送到X服务器/合成器,然后由后者将其与其他正在运行的应用程序组合在一起(也使用GPU),例如我当前用来撰写这篇文章的文本编辑器,然后将其发送回内核,最后显示出来。

长呼一口气。我略过了很多不太重要的细节,而且你的环境可能完全不同。比如你选择远程登录,那么内核会将的文本发送到sshd,然后将通过互联网发送的数据包(加密)发送回内核。或者,你使用的是物理终端,连接到串口转USB适配器,那么内核必须将文本放入USB数据包,并将其发送到下一级。再或者,你使用了帧缓冲控制台,这是在没有安装GUI的情况下与操作系统交互的默认方式。在这种情况下,内核必须将文本渲染成一帧,并将其输出到显示器。

重点在于,接下来的操作有很多可能性,而且具体的细节并不重要。因为你的Hello World消息只是一个系统调用,来自一个程序,此时此刻你的计算机上有数百万个系统调用和成千上万个程序在运行,而Hello World只是其中最微不足道的一个。


总结

现如今,硬件上的现代软件系统是如此错综复杂,想方设法完整地理解计算机上的一个小操作完全没有意义。显然,为了解释这个小程序所做的一切操作,我略过了很多内容。我没有提到所有边缘情况、附加信息以及计算机执行的其他任务。我也没有解释内核是如何工作的。

问:那么,Hello World 程序究竟是如何工作的呢?

答:一言难尽……

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
1940年,八路军在大娘家喝粥,班长突然说:先别吃,这小米不对劲

1940年,八路军在大娘家喝粥,班长突然说:先别吃,这小米不对劲

百年历史老号
2024-05-25 07:45:44
解放军重型合成旅实力几何?投资200亿,40分钟“歼灭”俄机步团

解放军重型合成旅实力几何?投资200亿,40分钟“歼灭”俄机步团

浅梦墨晞
2024-05-29 12:06:29
最新排名!加拿大女排1-3被扣大分,奥运席位不稳!荷兰女排反超

最新排名!加拿大女排1-3被扣大分,奥运席位不稳!荷兰女排反超

祝晓塬
2024-05-29 10:45:47
又有5人被抓!巨雷彻底炸响!比恒大还多1.14万亿?15万名投资者欲哭无泪…

又有5人被抓!巨雷彻底炸响!比恒大还多1.14万亿?15万名投资者欲哭无泪…

保险课堂
2024-05-28 23:28:07
性生活对女性的好处

性生活对女性的好处

今日养生之道
2024-05-27 21:19:06
中泰之战还没开打!泰国队就官宣重要决定,让国足下不来台

中泰之战还没开打!泰国队就官宣重要决定,让国足下不来台

罗掌柜体育
2024-05-28 12:21:22
ESA向世界宣布!6年内有货运飞船,撕毁中国载人合作协议:自己干

ESA向世界宣布!6年内有货运飞船,撕毁中国载人合作协议:自己干

环球科学猫
2024-05-27 07:13:16
许可馨:国人喜欢将苦难视为一种美德,简直是自轻自贱

许可馨:国人喜欢将苦难视为一种美德,简直是自轻自贱

铁山学者
2024-05-21 10:48:05
前华人首富赵长鹏:痛快交531亿罚款,成全球最相信美国的大冤种

前华人首富赵长鹏:痛快交531亿罚款,成全球最相信美国的大冤种

毒sir财经
2024-05-28 17:01:37
曝拉爵确定曼联首签人选,欲4000万趁火打劫!马奎尔求见争取留队

曝拉爵确定曼联首签人选,欲4000万趁火打劫!马奎尔求见争取留队

罗米的曼联博客
2024-05-29 08:57:23
费翔母亲毕丽娜:传言是大陆特工,17岁去台湾,两任丈夫身份特殊

费翔母亲毕丽娜:传言是大陆特工,17岁去台湾,两任丈夫身份特殊

阿胡
2024-05-20 12:44:22
长公主被家暴到不能生了

长公主被家暴到不能生了

毒舌扒姨太
2024-05-27 22:24:51
中方大批取消美订单后,拜登立马转变态度,终于意识到离不开中国

中方大批取消美订单后,拜登立马转变态度,终于意识到离不开中国

在逃汤姆猫
2024-05-29 06:39:26
华子:我告诉牛仔队的帕森斯 G6回来时我会给他带新签名鞋

华子:我告诉牛仔队的帕森斯 G6回来时我会给他带新签名鞋

直播吧
2024-05-29 12:10:15
将近40岁满脸褶,却尬演18岁少女,是谁给了她“强行装嫩”的勇气

将近40岁满脸褶,却尬演18岁少女,是谁给了她“强行装嫩”的勇气

娱乐圈十三太保
2024-05-28 13:56:53
小米高管全家福更新,林斌卢伟冰成左膀右臂,雷军真是捡到宝了

小米高管全家福更新,林斌卢伟冰成左膀右臂,雷军真是捡到宝了

龚进辉
2024-05-29 09:55:25
隆多爆出猛料,称当年在独行侠并非诈伤,而是管理层让他装病

隆多爆出猛料,称当年在独行侠并非诈伤,而是管理层让他装病

奕辰说球
2024-05-29 10:48:10
1936年,江青和唐纳的结婚照,看江青的长相,有点不一样

1936年,江青和唐纳的结婚照,看江青的长相,有点不一样

讲故事的大翠花花
2024-05-25 09:04:21
长江一支流恶臭熏天、鱼虾死亡,当地干部: 没有必要做毒性分析,喝茅台也能喝死人

长江一支流恶臭熏天、鱼虾死亡,当地干部: 没有必要做毒性分析,喝茅台也能喝死人

封面新闻
2024-05-28 18:50:15
为什么很多人怀念毛主席?读完这首词,你就明白了!

为什么很多人怀念毛主席?读完这首词,你就明白了!

李舟
2024-04-24 19:05:27
2024-05-29 13:42:44
CSDN
CSDN
成就一亿技术人
24682文章数 241811关注度
往期回顾 全部

科技要闻

王传福再放狠话,燃油车要成“非主流”

头条要闻

外国女孩被中国父母收养走红网络 能说一口流利河南话

头条要闻

外国女孩被中国父母收养走红网络 能说一口流利河南话

体育要闻

阿根廷一代神锋,击碎了沙特的金元足球梦

娱乐要闻

张若昀怎么剧外比剧内更惨兮兮…

财经要闻

东方通收购藏雷 花6亿买来"业绩变脸"

汽车要闻

新哈弗H6苦练内功 向燃油车绝缘智能SAY NO

态度原创

游戏
教育
亲子
本地
公开课

Faker名人堂皮肤卖3500,外国网友整了个大活:我们决定永Ban狐狸

教育要闻

再不看来不及了!高考日语阅读题型及解题关键剖析!

亲子要闻

家有萌娃,萌娃看电视模仿跳水!

本地新闻

食味印象|歙县限定!枇杷味儿的清甜初夏

公开课

近视只是视力差?小心并发症

无障碍浏览 进入关怀版