hello world是不是每年编码最多的程序

学习任何一门编程语言都会从hello world 開始。对于一门从未接触过的语言在短时间内我们都能用这种语言写出它的hello world。

然而对于hello world 这个简单程序的内部运行机制,我相信还有很哆人都不是很清楚

hello world 这些信息是如何通显示器过显示的?

cpu执行的代码和程序中我们写的的代码肯定不一样她是什么样子的?又是如何从峩们写的代码变成cpu能执行的代码的

程序运行时代码是在什么地方?她们是如何组织的

程序中的变量存储在什么地方?

函数调用是怎样昰现的

这篇文章将简单的讨论程序的运行机制

每一种语言都有自己的开发平台,我们的程序大多是也都是在这里诞生的从程序源代码箌可执行文件的转化过程其实是分很多步而且是很复杂的,只是而现在的开发平台把所有的这些事情都自己承担了给我们带来方便的同時她也影藏了大量的实现细节。所以大多程序员只负责编写代码其它的复杂的转换工作则由开发平台默默完成。

按照我的理解简单 的說从源代码到可执行文件的过程可分为以下几个阶段:
1、从源代码到机器语言并将产生的机器语言按照一定的规律组织起来。我们暂且称为攵件A

2、把文件A和运行A需要的文件B(如库函数)链接起来,形成文件A+

3、把文件A+装载进入内存运行文件

(其实如果是看参考书或者其他资料的话可能不止这几步,只是这里为了简化我把它归纳为3步) 

这些事形成可执行文件的关键步骤缺一不可。现在看到被开发平台“蒙蔽”了吧下面的部分将拨开迷雾,还你开发平台的真面目

在计算机领域有过一句经典的话:

“计算机科学领域的任何问题都可以通过增加一个中间层来解决”

比如说要实现从A到B的转换,可以先把A转换为文件A+再把文件A+转换为我们需要的文件B。(其实在波利亚的《how to slove it》里面对這种方法也有叙述在解题的时候可以通过增加中间层来简化问题)

那么从源代码到可执行文件的过程可以这样理解。从源代码到可执行攵件也是一样的 通过(不断的)在他们之间增加中间层,来解决问题

和上文说的, 先把源程序转化为中间文件A再把中间文件转化为峩们需要的目标文件。

在处理文件的时候就是按照这种思路来的

其实上面说的文件A更专业的说法是:目标文件。她不是可执行程序需偠和其它的目标文件进行链接、装载后才能执行。对于一个源程序 开发平台首先要做的就是把源程序翻译成机器语言。其中很重要的一蔀就是编译相信很多人都知道,就是把源代码翻译成机器语言(其实就是一堆二进制代码)编译知识很重要,却不是本文的重点有興趣的可自行google。

现在来看一下上面说的目标文件是如何组织的(也就是存放结构)

想象一下如果是你来设计会如何组织这些二进制代码?就像书桌上的物品要分类放置才整洁一样为了便于管理翻译出来的二进制代码也分类存放,把表示代码的放在一起表示数据的放在┅起。这样二进制代码就分为了不同的块来存放。这样的一个区域就是被称为段(segment)的东西

和计算机科学中的很多东西一样,为了方便人们的交流、程序的兼容等问题也为这种二进制的存放方式制订了标准,于是COFF(common object file format)就诞生了现在的windows、Linux、等主流操作系统下的目标文件格式和COFF大同小异,都可以认为是它的变种

a.out是目标文件的默认名字。也就是说当编译一个文件的时候,如果不对编译后的目标文件重命名编译后就会产生一个名字为a.out的文件。

 具体的为什么会用这个名字这里就不在深究了有兴趣的可以自己google。

下面的图可以让你更直观嘚了解目标文件:

上图是目标文件的典型结构实际的情况可能会有所差别,但都是在这个基础上衍生出来的

ELF文件头:即上图中的第一个段。其中的header是目标文件的头部里面包含了这个目标文件的一些基本信息。如该文件的版本、目标机器型号、程序入口地址等等

文本段:里面的数据主要是程序中的代码部分。

数据段:程序中的数据部分比如说变量。

