| Solrex's profileSolrex Shuffling.LifeBlogListsNetwork | Help |
|
2/28/2008 The True Story of Hello WorldHello World 背后的真实故事 (至少是大部分故事) * 原作者:Antonio Augusto M. Frohlich * 原文链接: http://www.lisha.ufsc.br/~guto/teaching/os/exercise/hello.html * 译者:杨文博 <http://solrex.cn> * 译文链接: http://share.solrex.cn/os/hello_cn.html * 最后更新时间: 2008 年 2 月 28 日 我们计算机科学专业的大多数学生至少都接触过一回著名的 "Hello World" 程序。相比一个典型的应用程序——几乎总是有一个带网络连接的图形用户界面,"Hello World" 程序看起来只是一段很简单无趣的代码。不过,许多计算机科学专业的学生其实并不了解它背后的真实故事。这个练习的目的就是利用对 "Hello World" 的生存周期的分析来帮助你揭开它神秘的面纱。 源代码 让我们先看一下 Hello World 的源代码: 1. #include <stdio.h> 2. int main(void) 3. { 4. printf("Hello World!\n"); 5. return 0; 6. 7. } 第 1 行指示编译器去包含调用 C 语言库(libc)函数 printf 所需要的头文件声明。 第 3 行声明了 main 函数,看起来好像是我们程序的入口点(在后面我们将看到,其实它不是)。它被声明为一个不带参数(我们这里不准备理会命令行参数)且会返回一个整型值给它的父进程(在我们的例子里是 shell)的函数。顺便说一下,shell 在调用程序时对其返回值有个约定:子进程在结束时必须返回一个 8 比特数来代表它的状态:0 代表正常结束,0~128 中间的数代表进程检测到的异常终止,大于 128 的数值代表由信号引起的终止。 从第 4 行到第 8 行构成了 main 函数的实现,即调用 C 语言库函数 printf 输出 "Hello World!\n" 字符串,在结束时返回 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 stripped 给出的信息告诉我们 hello.o 是个可重定位的目标文件(relocatable),为 IA-32(Intel Architecture 32) 平台编译(在这个练习中我使用了一台标准 PC),保存为 ELF(Executable 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" 编译生成的可执行代码,也就是说这个程序对应的 IA-32 指令序列。.text 段将被加载程序用来初始化进程的代码段。 2. .data:"Hello World" 的程序里既没有初始化的全局变量也没有初始化的静态局部变量,所以这个段是空的。否则,这个段应该包含变量的初始值,运行前被装载到进程的数据段。 3. .bss: "Hello World" 也没有任何未初始化的全局或者局部变量,所以这个段也是空的。否则,这个段指示的是,在进程的数据段中除了上文的 .data 段内容,还有多少字节应该被分配并赋 0。 4. .rodata: 这个段包含着被标记为只读 "Hello World!\n" 字符串。很多操作系统并不支持进程(运行的程序)有只读数据段,所以 .rodata 段的内容既可以被装载到进程的代码段(因为它是只读的),也可以被装载到进程的数据段(因为它是数据)。因为编译器并不知道你的操作系统所使用的策略,所以它额外生成了一个 ELF 文件段。 5. .comment:这个段包含着 33 字节的注释。因为我们在代码中没有写任何注释,所以我们无法追溯它的来源。不过我们将很快在下面看到它是怎么来的。 它也给我们展示了一个符号表(symbol table),其中符号 main 的地址被设置为 00000000,符号 puts 未定义。此外,重定位表(relocation table)告诉我们怎么样去在 .text 段中去重定位对其它段内容的引用。第一个可重定位的符号对应于 .rodata 中的 "Hello World!\n" 字符串,第二个可重定位符号 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 行)。因为我们使用 printf 来打印一个字符串,并且我们要求我们优秀的编译器对生成的代码进行优化(-Os),编译器用(应该更快的) puts 调用来取代 printf 调用。不幸的是,我们后面将会看到我们的 libc 库的实现会使这种优化变得没什么用。 那么这段汇编代码会生成什么代码呢?没什么意外之处:使用标志字符串地址的标号 .LCO 作为参数的一个对 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\n" hello.c 84 hello.o 788 hello 445506 你也可以尝试 "nm hello" 和 "objdump -d hello" 命令来得到什么东西被连接到了可执行文件中。 想了解动态连接的更多内容,请参考 Program Library HOWTO 装载和运行 在一个遵循 POSIX(Portable Operating System Interface) 标准的操作系统(OS)上,装载一个程序是由父进程发起 fork 系统调用来复制自己,然后刚生成的子进程发起 execve 系统调用来装载和执行要运行的程序组成的。无论何时你在 shell 中敲入一个外部命令,这个过程都会被实施。你可以使用 truss 或者 trace 命令来验证一下: # strace -i hello > /dev/null [????????] execve("./hello", ["hello"], [/* 46 vars */]) = 0 ... [08053d44] write(1, "Hello World!\n", 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 的整体结构。第一个程序头对应于进程的代码段,它将从文件偏移 0x000000 处被装载到映射到进程地址空间的 0x08048000 地址的物理内存中(虚拟内存机制)。代码段共有 0x55dac 字节大小而且必须按页对齐(0x1000, page-aligned)。这个段将包含我们前面讨论过的 ELF 文件中的 .text 段和 .rodata 段的内容,再加上在连接过程中生成的附加的段。正如我们预期,它被标志为:只读(R)和可执行(X),不过禁止写(W)。 第二个程序头对应于进程的数据段。装载这个段到内存的方式和上面所提到的一样。不过,需要注意的是,这个段占用的文件大小是 0x01df4 字节,而在内存中它占用了 0x03240 字节。这个差异主要归功于 .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 + heap),第四个区域在 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"], [/* 46 vars */]) = 0 fork() = 8321 [pid 8320] wait4(-1, <unfinished ...> [pid 8321] execve("./hello", ["hello"], [/* 46 vars */]) = 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 的运行并不是像魔法一样(无中生有的),即使在最简单的程序背后也有很多系统软件的支撑。如果您觉得这篇文章有用并且想提供建议来改进它,请发电子邮件给我。 常见问题 这一节是为了回答学生们的常见问题。 * 什么是 "libgcc"? 为什么它在连接的时候被包含进来? 编译器内部的函数库,比如 libgcc,是用来实现目标平台没有直接实现的语言元素。举个例子,C 语言的模运算符 ("%") 在某个平台上可能无法映射到一条汇编指令。可能用一个函数调用实现比让编译器为其生成内嵌代码更受欢迎(特别是对一些内存受限的计算机来说,比如微控制器)。很多其它的基本运算,包括除法、乘法、字符串处理(比如 memory copy)一般都会在这类函数库中实现。 2/26/2008 15 Minutes of Fame我前几日发布了免费电子书《使用开源软件-自己动手写操作系统》,本以为只是在和我相关的一些圈子,主要是南京大学和南京大学毕业的 Linux 爱好者中得到传播。虽然我预计到了它会慢慢的扩散开来,但是从没有想过它在发布之初就产生了这么大的影响,我的个人主页和博客的点击量在这几日都迅速上升到日近千次访问。 从统计结果来看,Jserv、silidot 和 LinuxGem 对此消息的分享功不可没。特别是对岸的 Jserv,繁体发布页的一千多次访问几乎都是他带来的(其实这也是我准备繁体发布页的原因,只可惜暂时没有精力发布繁体电子书)。在这里我对他们的无私帮助表示感谢。 再来说说这本电子书,其实我是将其当作对《自己动手写操作系统》一书的读书报告和扩展来写的。之所以将其发布出来,是想让更多人得益于我的共享,从零开始去理解一个操作系统,因为很多讲操作系统的书不会讲这些体验,了解了这些就能得到更完整的体验。 有很多人诟病本书的英文名"Write OS with Free Software"从语法上来讲有错误。我不得不承认我没有认真考虑这个名字,只是随手写了上去,所以可能下次发布的时候本书英文名会更改为"Write Your Own OS with Free and Open Source Software",不知道各位可有更好提议? 我不敢说自己写这本书就是大公无私,回报开源社区,没有一点私心。私心是有的,不过只是为了出一点小名。现在的杨文博仅仅是无名小卒一个,有一些小小名气之后,于找工作交朋友都有益处。我花费了不少的气力在这本书上面,除了一点点名气并没有要求其它的什么,所以我欢迎中肯的批评和建议,而不是对本书毛病的横加指责。谁不想得到别人对自己工作的尊重呢? 我仍是研究生一年级学生,上个学期周五和周末都会去一间公司兼职来赚点生活费。因为作息和饮食一直不够规律引发了身体的一些零件出现问题,所以这个学期辞去了兼职工作专心学习。我有我的正常生活,网络仅仅是我生活的一小部分,这些小小名气也不能解决我的吃饭问题。我不保证电子书的更新发布速率会满足某些读者的要求,所以我只能说我尽力,很抱歉但请不要在这一点上指责我。 Left Simplnano由于身体健康及其它原因,本人已经从北京简约纳电子公司辞去兼职软件工程师职位。 以前所发布文章和 GDB Mailing List 中所留的和该公司有关的联系方式均作废,请不要再向 wenbo.yang@simplnano.com 发送电子邮件。如需联系本人,请访问本人个人主页 http://solrex.cn 获取最新联系方式。 2/24/2008 It is A Cold Cold Winter这是一篇寒假生活的补记。 2007 年的冬天不是一个一般的冬天,它可能给很多人留下了深刻的回忆,包括在路上的和不在路上的。"五一"长假的取消和大雪封路的威胁也让买张回家的火车票成为一件很奢侈的事情。回家前和我的导师聊天,他说:"买张回家的卧铺票就好比发篇好点儿的论文,都是一年一次,都是难上加难。"幸好因为国家的某些政策,学生能享受提前许多天买票的优惠,才让这个过程显得没那么困难。 我回家的前日家里还飘着雪花,但抵家的凌晨天色就放晴了。刚下火车时从暖暖的车厢中出来还没有感觉到寒意,一到家里就支撑不住了。尤其是在北京习惯于暖气开放的宿舍和教室,也没有着厚厚的衣衫,回到豫东平原上这小小院落的家中,便感觉异常的单薄寒冷。于是自从回家那天起就开始躲被窝中轻易不肯起来,但因活动太少而吃得太多太杂引起的胃胀嗳气几乎困扰了我一个冬天。常年孤单一人在家的妈妈看到在外地求学的两个孩子都回到自己身边,自然是开开心心又买这又买那的,零食一大筐,结果我是看着眼馋还不敢多吃,好生郁闷。 半年未见的家中房屋器物又显得破败了几分,因为爸爸去世的早,我和妹妹又在外地上学,反正也没有人用,我妈一个人也顾不上去修葺它们,所以我回去就得修修电话、开关、插座等一些零碎的东西。这次回家还有一个变化是在我的强烈要求下,家里安装上了卫星电视接收机,这样我们总算不用再忍受地方台的垃圾电视节目了。我知道,在中国这种做法是违反某些广电总局的法规的,但是对于一个贫穷的母亲来说一次性 200 块钱的消费总好过于初装 300+ 再加上每年 120 元的有线电视费。我妈之所以愿意装这个也是因为我开始读研究生了,而且自己能打工挣钱,不用再花家里的钱了,否则两个大学生的学费怎么是一个贫困地区小学教师能承受的了的啊! 阔别三年的于洁妹子说要给我抓一些治口腔溃疡的中药,所以二月三四号那两天又去了趟我读高中所在的商丘市,见了几个好友,聊了很多八卦。自从读研究生以来我就感觉自己自闭了许多,有些疏远好友,不过见了面还是那么亲切。每个人都奋斗在自己选择的道路上,我相信那些临时的困难都会过去的。 在仍然有着很多传统习俗的中原地区,过年总免不了走亲访友一番,尤其是像我们这种根在农村的大家族。叔伯兄弟还能认得准,因为我是这辈分里最小的,那些侄子外甥们就不知道有多少个了,谁能认得清啊?都说只生一个好,可家里的哥哥们都是忍着计划生育罚款也是不生男孩不罢休,那么多孩子怎么能养得过来。以前大伯不在家,我们回家也就到其他的几个伯伯家坐坐就走了。这两年大伯从甘肃退休回家住着,在老家的老院子里,一家人才能聚在一起热热闹闹的吃顿饭。 回京的时候买票倒还方便,我托小力姐在商丘帮忙买了张卧铺票,坐的时候也不挤,顺顺当当地回来了。 :tag: 回家, 过年 2/22/2008 Google Reader New ProblemGoogle Reader 是我非常喜欢的在线博客订阅器,自从发现这个服务之后我就结束了尝试 N 个离线博客订阅软件的历史。用 Google Reader 看别人的博客好像看邮件一样,而且可以抓取一些国内无法访问的国外博客系统的 feed,很方便。 自从使用以来它就一直很稳定,不过这两天我发现 GR 貌似出现了一些小问题:一个是抓取我的博客 feed 有错,订阅的 http://feeds.feedburner.com/solrex 的条目中忽然多出了十条和我同一服务器的某个博客条目,而 feedburner 的原始连接中并没有这十条内容;二是抓取 feed 的时候没有通知订阅用户数,所以我的 feedburner 和 feedsky 显示的订阅用户一下子少了一大半,本来就只有一百多人订阅,现在显示的订阅数就更可怜了。 本来我以为这可能是个别问题,可是著名博客和菜头也在文章中说某些用户的订阅在 GR 中显示为乱码。这就叫人奇怪了,为什么 GR 在这几天发生那么多问题?难道是 GR 在测试新功能? 虽然 GR 这两天出了那么多问题,我仍然要推荐它,下面简单介绍一下我觉得比较好的特色: 1. 共享阅读。一种方法是把共享阅读块放在自己的主页上(看我的博客右侧栏),另一种是在 GR 的阅读视图里与 Google 好友共享阅读条目,还有一种方法就是把条目直接 email 给别人。 2. 快捷键操作。GR 有很多键盘快捷键,比如:j 阅读下一条,k 阅读上一条,n 移到下一条,p 移到上一条,o 打开列表项,这样操作起来非常方便。 3. 使用安全连接(https)的 GR,避免连接被重置。在中国国内某些网络条件下,明文传输的网页是会被过滤的,如果订阅的博客文章中有敏感词条,路由器就会重置网络连接,比如经常看到的浏览器提示"与服务器的连接被重置"就是由于这个原因。使用安全连接的 GR https://www.google.com/reader/view/ 能避免此类状况的发生。(此方法同样适用于 Gmail 的连接不稳定现象) 4. feed 阅读趋势。GR 可以统计你的阅读情况,并提供一些阅读趋势排行榜,以供你了解自己的阅读习惯。 :tag: GoogleReader, Feed, Blog, RSS 2/21/2008 Free eBook Write OS with Free Software Revision 1 Released免费电子书《使用开源软件-自己动手写操作系统》Revision 1 发布 作者:Wenbo Yang 最后更新时间:2008年02月21日19点08分 免费电子书《使用开源软件-自己动手写操作系统》的官方主页为:http://share.solrex.cn/WriteOS/ ,您可以到这里下载 pdf 格式电子书和随书源代码。 我叨叨了一个多月的这本书总算能发布第一个版本了,娘啊,费了老鼻子劲了。好多工作要花费在排版上,因为希望整得效果好点儿嘛!而且写了才知道,把自己知道的东西清楚地表述出来真不是件容易的事儿。啥也不说了,看图,还有目录。
写在前面的话i |
|
|