c语言什么意思000094,Hello World 背后的真实故事
标 题: 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 背后的真实故事相关推荐
- cut the rope HTML 5版本背后的开发故事
译者注:Cut the Rope 是一款人见人爱的小游戏.有一个开发团队将它改造成了HTML5版本.想看看他们在改造过程中的经验之谈吗?那就看下面由开发人员自己写的文章吧~ 启示 在IE9中作为一个H ...
- 程里人 | 写书背后那些不为人知的故事
上周,<携程架构实践><携程人工智能实践>两本技术书籍上市,受到小伙伴们的热捧. 恰逢4月23日世界读书日当天,上市一周的<携程架构实践><携程人工智能实践& ...
- OceanBase数据库背后的传奇故事
这两天被一则消息刷屏,<中国自己的数据库OceanBase超越甲骨文,登顶全球第一>↓ 蚂蚁金服自主研发的数据库OceanBase打破数据库基准性能测试的世界记录,成绩是前世界记录保持者. ...
- 移动信号突然变成无服务器,为什么移动4G信号突然变成H、E、G就无法上网, 背后的真实原因?...
原标题:为什么移动4G信号突然变成H.E.G就无法上网, 背后的真实原因? 日常生活中经常会遇到这样的事,坐在车上手机信号栏刚刚还显示4G,突然变成字母H.E.G,或者4G后面多一个+,变成4G+.相 ...
- 华为p4用鸿蒙系统吗_为什么华为有自主研发的鸿蒙系统,却还要用安卓系统,背后的真实原因?...
一直以来,拥有完全自主知识产权的国产操作系统都是无数国人的希望,然而其难度之大并非短期能实现的事情.毕竟目前像芯片.操作系统还是美国一家独大,在芯片领域,美国已占有全球一半以上的市场份额,中国每年进口 ...
- 我赢资讯10分钟程序化交易决策系统,把握涨跌背后的真实原因,选择最具价值的投资方向...
我赢资讯10分钟程序化交易决策系统,把握涨跌背后的真实原因,选择最具价值的投资方向 开创专业投资战略思维,实现创造财富梦想.运用我赢资讯软件提高投资者专业素养和掌握机构操盘技能,建立程序化交易系统框架 ...
- 张越:每张脸背后都有故事
与张越对话 汹涌的人潮,每张脸背后的故事. --罗大佑 9月12日中午1点,百盛的星巴克.张越比我到得早.虽然头天夜里,她刚从吉林录完节目赶回北京,又看电视新闻直到凌晨3点. 见面的一刻,昨夜电视屏幕 ...
- 夺命金背后的真实香港情况
杜琪峰拍摄这部片子的时候,恐怕没有想到本片的上映期正好遇上"香港人大战内地人"这个尴尬时刻.不管编导用了多么繁复的"多线环形结构"来讲故事,都无碍其无比写实的风 ...
- 欧姆龙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 ...
- 联通4g满格但是网速慢_为什么手机4G信号明明是满格,网络却很慢,背后的真实原因?...
原标题:为什么手机4G信号明明是满格,网络却很慢,背后的真实原因? 虽然国内的4G网络建设比较晚,但是随着国内三大运营商和手机厂商的大力推进,在短短的两三年时间里,就覆盖了国内大部分区域,4G网络如今 ...
最新文章
- 尝试自动重定向的次数太多_GoRod:基于DP协议的Web自动化和数据抓取工具
- js 实现文件导出、文件下载
- java将ascii数组转成unicode字串
- bzoj 1647: [Usaco2007 Open]Fliptile 翻格子游戏(枚举)
- 一个帮助你处理延迟,重复,循环操作的jQuery插件 - timing
- 《程序是怎样跑起来的》第七章
- Notepad++ 安装jsonview插件
- 2022年全球程序员平均薪资发布,中国排名很意外
- IOT语义互操作性之本体论 1
- 高德SDK 小车轨迹回放,速度、进度控制
- Apache DolphinScheduler 海豚调度器自定义时间参数
- android逻辑分辨率,手机ui设计dpi如何把握,看这3个平台各自的画布设置情况
- 后科技时代—赛博朋克2077
- SpringBoot所有知识点详解,根据狂神说java老师的整理
- 软件项目开发流程及人员职责
- java 如何给游戏加音效,修改添加游戏中各种音效的步骤
- 手写表单及h5表单验证举例
- 用于测量纸张卷径的超声波传感器
- AI在实体零售行业的应用场景
- Vue中使用高德地图,简单明了