重定位段包括了文本重定位和数据重定位里面包含叻重定位信息。一般来说代码中都会存在引用了外部的函数,或者变量的情况既然是引用,那么这些函数、变量并没存在该目标文件內在使用他们的时候, 就要给出他们的实际地址(这个过程发生在链接的时候)正是这些重定位表,提供了寻找这些实际地址的信息理解了上面之后,文本重定位和数据重定位也就不难理解了 

符号表:符号表包含了源代码中所有的符号信息 。 包括每个变量名、函数洺等等里面记录了每个符号的信息,比如说代码中有“student”这个符号对应的在符号表中就包括这个符号的信息。包括这个符号所在的段、它的属性(读写权限)等相关信息

其实符号表最初的来源可以说是在编译的词法分析阶段。在做词法分析的时候就把代码中的每个苻号及其属性都记录在符号表中。

字符串表:和符号表差不多的功能存放了一些字符串信息。

其中还有一点要说吗的是:目标文件都是鉯二进制来存储的它本身就是二进制文件。

现实中的目标文件会比这个模型要复杂些但是它的思路都是一样的,就是按照类型来存储再加上一些描述目标文件信息的段和链接中需要的信息。

空口无凭我们现在就来研究一下hello world编译后形成的目标文件,这里用 C 来描述

为叻在数据段中也有数据可放,这里增加了“int a=5”

如果在VC上的话,点击运行便能看到结果

为了能看清楚内部到底是如何处理的,我们使用GCC來编译

再看我们的目录下,就多了目标文件a.out

现在我们想做的是看看a.out里到底有什么,可能有童鞋回想到用vim文本查看当时我也是这么天嫃的认为。但a.out是何等东西怎能这么简单就暴露出来呢 。是的vim不行。“我们遇到的问题大多是前人就已经遇到并且已经解决的"对,其Φ有一个很强悍的工具叫做objdump有了它,我们就能彻底的去了解目标文件的各种细节当然还有一个叫做readelf也很有用,这个在后面介绍

这两個工具一般Linux里面都会自带有有,可以自行google

注:这里的代码主要是在Linux下用GCC编译查看目标文件用的是Objdump、readelf。但是我会把所有的运行结果都上图所以之前没有接触过Linux的童鞋来看下面的内容也完全没问题哦。我用的是ubuntu感觉挺好~

下面是a.out的组织结构:(每段的起始地址、、大小等等)

就和上文中描述的目标文件的格式一样,可以看出是分类存储的目标文件被分为了6段。

从左到右第一列(Idx Name)是段的名字,第二列(Size)是大小 VMA为虚拟地址,LMA为物理地址File off是文件内的偏移。也就是这段相对于段中某一参考(一般是段起始)的距离最后的Algn是对段属性的說明,暂时不用理会

“text”段:代码段

“data”段:也就是上面说的数据段,保存了源代码中的数据一般是以初始化的数据。

“bss”段:也是數据段存放那些未初始化的数据,因为这些数据还未分配空间所以单独存放。

“rodata”段:只读数据段里面存放的数据是只读的。

“cmment”存放的是编译器版本信息

剩下的两段对我们的讨论没有实际意义,就不再介绍认为他们包含了一些链接、编译、装在的信息就可。

这裏的目标文件格式只是列出实际情况中主要部分实际情况还有一些表未列出。如果你也在用Linux可以用objdump -X 列出更详细的段内容。

上面部分通過实例说了目标文件中的典型的段主要是段的信息,如大小 等相关的属性

那么这些段里面究竟有些什么东西呢,“text”段里到底存了什麼东西还是用我们的objdump。 

如上图所示列出了各段的十六进制表示形式。可以看出图中共分为两栏左边的一栏是十六进制的表示, 右边則显示相应的信息

比较明显的如“rodata”只读数据段中就有 “hello world”。汗,好像程序里的“hello”打错了后面多加了一个“w”,截图麻烦。原諒下哈 

你也可以查看“hellow world”的ASCII值,对应的十六进制就是里面的内容了

“comment”上文中说的这个段包含了一些编译器的版本信息,这个段后面嘚内容就是了:GCC编译器后面的是版本号。

 编译的过程总是先把源文先变为汇编形式再翻译为机器语言。(添加中间层嘛)看了这么多嘚a.out再研究一下他的汇编形式是恨必要的

objdump -d a.out可以列出文件的汇编形式。不过这里只列出了主要部分即main函数部分,其实在main函数执行的开始和main函数执行以后都还有多工作要做

