标  题: Hello World 背后的真实故事

发信站: 南京大学小百合站 (Thu Feb 28 16:15:49 2008)

The True Story of Hello World

Hello World 背后的真实故事

(至少是大部分故事)

我们计算机科学专业的大多数学生至少都接触过一回著名的 "Hello World" 程序。相比一

个典型的应用程序——几乎总是有一个带网络连接的图形用户界面,"Hello World" 程序

看起来只是一段很简单无趣的代码。不过,许多计算机科学专业的学生其实并不了解它背

后的真实故事。这个练习的目的就是利用对 "Hello World" 的生存周期的分析来帮助你揭

开它神秘的面纱。

源代码

让我们先看一下 Hello World 的源代码:

1. #include

2. int main(void)

3. {

4.     printf("Hello World!

");

5.     return 0;

6.

7. }

第 1 行指示编译器去包含调用 C 语言库(libc)函数 printf 所需要的头文件声明。

第 3 行声明了 main 函数,看起来好像是我们程序的入口点(在后面我们将看到,其实它

不是)。它被声明为一个不带参数(我们这里不准备理会命令行参数)且会返回一个整型

值给它的父进程(在我们的例子里是 shell)的函数。顺便说一下,shell 在调用程序时

对其返回值有个约定:子进程在结束时必须返回一个 8 比特数来代表它的状态:0 代表正

常结束,0~128 中间的数代表进程检测到的异常终止,大于 128 的数值代表由信号引起的

终止。

从第 4 行到第 8 行构成了 main 函数的实现,即调用 C 语言库函数 printf 输出 "Hel

lo World!

" 字符串,在结束时返回 0 给它的父进程。

简单,非常简单!

编译

现在让我们看看 "Hello World" 的编译过程。在下面的讨论中,我们将使用非常流行的

GNU 编译器(gcc)和它的二进制辅助工具(binutils)。我们可以使用下面命令来编译我

们的程序:

# gcc -Os -c hello.c

这样就生成了目标文件 hello.o,来看一下它的属性:

# file hello.o

hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripp

ed

给出的信息告诉我们 hello.o 是个可重定位的目标文件(relocatable),为 IA-32(Int

el Architecture 32) 平台编译(在这个练习中我使用了一台标准 PC),保存为 ELF(Ex

ecutable and Linking Format) 文件格式,并且包含着符号表(not stripped)。

顺便:

# objdump -hrt hello.o

hello.o:     file format elf32-i386

Sections:

Idx Name          Size     VMA       LMA       File off  Algn

0 .text         00000011 00000000  00000000  00000034  2**2

CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

1 .data         00000000 00000000  00000000  00000048  2**2

CONTENTS, ALLOC, LOAD, DATA

2 .bss          00000000 00000000  00000000  00000048  2**2

ALLOC

3 .rodata.str1.1 0000000d  00000000  00000000  00000048 2**0

CONTENTS, ALLOC, LOAD, READONLY, DATA

4 .comment      00000033  00000000 00000000  00000055  2**0

CONTENTS, READONLY

SYMBOL TABLE:

00000000 l    df *ABS*  00000000 hello.c

00000000 l    d  .text  00000000

00000000 l    d  .data  00000000

00000000 l    d  .bss   00000000

00000000 l    d  .rodata.str1.1 00000000

00000000 l    d  .comment       00000000

00000000 g    F  .text  00000011 main

00000000         *UND*  00000000 puts

RELOCATION RECORDS FOR [.text]:

OFFSET   TYPE              VALUE

00000004 R_386_32          .rodata.str1.1

00000009 R_386_PC32        puts

这告诉我们 hello.o 有 5 个段:

(译者注:在下面的解释中读者要分清什么是 ELF 文件中的段(section)和进程中的段

(segment)。比如 .text 是 ELF 文件中的段名,当程序被加载到内存中之后,.text 段

构成了程序的可执行代码段。其实有时候在中文环境下也称 .text 段为代码段,要根据上

下文分清它代表的意思。)

1. .text: 这是 "Hello World" 编译生成的可执行代码,也就是说这个程序对应的 I

A-32 指令序列。.text 段将被加载程序用来初始化进程的代码段。

2. .data:"Hello World" 的程序里既没有初始化的全局变量也没有初始化的静态局部

变量,所以这个段是空的。否则,这个段应该包含变量的初始值,运行前被装载到进程的

数据段。

3. .bss: "Hello World" 也没有任何未初始化的全局或者局部变量,所以这个段也是

空的。否则,这个段指示的是,在进程的数据段中除了上文的 .data 段内容,还有多少字

节应该被分配并赋 0。

4. .rodata: 这个段包含着被标记为只读 "Hello World!

" 字符串。很多操作系统并

不支持进程(运行的程序)有只读数据段,所以 .rodata 段的内容既可以被装载到进程的

代码段(因为它是只读的),也可以被装载到进程的数据段(因为它是数据)。因为编译

器并不知道你的操作系统所使用的策略,所以它额外生成了一个 ELF 文件段。

5. .comment:这个段包含着 33 字节的注释。因为我们在代码中没有写任何注释,所

以我们无法追溯它的来源。不过我们将很快在下面看到它是怎么来的。

它也给我们展示了一个符号表(symbol table),其中符号 main 的地址被设置为 00000

000,符号 puts 未定义。此外,重定位表(relocation table)告诉我们怎么样去在 .t

ext 段中去重定位对其它段内容的引用。第一个可重定位的符号对应于 .rodata 中的 "H

ello World!

" 字符串,第二个可重定位符号 puts,代表了使用 printf 所产生的对一

个 libc 库函数的调用。为了更好的理解 hello.o 的内容,让我们来看看它的汇编代码:

1. # gcc -Os -S hello.c -o -

2.         .file   "hello.c"

3.         .section       .rodata.str1.1,"aMS",@progbits,1

4. .LC0:

5.         .string "Hello World!"

6.         .text

7.         .align 2

8. .globl main

9.         .type   main,@function

10. main:

11.         pushl   %ebp

12.         movl    %esp, %ebp

13.         pushl   $.LC0

14.         call    puts

15.         xorl    %eax, %eax

16.         leave

17.         ret

18. .Lfe1:

19.         .size   n,.Lfe1-n

20.         .ident  "GCC: (GNU) 3.2 20020903 (Red Hat Linux 8.0 3.2-7)"

从汇编代码中我们可以清楚的看到 ELF 段标记是怎么来的。比如,.text 段是 32 位对齐

的(第 7 行)。它也揭示了 .comment 段是从哪儿来的(第 20 行)。因为我们使用 pr

intf 来打印一个字符串,并且我们要求我们优秀的编译器对生成的代码进行优化(-Os)

,编译器用(应该更快的) puts 调用来取代 printf 调用。不幸的是,我们后面将会看

到我们的 libc 库的实现会使这种优化变得没什么用。

那么这段汇编代码会生成什么代码呢?没什么意外之处:使用标志字符串地址的标号 .LC

O 作为参数的一个对 puts 库函数的简单调用。

连接

下面让我们看一下 hello.o 转化为可执行文件的过程。可能会有人觉得用下面的命令就可

以了:

# ld -o hello hello.o -lc

ld: warning: cannot find entry symbol _start; defaulting to 08048184

不过,那个警告是什么意思?尝试运行一下!

是的,hello 程序不工作。让我们回到那个警告:它告诉我们连接器(ld)不能找到我们

程序的入口点 _start。不过 main 难道不是入口点吗?简短的来说,从程序员的角度来看

main 可能是一个 C 程序的入口点。但实际上,在调用 main 之前,一个进程已经执行了

一大堆代码来“为可执行程序清理房间”。我们通常情况下从编译器或者操作系统提供者

那里得到这些外壳程序(surrounding code,译者注:比如 CRT)。

下面让我们试试这个命令:

# ld -static -o hello -L`gcc -print-file-name=` /usr/lib/crt1.o /usr/lib/crti.

o hello.o /usr/lib/crtn.o -lc -lgcc

现在我们可以得到一个真正的可执行文件了。使用静态连接(static linking)有两个原

因:一,在这里我不想深入去讨论动态连接库(dynamic libraries)是怎么工作的;二,

我想让你看看在我们库(libc 和 libgcc)的实现中,有多少不必要的代码将被添加到 "

Hello World" 程序中。试一下这个命令:

# find hello.c hello.o hello -printf "%f\t%s

"

hello.c 84

hello.o 788

hello   445506

你也可以尝试 "nm hello" 和 "objdump -d hello" 命令来得到什么东西被连接到了可执

行文件中。

想了解动态连接的更多内容,请参考 Program Library HOWTO http://www.tldp.org/HOW

TO/Program-Library-HOWTO/

装载和运行

在一个遵循 POSIX(Portable Operating System Interface) 标准的操作系统(OS)上,

装载一个程序是由父进程发起 fork 系统调用来复制自己,然后刚生成的子进程发起 exe

cve 系统调用来装载和执行要运行的程序组成的。无论何时你在 shell 中敲入一个外部命

令,这个过程都会被实施。你可以使用 truss 或者 trace 命令来验证一下:

# strace -i hello > /dev/null

[????????] execve("./hello", ["hello"], ) = 0

...

[08053d44] write(1, "Hello World!

", 13) = 13

...

[0804e7ad] _exit(0) = ?

除了 execve 系统调用,上面的输出展示了打印函数 puts 中的 write 系统调用,和用

main 的返回值(0)作为参数的 exit 系统调用。

为了解 execve 实施的装载过程背后的细节,让我们看一下我们的 ELF 可执行文件:

# readelf -l hello

Elf file type is EXEC (Executable file)

Entry point 0x80480e0

There are 3 program headers, starting at offset 52

Program Headers:

Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

LOAD           0x000000 0x08048000 0x08048000 0x55dac 0x55dac R E 0x1000

LOAD           0x055dc0 0x0809edc0 0x0809edc0 0x01df4 0x03240 RW  0x1000

NOTE           0x000094 0x08048094 0x08048094 0x00020 0x00020 R   0x4

Section to Segment mapping:

Segment Sections...

00     .init .text .fini .rodata __libc_atexit __libc_subfreeres .note.ABI-

tag

01     .data .eh_frame .got .bss

02     .note.ABI-tag

输出显示了 hello 的整体结构。第一个程序头对应于进程的代码段,它将从文件偏移 0x

000000 处被装载到映射到进程地址空间的 0x08048000 地址的物理内存中(虚拟内存机制

)。代码段共有 0x55dac 字节大小而且必须按页对齐(0x1000, page-aligned)。这个段

将包含我们前面讨论过的 ELF 文件中的 .text 段和 .rodata 段的内容,再加上在连接过

程中生成的附加的段。正如我们预期,它被标志为:只读(R)和可执行(X),不过禁止写(W

)。

第二个程序头对应于进程的数据段。装载这个段到内存的方式和上面所提到的一样。不过

,需要注意的是,这个段占用的文件大小是 0x01df4 字节,而在内存中它占用了 0x0324

0 字节。这个差异主要归功于 .bss 段,它在内存中只需要被赋 0,所以不用在文件中出

现(译者注:文件中只需要知道它的起始地址和大小即可)。进程的数据段仍然需要按页

对齐(0x1000, page-aligned)并且将包含 .data 和 .bss 段。它将被标识为可读写(RW

)。第三个程序头是连接阶段产生的,和这里的讨论没有什么关系。

如果你有一个 proc 文件系统,当你得到 "Hello World" 时停止进程(提示: gdb,译者

注:用 gdb 设置断点),你可以用下面的命令检查一下是不是如上所说:

# cat /proc/`ps -C hello -o pid=`/maps

08048000-0809e000 r-xp 00000000 03:06 479202     .../hello

0809e000-080a1000 rw-p 00055000 03:06 479202     .../hello

080a1000-080a3000 rwxp 00000000 00:00 0

bffff000-c0000000 rwxp 00000000 00:00 0

第一个映射的区域是这个进程的代码段,第二个和第三个构成了数据段(data + bss + he

ap),第四个区域在 ELF 文件中没有对应的内容,是程序栈。更多和正在运行的 hello 进

程有关的信息可以用 GNU 程序:time, ps 和 /proc/pid/stat 得到。

程序终止

当 "Hello World" 程序运行到 main 函数中的 return 语句时,它向我们在段连接部分讨

论过的外壳函数传入了一个参数。这些函数中的某一个发起 exit 系统调用。这个 exit

系统调用将返回值转交给被 wait 系统调用阻塞的父进程。此外,它还要对终止的进程进

行清理,将其占用的资源还给操作系统。用下面命令我们可以追踪到部分过程:

# strace -e trace=process -f sh -c "hello; echo $?" > /dev/null

execve("/bin/sh", ["sh", "-c", "hello; echo 0"], ) = 0

fork()                                  = 8321

[pid  8320] wait4(-1,

[pid  8321] execve("./hello", ["hello"], ) = 0

[pid  8321] _exit(0)                    = ?

<... wait4 resumed> [WIFEXITED(s) && WEXITSTATUS(s)== 0], 0, NULL) = 8321

--- SIGCHLD (Child exited) ---

wait4(-1, 0xbffff06c, WNOHANG, NULL)    = -1 ECHILD (No child processes)

_exit(0)

结束

这个练习的目的是让计算机专业的新生注意这样一个事实:一个 Java Applet 的运行并不

是像魔法一样(无中生有的),即使在最简单的程序背后也有很多系统软件的支撑。如果

您觉得这篇文章有用并且想提供建议来改进它,请发电子邮件给我。mailto:guto@lisha.

ufsc.br

常见问题

这一节是为了回答学生们的常见问题。

* 什么是 "libgcc"? 为什么它在连接的时候被包含进来?

编译器内部的函数库,比如 libgcc,是用来实现目标平台没有直接实现的语言元素

。举个例子,C 语言的模运算符 ("%") 在某个平台上可能无法映射到一条汇编指令。可能

用一个函数调用实现比让编译器为其生成内嵌代码更受欢迎(特别是对一些内存受限的计

算机来说,比如微控制器)。很多其它的基本运算,包括除法、乘法、字符串处理(比如

memory copy)一般都会在这类函数库中实现

c语言什么意思000094,Hello World 背后的真实故事相关推荐

  1. cut the rope HTML 5版本背后的开发故事

    译者注:Cut the Rope 是一款人见人爱的小游戏.有一个开发团队将它改造成了HTML5版本.想看看他们在改造过程中的经验之谈吗?那就看下面由开发人员自己写的文章吧~ 启示 在IE9中作为一个H ...

  2. 程里人 | 写书背后那些不为人知的故事

    上周,<携程架构实践><携程人工智能实践>两本技术书籍上市,受到小伙伴们的热捧. 恰逢4月23日世界读书日当天,上市一周的<携程架构实践><携程人工智能实践& ...

  3. OceanBase数据库背后的传奇故事

    这两天被一则消息刷屏,<中国自己的数据库OceanBase超越甲骨文,登顶全球第一>↓ 蚂蚁金服自主研发的数据库OceanBase打破数据库基准性能测试的世界记录,成绩是前世界记录保持者. ...

  4. 移动信号突然变成无服务器,为什么移动4G信号突然变成H、E、G就无法上网, 背后的真实原因?...

    原标题:为什么移动4G信号突然变成H.E.G就无法上网, 背后的真实原因? 日常生活中经常会遇到这样的事,坐在车上手机信号栏刚刚还显示4G,突然变成字母H.E.G,或者4G后面多一个+,变成4G+.相 ...

  5. 华为p4用鸿蒙系统吗_为什么华为有自主研发的鸿蒙系统,却还要用安卓系统,背后的真实原因?...

    一直以来,拥有完全自主知识产权的国产操作系统都是无数国人的希望,然而其难度之大并非短期能实现的事情.毕竟目前像芯片.操作系统还是美国一家独大,在芯片领域,美国已占有全球一半以上的市场份额,中国每年进口 ...

  6. 我赢资讯10分钟程序化交易决策系统,把握涨跌背后的真实原因,选择最具价值的投资方向...

    我赢资讯10分钟程序化交易决策系统,把握涨跌背后的真实原因,选择最具价值的投资方向 开创专业投资战略思维,实现创造财富梦想.运用我赢资讯软件提高投资者专业素养和掌握机构操盘技能,建立程序化交易系统框架 ...

  7. 张越:每张脸背后都有故事

    与张越对话 汹涌的人潮,每张脸背后的故事. --罗大佑 9月12日中午1点,百盛的星巴克.张越比我到得早.虽然头天夜里,她刚从吉林录完节目赶回北京,又看电视新闻直到凌晨3点. 见面的一刻,昨夜电视屏幕 ...

  8. 夺命金背后的真实香港情况

    杜琪峰拍摄这部片子的时候,恐怕没有想到本片的上映期正好遇上"香港人大战内地人"这个尴尬时刻.不管编导用了多么繁复的"多线环形结构"来讲故事,都无碍其无比写实的风 ...

  9. 欧姆龙PLC ST语言6轴伺服RS232C通讯板CP1W-C IF0 真实项目程序,ST语言写的FB块

    欧姆龙PLC ST语言6轴伺服RS232C通讯板CP1W-C IF0 真实项目程序,ST语言写的FB块 PLC用是两台CP1H-X40DT-D配置4块RS232C通讯板CP1W-CIF01 触摸屏是N ...

  10. 联通4g满格但是网速慢_为什么手机4G信号明明是满格,网络却很慢,背后的真实原因?...

    原标题:为什么手机4G信号明明是满格,网络却很慢,背后的真实原因? 虽然国内的4G网络建设比较晚,但是随着国内三大运营商和手机厂商的大力推进,在短短的两三年时间里,就覆盖了国内大部分区域,4G网络如今 ...

最新文章

  1. 尝试自动重定向的次数太多_GoRod:基于DP协议的Web自动化和数据抓取工具
  2. js 实现文件导出、文件下载
  3. java将ascii数组转成unicode字串
  4. bzoj 1647: [Usaco2007 Open]Fliptile 翻格子游戏(枚举)
  5. 一个帮助你处理延迟,重复,循环操作的jQuery插件 - timing
  6. 《程序是怎样跑起来的》第七章
  7. Notepad++ 安装jsonview插件
  8. 2022年全球程序员平均薪资发布,中国排名很意外
  9. IOT语义互操作性之本体论 1
  10. 高德SDK 小车轨迹回放,速度、进度控制
  11. Apache DolphinScheduler 海豚调度器自定义时间参数
  12. android逻辑分辨率,手机ui设计dpi如何把握,看这3个平台各自的画布设置情况
  13. 后科技时代—赛博朋克2077
  14. SpringBoot所有知识点详解,根据狂神说java老师的整理
  15. 软件项目开发流程及人员职责
  16. java 如何给游戏加音效,修改添加游戏中各种音效的步骤
  17. 手写表单及h5表单验证举例
  18. 用于测量纸张卷径的超声波传感器
  19. AI在实体零售行业的应用场景
  20. Vue中使用高德地图,简单明了

热门文章

  1. Halcon 错误 提示 2021 System clock has been set back 解决方法
  2. Altium Designer--如何将视图进行翻转(Flipped)
  3. 常见分布式应用系统设计图解(十二):证券交易系统 | 极客分享第 12 期
  4. 凌晨 3 点不回家:成年人的世界不是他们说的那样子
  5. 建模实训报告总结_模型实训心得体会
  6. day-9 字符串笔记
  7. [转载]主流漫游技术-OKC802.11r介绍
  8. 【C语言进阶】预定义详解
  9. 读书文摘卡 -《大秦帝国》
  10. 概率论基础(3)一维随机变量(离散型和连续型)