The True Story of Hello World

原文链接:The True Story of Hello World


The True Story of Hello WorldHello World的真实故事
(or at least a good part of it)/(至少是其一部分)

Most of our computer science students have been through the famous "Hello World" program at least once. When compared to a typical application program ---almost always featuring a web-aware graphical user interface, "Hello World" turns into an very uninteresting fragment of code. Nevertheless, many computer science students still didn't get the real story behind it. The goal of this exercise is to cast some light in the subject by snooping in the "Hello World" life-cycle.

大多数计算机科学的学生至少都经历过一次有名的“Hello World”程序。比起一个典型的应用程序(比如能上网的图形用户界面),“Hello World”看上去不过是些无趣的代码片段;尽管如此,许多学生还是不了解它背后真正的故事。这个练习通过审视“Hello World”的一生来了解这个故事。

The source code/源代码

Let's begin with Hello World's source code:/让我们从Hello World的源代码开始

# include <stdio.h>

int main(void)
{
    printf("Hello World!\n");
    return 0;
}

Line 1 instructs the compiler to include the declarations needed to invoke theprintf C library (libc) function.

第1行指示编译器包含为调用printf(C库/libc函数)所需要的声明。


Line 3 declares function main, which is believed to be our program entry point (it is not, as we will see later). It is declared as a function that takes no parameter (we disregard command line arguments in this program) and returns an integer to the parent process --- the shell, in our case. By the way, the shell dictates a convention by which a child process must return an 8-bit number representing it status:0 for normal termination,0 > n < 128 for process detected abnormal termination, andn > 128 for signal induced termination.

第3行声明了main函数,一般被认为是程序的入口(后面我们将看到其实不是)。它被声明成不带参数(目前不考虑命令行参数)以及返回给父进程(这里是shell)一个整数值。顺便一提有个惯例,shell要求子进程必须返回一个8位整数来表示其状态——0表示正常退出;大于0小于128表示进程异常退出;大于128表示由于信号导致的结束。


Line 4 through 8 comprise the definition of function main, which invokes theprintf C library function to output the "Hello World!/n" string and returns 0 to the parent process.

第4到8行组成了main函数的定义,它调用了printf(C库函数)来输出字符串"Hello World!/n"然后返回给父进程0。


Simple, very simple!

简单,非常简单!

Compilation/编译

Now let's take a look at the compilation process for "Hello World". For the upcoming discussion, we'll take the widely-used GNU compiler (gcc) and its associated tools (binutils). We can compile the program as follows:

现在我们来看看“Hello World”的编译过程。接下来的讨论,我们将使用广泛应用的GNU compiler(gcc)以及相应的工具(binutils)。如下编译程序:

# gcc -Os -c hello.c

This produces the object file hello.o. More specifically,/这将产生目标文件hello.o,进一步

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

tells us hello.o is a relocatable object file, compiled for the IA-32 architecture (I used a standard PC for this study), stored in theExecutable and Linking Format (ELF), that contains a symbol table (not stripped).

告诉我们hello.o是个可重定位的目标文件,编译于IA-32架构(在这里我使用了标准PC),存储成ELF格式(可执行和链接格式),它包含了符号表(not stripped)。


By the way,/顺便一提

# 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

tells us hello.o has 5 sections:/可以发现hello.o有5个段:

  1. .text: that's "Hello World" compiled program, i.e. IA-32 opcodes corresponding to the program. This will be used by the programloader to initialize theprocess' code segment.
  2. .data: "Hello World" has neither initialized global variables nor initialized static local variables, so this section is empty. Otherwise, it would contain the variable initial values to be loaded into thedata segment.
  3. .bss: "Hello World" also doesn't have any non-initialized variable, either global or local, so this section is also empty. Otherwise, it would indicate how many bytes must be allocated and zeroed in thedata segment in addition to section .data.
  4. .rodata: this segment contains the "Hello World!/n" string, which is tagged read-only. Most operating systems do not support aread-only data segment for processes (running programs), so the contents of.rodata go either to the process' code segment (because it's read-only), or to thedata segment (because it's data). Since the compiler doesn't know the policy adopted by your OS, it creates this extra ELF section.
  5. .comment: this segment contains 33 bytes of comments which cannot be tracked back to our program, since we didn't write any comment. We'll soon see where it comes from.


  1. .text:编译过的“Hello World”程序,即程序相应的IA-32机器码。程序装载器loader)将用其来初始化进程的代码段
  2. .data:“Hello World”既没有全局变量又没有静态局部变量(均初始化过),所以这个段是空的;否则,这个段将包含变量的初始化值,它们将被装载至数据段
  3. .bss:“Hello World”也没有任何未初始化过的全局/局部变量,所以这个段也是空的;否则,它表明在数据段中分配并清零一定数目的字节(除了.data段之外)。(译注:即在数据段中给未初始化的全局/静态局部变量预留空间并将其初始化为0。)
  4. .rodata:这个段包含字符串"Hello World!/n",它被标记为只读。大多数操作系统并不支持进程(运行着的程序)只读数据段,所以.rodata的内容既可以放置在进程的代码段(因为它是只读的),又可以放置在数据段(因为它是数据)。既然编译器并不知道操作系统的策略,所以它产生了这个额外的ELF段。
  5. .comment:这个段包含33字节的注释,但在这个程序中没有蛛丝马迹,因为我们没有写任何注释。很快我们会看到它如何产生。