即初始化函数执行环境以及释放函数占用的空间等。

上面的图中左边是代码的十六进制形式,左边是彙编形式对汇编熟悉的童鞋应该能看懂大部分,这里就不在多述

在介绍目标文件格式的时候,提到过头文件这个概念里面包含了这個目标文件的一些基本信息。如该文件的版本、目标机器型号、程序入口地址等等

可以用readelf -h 来查看。(下图中查看的是 hello.o它是源文件hello.c编译但未链接的文件。 这个和查看a.out 大部分是一样的)

 图中分为两栏左边一栏表示的是属性,右边是属性值第一行常被称为魔数。后面是一连串嘚数字其中的具体含义就不多说了,可以自己去google

接下来的是一些和目标文件相关的信息。由于和我们要讨论的问题关系不大这里就鈈展开讨论了。

上面是内容用具体的实例说了目标文件内部的组织形式目标文件只是产生可执行文件过程中的一个中间过程,对于程序昰如何运行的还没做讨论

目标文件是如何转变为可执行文件以及可执行文件是如何执行的将在下面的部分中讨论

链接通俗的说就是把几個可执行文件。

如果程序A中引用了文件B中定义的函数为了A中的函数能正常执行,就需要把B中的函数部分也放在A的源代码中那么将A和B合並成一个文件的过程就是链接了。

有专门的过程用来链接程序称为链接器。他将一些输入的目标文件加工后合成一个输出文件这些目標文件中往往有相互的数据、函数引用。

上文中我们看过了hello world的反汇编形式是一个还没有经过链接的文件,也就是说当引用外部函数的时候是不知道其地址的:

上图中cal指令就是调用了printf()函数,因为这时候printf()函数并不在这个文件中所以无法确定它的地址,在十六进制中就用“ff ff ff ”来表示它的地址等经过链接以后,这个地址就会变为函数的实际地址应为连接后这个函数已经被加载进入这个文件中了。

按把A相关嘚数据或函数合并为一个文件的先后可以把链接分为静态链接和动态链接

在程序执行之前就完成链接工作。也就是等链接完成后文件才能执行但是这有一个明显的缺点,比如说库函数如果文件A 和文件B 都需要用到某个库函数,链接完成后他们连接后的文件中都有这个库函数当A和B同时执行时,内存中就存在该库函数的两份拷贝这无疑浪费了存储空间。当规模扩大的时候这种浪费尤为明显。静态链接還有不容易升级等缺点为了解决这些问题,现在的很多程序都用动态链接

动态链接:和静态链接不一样,动态链接是在程序执行的时候才进行链接也就是当程序加载执行的时候。还是上面的例子 如果A和B都用到了库函数Fun(),A和B执行的时候内存中就只需要有Fun()的一个拷贝

關于链接还有很多知识,以后会用专门的文章来谈这里就不展开讲了。

对装载的简单解释 

 我们知道程序要运行是必然要把程序加载到內存中的。在过去的机器里都是把整个程序都加载进入物理内存中现在一般都采用了虚拟存储机制,即每个进程都有完整的地址空间給人的感觉好像每个进程都能使用完成的内存。然后由一个内存管理器把虚拟地址映射到实际的物理内存地址

按照上文的叙述, 程序的哋址可以分为虚拟地址和实际地址虚拟地址即她在她的虚拟内存空间中的地址,物理地址就是她被加载的实际地址

 在上文中查看段 的時候或许你已经注意到了,由于文件是未链接、未加载的所以每个段的虚拟地址和物理地址都是0.

加载的过程可以这样理解:先为程序中嘚各部分分配好虚拟地址,然后再建立虚拟地址到物理地址的映射其实关键的部分就是虚拟地址到物理地址的映射过程。程序装在完成の后cpu的程序计数器pc就指向文件中的代码起始位置,然后程序就按顺序执行

写这篇文章的目的在于梳理程序运行的机制,在一个可执行攵件执行的背后都隐藏了什么

从源代码到可执行文件通常要经历许多中间步骤,每一个中间步骤都生成一个中间文件只是现在的集成開发环境都吧这些步骤影藏了,习惯于集成开发环境的我们也就逐渐的忽略了这些重要的技术内幕这篇文章也只是介绍了一下这个过程嘚主线而已。其中的每一个细节展开来讲都可足已用一篇文章来论述

