欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費電子書(shū)等14項超值服

開(kāi)通VIP
Hello World 背后的故事

Hello World 背后的真實(shí)故事

(至少是大部分故事)


我們計算機科學(xué)專(zhuān)業(yè)的大多數學(xué)生至少都接觸過(guò)一回著(zhù)名的 "Hello World" 程序。相比一個(gè)典型的應用程序——幾乎總是有一個(gè)帶網(wǎng)絡(luò )連接的圖形用戶(hù)界面,"Hello World" 程序看起來(lái)只是一段很簡(jiǎn)單無(wú)趣的代碼。不過(guò),許多計算機科學(xué)專(zhuān)業(yè)的學(xué)生其實(shí)并不了解它背后的真實(shí)故事。這個(gè)練習的目的就是利用對 "Hello World" 的生存周期的分析來(lái)幫助你揭開(kāi)它神秘的面紗。

源代碼

讓我們先看一下 Hello World 的源代碼:

1.
2.
3.
4.
5.
6.
7.
#include <stdio.h>

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


第 1 行指示編譯器去包含調用 C 語(yǔ)言庫(libc)函數 printf 所需要的頭文件聲明。


第 3 行聲明了 main 函數,看起來(lái)好像是我們程序的入口點(diǎn)(在后面我們將看到,其實(shí)它不是)。它被聲明為一個(gè)不帶參數(我們這里不準備理會(huì )命令行參數)且會(huì )返回一個(gè)整型值給它的父進(jìn)程(在我們的例子里是 shell )的函數。順便說(shuō)一下,shell 在調用程序時(shí)對其返回值有個(gè)約定:子進(jìn)程在結束時(shí)必須返回一個(gè) 8 比特數來(lái)代表它的狀態(tài):0 代表正常結束,0~128 中間的數代表進(jìn)程檢測到的異常終止,大于 128 的數值代表由信號引起的終止。


從第 4 行到第 8 行構成了 main 函數的實(shí)現,即調用 C 語(yǔ)言庫函數 printf 輸出 "Hello World!\n" 字符串,在結束時(shí)返回 0 給它的父進(jìn)程。

簡(jiǎn)單,非常簡(jiǎn)單!

編譯

現在讓我們看看 "Hello World" 的編譯過(guò)程。在下面的討論中,我們將使用非常流行的 GNU 編譯器( gcc )和它的二進(jìn)制輔助工具( binutils )。我們可以使用下面命令來(lái)編譯我們的程序:


# gcc -Os -c hello.c

這樣就生成了目標文件 hello.o,來(lái)看一下它的屬性:


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

給出的信息告訴我們 hello.o 是個(gè)可重定位的目標文件(relocatable),為 IA-32(Intel Architecture 32) 平臺編譯(在這個(gè)練習中我使用了一臺標準 PC),保存為 ELF(Executable and Linking Format) 文件格式,并且包含著(zhù)符號表(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 個(gè)段:


(譯者注:在下面的解釋中讀者要分清什么是 ELF 文件中的段(section)和進(jìn)程中的段(segment)。比如 .text 是 ELF 文件中的段名,當程序被加載到內存中之后,.text 段構成了程序的可執行代碼段。其實(shí)有時(shí)候在中文環(huán)境下也稱(chēng) .text 段為代碼段,要根據上下文分清它代表的意思。)


  1. .text: 這是 "Hello World" 編譯生成的可執行代碼,也就是說(shuō)這個(gè)程序對應的 IA-32 指令序列。.text 段將被加載程序( loader )用來(lái)初始化進(jìn)程的代碼段。
  2. .data:"Hello World" 的程序里既沒(méi)有初始化的全局變量也沒(méi)有初始化的靜態(tài)局部變量,所以這個(gè)段是空的。否則,這個(gè)段應該包含變量的初始值,運行前被裝載到進(jìn)程的數據段。
  3. .bss: "Hello World" 也沒(méi)有任何未初始化的全局或者局部變量,所以這個(gè)段也是空的。否則,這個(gè)段指示的是,在進(jìn)程的數據段中除了上文的 .data 段內容,還有多少字節應該被分配并賦 0。
  4. .rodata: 這個(gè)段包含著(zhù)被標記為只讀 "Hello World!\n" 字符串。很多操作系統并不支持進(jìn)程(運行的程序)有只讀數據段,所以 .rodata 段的內容既可以被裝載到進(jìn)程的代碼段(因為它是只讀的),也可以被裝載到進(jìn)程的數據段(因為它是數據)。因為編譯器并不知道你的操作系統所使用的策略,所以它額外生成了一個(gè) ELF 文件段。
  5. .comment: 這個(gè)段包含著(zhù) 33 字節的注釋。因為我們在代碼中沒(méi)有寫(xiě)任何注釋?zhuān)晕覀儫o(wú)法追溯它的來(lái)源。不過(guò)我們將很快在下面看到它是怎么來(lái)的。