(译注:上述的段可能是section——静态程序的段,也可能是segment——进程(运行着的程序)的段。请根据上下文判断。)


It also shows us a symbol table with symbol main bound to address 00000000 and symbolputs undefined. Moreover, therelocation table tells us how to relocate the references to external sections made in section.text. The first relocatable symbol corresponds to the "Hello World!/n" string contained in section.rodata. The second relocatable symbol,puts, designates alibc function which was generated as a result of invokingprintf. To better understand the contents ofhello.o, let's take a look at the assembly code:

它也显示了符号表,符号main绑定至地址00000000,符号puts未定义。进一步,重定位表显示如何重新定位在.text段中至别的段的引用。第一个可重定位的符号对应于字符串"Hello World!/n",它包含在.rodata段中。第二个可重定位的符号,puts,指明了一个libc函数,它是由于调用printf而产生的。为了更好的理解hello.o的内容,让我们来看看汇编码:

# gcc -Os -S hello.c -o -
        .file   "hello.c"
        .section       .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "Hello World!"
        .text
        .align 2
.globl main
        .type   main,@function
main:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   $.LC0
        call    puts
        xorl    %eax, %eax
        leave
        ret
.Lfe1:
        .size   n,.Lfe1-n
        .ident  "GCC: (GNU) 3.2 20020903 (Red Hat Linux 8.0 3.2-7)" 

From the assembly code, it becomes clear where the ELF section flags come from. For instance, section.text is to be 32-bit aligned (line 7). It also reveals where the.comment section comes from (line 20). Sinceprintf was called to print a single string, and we requested our nice compiler to optimize the generated code (-Os),puts was generated instead. Unfortunately, we'll see later that ourlibc implementation will render the compilereffort useless.

根据汇编码,ELF段标志从何而来就很清楚了。例如,段.text是32位对齐的(第7行)。它也显示了.comment从何而来(第20行)。既然调用printf来打印单一的字串,而我们又要求编译器优化生成的代码(-Os),所以puts被生成。不幸的是,随后我们发现libc的实现将导致编译器的努力作废。


And what about the assembly code produced? No surprises here: a simple call to functionputs with the string addressed by.LC0 as argument.

至于生成的汇编码?没什么奇怪的:简单的调用puts函数,由.LC0编址的字符串是参数。

Linking/链接

Now let's take a look at the process of transforming hello.o into an executable. One might think the following command would do:

现在来看看转换hello.o成可执行文件的过程。可能会想到下面的命令:

# ld -o hello hello.o -lc
ld: warning: cannot find entry symbol _start; defaulting to 08048184

But what's that warning? Try running it!/但是这个警告是怎么回事?试着运行一下!


Yes, it doesn't work. So let's go back to that warning: it tells the linker couldn't find our program's entry point_start. But wasn't itmain our entry point? To be short here,main is the start point of a C program from the programmer's perspective. In fact, before calling main, a process has already executed a bulk of code to "clean up the room for execution". We usually get this surrounding code transparently from the compiler/OS provider.

是的,它不能工作。让我们回到这个警告:它告诉链接器找不到程序的入口点_start。难道main不是入口点么?简单的说,从程序员的视角,main是C程序的入口点。事实上在调用main之前,进程为了“清理执行空间”已经执行了一堆代码。编译器/操作系统为我们做了这些事(所以我们感觉不到,即“透明”)。


So let's try this:/所以,试试这个:

# 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

Now we should have a real executable. Static linking was used for two reasons: first, I don't want to go into the discussion of how dynamic libraries work here; second, I'd like to show you how much unnecessary code comes into "Hello World" due to the way libraries (libc and libgcc) are implemented. Try the following:

现在我们有了一个真正的可执行程序。使用静态链接有两个原因:第一,我不想在这里讨论动态链接是如何工作的;第二,我想展示下由于库的实现(libclibgcc),“Hello World”引入了许多不需要的代码。试试这个:

# find hello.c hello.o hello -printf "%f/t%s/n"
hello.c 84
hello.o 788
hello   445506

You can also try "nm hello" or "objdump -d hello" to get an idea of what got linked into the executable.

你也可以试试"nm hello"或者"objdump -d hello"来看看都有哪些东西被链接进来。


For information about dynamic linking, please refer to Program Library HOWTO.

关于动态链接,请参考Program Library HOWTO

Loading and running/装载和运行

In a POSIX OS, loading a program for execution is accomplished by having the father process to invoke the fork system call to replicates itself and having the just-createdchild process to invoke theexecve system call to load and start the desired program. This procedure is carried out, for instance, by theshell whenever you type an external command. You can confirm this withtruss orstrace:

在一个POSIX操作系统中,装载程序运行是这样完成的:进程调用fork(系统调用)来复制它自己,紧接着产生的进程调用execve装载和运行程序。当你输入一个外部命令的时候,这个过程是由shell完成的。可以由truss或者strace来确认这个过程:

# strace -i hello > /dev/null
[????????] execve("./hello", ["hello"], [/* 46 vars */]) = 0
...
[08053d44] write(1, "Hello World!/n", 13) = 13
...
[0804e7ad] _exit(0) = ?

Besides the execve system call, the output shows the call to write that results fromputs, and the call toexit with the argument returned by functionmain (0).

除了execve系统调用,输出也显示了由于puts而调用的write,以及对exit的调用,main函数的返回值(0)是它的参数。


To understand the details behind the loading procedure carried out by execve, let's take a look at our ELF executable:

为了理解由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 

The output shows the overall structure of hello. The first program header corresponds to the process'code segment, which will be loaded from file at offset 0x000000 into a memory region that will be mapped into the process'address space at address 0x08048000. The code segment will be 0x55dac bytes large and must be page-aligned (0x1000). This segment will comprise the.text and.rodata ELF segments discussed earlier, plus additional segments generated during the linking procedure. As expected, it's flagged read-only (R) and executable (X), but not writable (W).

输出显示了hello的整体结构。第一个程序头对应于进程的代码段,它将从文件的0x000000处装载至内存,这块内存将映射至进程的地址空间0x08048000处。代码段有0x55dac字节这么大,并且必须页对齐(0x1000)。这个段也包含早期讨论过的.text.rodata段(ELF段)以及链接过程产生的额外的段。正如预期的,它有可读和可执行标志(R and E),但是不可写(W)。


The second program header corresponds to the process' data segment. Loading this segment follows the same steps mentioned above. However, note that the segment size is 0x01df4 on file and 0x03240 in memory. This is due to the.bss section, which is to be zeroed and therefore doesn't need to be present in the file. The data segment will also be page-aligned (0x1000) and will contain the.data and.bss ELF segments. It will be flagged readable and writable (RW). The third program header results from the linking procedure and is irrelevant for this discussion.

第二个程序头对应于进程的数据段。装载此段和上述提到的步骤相似。但是请注意文件中段的大小是0x01df4而内存中的大小则是0x03240。这是由于.bss段,因为它将被置零所以无需在文件中有所表示。数据段也需要段对齐(0x1000),它包含.data.bss ELF段。它被标志成可读和可写(RW)。第三个程序段是由链接过程产生的,在此不予讨论。


If you have a proc file system, you can check this, as long as you get "Hello World" to run long enough (hint:gdb), with the following command:

如果有个proc文件系统,可以用这样的命令来确认(只要“Hello World”运行得足够久,提示: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 

The first mapped region is the process' code segment, the second and third build up thedata segment(data + bss + heap), and the fourth, which has no correspondent in the ELF file, is thestack. Additional information about the runninghello process can be obtained with GNUtime, ps, and/proc/pid/stat.

第一个被映射的区域是进程的代码段,第二三个区域组成了数据段data+bss+heap),第四个区域是,与ELF文件没什么关系。运行hello进程的额外信息可以由GNUtimeps/proc/pid/stat得到。

Terminating/终止

When "Hello World" executes the return statement in main function, it passes a parameter to the surrounding functions discussed in section linking. One of these functions invokes theexit system call passing by thereturn argument. The exit system call hands over that value to theparent process, which is currently blocked on thewait system call. Moreover, it conducts a clean process termination, with resources being returned to the system. This procedure can be partially traced with the following:

当“Hello World”执行main函数中的return语句的时候,它传递参数给链接过程讨论过的环境函数。其中一个函数又调用exit系统调用,return的参数被传给exitexit将这个值传给进程,它此时由于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) 

Closing/结束

The intention of this exercise is to call attention of new computer science students to the fact that a Java applet doesn't get run by magic: there's a lot of system software behind even the simplest program. If consider it useful and have any suggestion to improve it, please e-mail me.

这个练习的目的是为了引起计算机科学新生代注意,即Java applet不是凭空运行的——即使是一个最简单程序,它后面也有许多系统软件。如果你有任何改善的建议,请联系我(译注,是原作者,不是译者。)

FAQ/常见问题

This section is dedicated to student's frequently asked questions./这一节是学生经常问的问题。

  • What is "libgcc"? Why is included in linkage?

    Internal compiler libs, such as libgcc, are used to implement language constructs not directly implemented by the target architecture. For instance, the module operator in C ("%") might not be mappable to a single assembly instruction on the target architecture. Instead of having the compiler to generate in-line code, a function call might be preferable (specially for memory limited machines such as microcontrollers). Many other primitives, including division, multiplication, string manipulation (e.g. memory copy) are typically implemented on such libraries.

  • 什么是“libgcc”?为什么在链接中包含它?
    编译器内部库,比如libgcc,用来实现目标架构不能直接实现的语言结构。例如C语言的求模运算符(“%”)在目标架构上没有相应的单一汇编指令。比起编译器生成内嵌代码,一个函数调用也许更有用(特别是对内存受限的机器,比如微控制器)。许多其它的原语,包括除法,乘法,字符串操作(例如内存拷贝)典型地在这些库中实现。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值