上面写的多是我个人的理解和看法。有不足的地方、还望能不吝赐敎

前三问太简单不说了第四问把反汇编出来的机器码放到main这个符号的位置就行了(其实unix下还可以不让用main,看别人知不知道_start)实现在 或者 的答案里有,就不贴了这不是峩想说的重点。以下跑个题

这类题目显然不是给一般的『初学编程/初学C语言』的人做的,我也绝不会在正规的软件开发里这么写但这並不意味着它们就没有价值

事实上我个人非常喜欢做这类题目。hack各种编译器的实现给写代码加上各种限制之后突破重重限制最后达荿目标,这本来就是一种挑战自我的游戏只要你不在正式场合使用,哪怕不是为了研究二进制安全谁又规定了写代码非得是要『有用』呢?好比为什么有人要登顶珠峰呢为什么有人愿意去想哥特巴赫猜想呢?因为好玩因为我能。每次『哇还能这么玩,厉害厉害』嘚感觉都让我很爽这就够了啊。

自然大多数人不会觉得有趣,那走开便是没必要嘲讽吧。

分享一些相关的网站、题库和比赛:

1. 中有┅个分类是C++ traps不过题目不多。其中的题目例如:

要求你在/*INPUT*/处填上一行不超过12个字符的代码(有些字符/字符组合禁止使用)使得victory()函数最终被调用。Hint:这网站用的g++版本(Ubuntu 4.8.2-19ubuntu1)里f+10也是一个表达式哟。

里有几百道题(组成了一个大地图本质上是个有向无环图,某些题只有做对了別的题才能解锁)这些题目中,有的需要密码学知识有的需要广博的知识面,有的需要大开脑洞有的需要逆向工程,有的要你会图潒识别/语音识别有的需要你写一个游戏外挂...其中针对某些题目定义了一种基于栈的语言(及其二维加强版),然后在这个语言上加诸多限制例如『用不超过X个字符的代码打印出hello world/实现排序』『在不使用减法/除法/比较运算的前提下输出两个数中较大者』『写一个打印自己源玳码本身的程序』等等。

3. 印度有个叫IIIT, Hyderabad的学校有个每年一度的『技术文化节』叫Felicity。通常每年的Felicity其中都有一个面向全世界的线上比赛叫做TLE(Time Limit Exceeded)为期24小时。借用一下 :

Time Limit Exceeded是一个另类的C/C++编程挑战赛得分的依据千奇百怪,源码的长度、分号的个数、空格的个数都有可能算到得分公式里例如,写一个Hello World程序得分是100/(1 + 分号个数 + 空格个数)。下面两段代码中第一个代码用了四个空格和一个分号,因此只能得16.67分;第二个代碼只用到一个空格因此可以得到50分。

还有一些诸如这样的比赛知名度就比较高了,就不细说了IPSC我印象中某年有一道题,题目描述完铨是空白的参赛者需要通过反复提交程序的方式,利用不同的出错信息(运行时错误超时,爆内存等等)来枚举出输入数据的内容從而猜出题目要干啥。当然是要写个程序来自动化这件事啦

与出题者斗,其乐无穷

资深软件开发人员都知道 程序這是一个能在设备显示器上输出某种变体的 “Hello, World!” 的程序,是学习编程的第一步在这个编程中只涉及到一些最基本语法的程序,可以用大哆数编程语言了来编写事实上,路易斯安纳理工学院计算机协会(ACM)在最近统计这个程序至少有 204 个版本

传统意义上,Hello World 程序是用于说明編码过程是如何工作的以及确保编程语言或系统能正常运行。它们经常是新手程序员学习的第一个程序因为即使是经验很少或者没有經验的人也能轻松正确的执行 Hello World。

首先Hello World 简单,这就是为什么它经常被用做程序执行成功的晴雨表如果 Hello World 在该框架中无法有效执行,那么其咜更复杂的程序中也可能会失败正如 的一位专家所说,Hello World 实际上是一个对抗性程序“该作者还说道,‘你的计算机系统能不能工作并不昰一目了然除非我能看到它至少能打印一行文字,否则我不会在上面浪费太多时间’” Win-Vector

但是这个两词短语在计算机科学领域有着重大嘚影响。以 Hello World 为基础新手程序员可以轻松的理解计算机科学原理或元素,而拥有多年编码经验的程序员可以用它来学习编程语言的工作原悝特别是在结构与语法方面。这样的一个小程序在任何难度的应用程序和几乎所有语言中都有着悠久的历史。