它也給我們展示了一個(gè)符號表( symbol table ),其中符號 main 的地址被設置為 00000000,符號 puts 未定義。此外,重定位表(relocation table)告訴我們怎么樣去在 .text 段中去重定位對其它段內容的引用。第一個(gè)可重定位的符號對應于 .rodata 中的 "Hello World!\n" 字符串,第二個(gè)可重定位符號 puts,代表了使用 printf 所產(chǎn)生的對一個(gè) libc 庫函數的調用。為了更好的理解 hello.o 的內容,讓我們來(lái)看看它的匯編代碼:

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
# 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)"


從匯編代碼中我們可以清楚的看到 ELF 段標記是怎么來(lái)的。比如,.text 段是 32 位對齊的(第 7 行)。它也揭示了 .comment 段是從哪兒來(lái)的(第 20 行)。因為我們使用 printf 來(lái)打印一個(gè)字符串,并且我們要求我們優(yōu)秀的編譯器對生成的代碼進(jìn)行優(yōu)化( -Os ),編譯器用(應該更快的) puts 調用來(lái)取代 printf 調用。不幸的是,我們后面將會(huì )看到我們的 libc 庫的實(shí)現會(huì )使這種優(yōu)化變得沒(méi)什么用。

那么這段匯編代碼會(huì )生成什么代碼呢?沒(méi)什么意外之處:使用標志字符串地址的標號 .LC0 作為參數的一個(gè)對 puts 庫函數的簡(jiǎn)單調用。

連接

下面讓我們看一下 hello.o 轉化為可執行文件的過(guò)程??赡軙?huì )有人覺(jué)得用下面的命令就可以了:


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

不過(guò),那個(gè)警告是什么意思?嘗試運行一下!


是的,hello 程序不工作。讓我們回到那個(gè)警告:它告訴我們連接器(ld)不能找到我們程序的入口點(diǎn) _start。不過(guò) main 難道不是入口點(diǎn)嗎?簡(jiǎn)短的來(lái)說(shuō),從程序員的角度來(lái)看 main 可能是一個(gè) C 程序的入口點(diǎn)。但實(shí)際上,在調用 main 之前,一個(gè)進(jìn)程已經(jīng)執行了一大堆代碼來(lái)“為可執行程序清理房間”。我們通常情況下從編譯器或者操作系統提供者那里得到這些外殼程序(surrounding code,譯者注:比如 CRT)。


下面讓我們試試這個(gè)命令:


# 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


現在我們可以得到一個(gè)真正的可執行文件了。使用靜態(tài)連接(static linking)有兩個(gè)原因:一,在這里我不想深入去討論動(dòng)態(tài)連接庫(dynamic libraries)是怎么工作的;二,我想讓你看看在我們庫(libclibgcc)的實(shí)現中,有多少不必要的代碼將被添加到 "Hello World" 程序中。試一下這個(gè)命令:


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


你也可以嘗試 "nm hello" 和 "objdump -d hello" 命令來(lái)得到什么東西被連接到了可執行文件中。

想了解動(dòng)態(tài)連接的更多內容,請參考 Program Library HOWTO。

裝載和運行

在一個(gè)遵循 POSIX(Portable Operating System Interface) 標準的操作系統(OS)上,裝載一個(gè)程序是由父進(jìn)程發(fā)起 fork 系統調用來(lái)復制自己,然后剛生成的子進(jìn)程發(fā)起 execve 系統調用來(lái)裝載和執行要運行的程序組成的。無(wú)論何時(shí)你在 shell 中敲入一個(gè)外部命令,這個(gè)過(guò)程都會(huì )被實(shí)施。你可以使用 truss 或者 strace 命令來(lái)驗證一下:


# 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 實(shí)施的裝載過(guò)程背后的細節,讓我們看一下我們的 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 的整體結構。第一個(gè)程序頭對應于進(jìn)程的代碼段,它將從文件偏移 0x000000 處被裝載到映射到進(jìn)程地址空間的 0x08048000 地址的物理內存中(虛擬內存機制)。代碼段共有 0x55dac 字節大小而且必須按頁(yè)對齊(0x1000, page-aligned)。這個(gè)段將包含我們前面討論過(guò)的 ELF 文件中的 .text 段和 .rodata 段的內容,再加上在連接過(guò)程中生成的附加的段。正如我們預期,它被標志為:只讀(R)和可執行(X),不過(guò)禁止寫(xiě)(W)。


第二個(gè)程序頭對應于進(jìn)程的數據段。裝載這個(gè)段到內存的方式和上面所提到的一樣。不過(guò),需要注意的是,這個(gè)段占用的文件大小是 0x01df4 字節,而在內存中它占用了 0x03240 字節。這個(gè)差異主要歸功于 .bss 段,它在內存中只需要被賦 0,所以不用在文件中出現(譯者注:文件中只需要知道它的起始地址和大小即可)。進(jìn)程的數據段仍然需要按頁(yè)對齊(0x1000, page-aligned)并且將包含 .data.bss 段。它將被標識為可讀寫(xiě)(RW)。第三個(gè)程序頭是連接階段產(chǎn)生的,和這里的討論沒(méi)有什么關(guān)系。


如果你有一個(gè) proc 文件系統,當你得到 "Hello World" 時(shí)停止進(jìn)程(提示: gdb,譯者注:用 gdb 設置斷點(diǎn)),你可以用下面的命令檢查一下是不是如上所說(shuō):


# 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


第一個(gè)映射的區域是這個(gè)進(jìn)程的代碼段,第二個(gè)和第三個(gè)構成了數據段(data + bss + heap),第四個(gè)區域在 ELF 文件中沒(méi)有對應的內容,是程序棧。更多和正在運行的 hello 進(jìn)程有關(guān)的信息可以用 GNU 程序:time, ps/proc/pid/stat 得到。

程序終止

當 "Hello World" 程序運行到 main 函數中的 return 語(yǔ)句時(shí),它向我們在段連接部分討論過(guò)的外殼函數傳入了一個(gè)參數。這些函數中的某一個(gè)發(fā)起 exit 系統調用。這個(gè) exit 系統調用將返回值轉交給被 wait 系統調用阻塞的父進(jìn)程。此外,它還要對終止的進(jìn)程進(jìn)行清理,將其占用的資源還給操作系統。用下面命令我們可以追蹤到部分過(guò)程:


# 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)

結束

這個(gè)練習的目的是讓計算機專(zhuān)業(yè)的新生注意這樣一個(gè)事實(shí):一個(gè) Java Applet 的運行并不是像魔法一樣(無(wú)中生有的),即使在最簡(jiǎn)單的程序背后也有很多系統軟件的支撐。如果您覺(jué)得這篇文章有用并且想提供建議來(lái)改進(jìn)它,請

編譯器內部的函數庫,比如 libgcc,是用來(lái)實(shí)現目標平臺沒(méi)有直接實(shí)現的語(yǔ)言元素。舉個(gè)例子,C 語(yǔ)言的模運算符 ("%") 在某個(gè)平臺上可能無(wú)法映射到一條匯編指令??赡苡靡粋€(gè)函數調用實(shí)現比讓編譯器為其生成內嵌代碼更受歡迎(特別是對一些內存受限的計算機來(lái)說(shuō),比如微控制 器)。很多其它的基本運算,包括除法、乘法、字符串處理(比如 memory copy)一般都會(huì )在這類(lèi)函數庫中實(shí)現。



打開(kāi)APP,閱讀全文并永久保存 查看更多類(lèi)似文章
猜你喜歡
類(lèi)似文章
匯編中bss,data,text,rodata,heap,stack的意義
程序內存映像、磁盤(pán)映像的理解,可執行文件的運行過(guò)程
程序運行時(shí)的內存空間分布
為可執行文件“減肥”(一)
GCC-LD 連接腳本分析--uboot.ld
嵌入式Linux學(xué)習筆記(一)
更多類(lèi)似文章 >>
生活服務(wù)
分享 收藏 導長(cháng)圖 關(guān)注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服

欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久