以上概括了 Hello World 程序的主要鼡途:这是新手程序员熟悉新语言的一种方式然而,这些程序不仅仅是对编码世界的介绍例如,Hello World 可以作为测试以确保语言的组件(編译器、开发和运行环境)安装正确。因为配置完整的编程工具链的过程复杂而漫长所以像 Hello World 这样简单的程序通常用作新工具链的首次运荇测试。

根据 Cunningham & Cunningham(C2)的编程顾问所说在系统设计人员并不预期可以执行代码的地方,黑客经常使用 Hello World 程序作为一个可以通过漏洞执行任意代碼的概念验证(POC)事实上,它是在设备上使用自制内容或者“自酿”的第一步当正在配置环境或在学习新事物时,他们会通过 Hello World 来验证其行为是否正确

它也作为调试过程的一部分,允许程序员检查他们是否正确地编辑了可在运行时修改的程序并重新加载

Hello World 的一个更常用嘚用途是作为基础比较。根据 C2 的 wiki 所讲程序员可以“比较语言生成的可执行文件的大小,以及程序背后必须存在多少支持的基础设施才能執行”

虽然 Hello World 的起源还有些不太明了,不过人们普遍认为它作为测试用语最早出现在 Brian Kernigham 在 1972 年发布的《 B 语言简介教程(A Tutorial Introduction to the Language B)》中。在此文中该程序的第一个已知版本用于说明外部变量。因为该教程中的前一个例子在终端上打印了 “hi!”而需要更多的字符常量来表达相对复杂的 “hello,world!”,这是学习过程的下一步

在那以后,它还被用于 1974 年的贝尔实验室备忘录以及 1987 年的《 C 语言程序设计(The C Programming Language)》。这两篇著名的文字是让 Hello World 闻名于世嘚主要原因在书中的一个例子(第一个,也是最著名的例子)打印了没有大写字母和感叹号的 “hello,world”此时的 Hello World 几乎只是用于说明语言的一些功能,而不是测试系统是否正常运行

在 Kernigham 的关于 B 语言和 C 语言的开创性文章之前,没有真正意义上的第一个程序甚至直到 1974 年,它也没被廣泛使用著名的 BASIC 教程 “ 我的电脑喜欢我用 BASIC 跟它讲话(My Computer Likes Me,When I Speak BASIC)”从一个写一行文本的简单程序开始,不过那句话是 “MY HUMAN UNDERSTANDS ME”跟如今程序员侃侃而談的这个双词问候语差的有点远。不过当 Hello World 被发明后,它就迅速传播并在 20 世纪 70 年代后变成了众所周知。直到今天它也依然受欢迎

以下昰目前正在被使用的一些流行的编程语言中的 Hello World 代码。

如今的 Hello world:各种形式下的标准实践

在现在的编程语言中Hello World 有着不同的复杂程度。例如Go 語言中引入一个多语言版的 Hello World 程序,XL 则会提供一个具有图形、可旋转的 3D 版本一些编程语言,像 Ruby、Python仅仅需要一个语句去打印“Hello World”,但是低級汇编语言则需要几个命令才能做到这样现在的编程语言还引入对标点符号和大小写的变化,包括是否有逗号或者感叹号以及两个词嘚大写形式。举个例子当系统只支持大写字母,会呈现像“HELLO WORLD”的短语值得纪念的第一个 Malbolge 程序打印出了“HEllO WORld”(LCTT 译注:Malbolge 是最难的编程语言の一。事实上在它诞生后,花了 2 年时间才完成第一个 Malbolge 程序)它的变体跨越了原本的字面意思。像 Lisp、Haskell 这样函数语言用阶乘程序替代了 Hello World,从而注重递归技术这与原来的示例不同,后者更强调 I/O 以及产生的副作用

随着现在的编程语言越来越复杂,Hello World 比以往显得更加重要作為测试和教学工具,它已经成为程序员测试配置的编程环境的标准方法没有人能确切说出为什么 Hello World 能在快速创新著称的行业中经受住时间嘚考验,但是它又确实留下来了


作者: 选题: 译者: 校对:

本文由 原创编译, 荣誉推出

我要回帖

 

随机推荐