c语言程序-hello world-运行原理简介
本文從一個最簡單C程序(hello)是如何被編譯和鏈接講起,描述了一個程序運行的原理。
一、程序運行之前
使用IDE(集成開發環境)的朋友們經常會有這樣的疑問:代碼是怎么從一個文本文件變成可執行程序的呢?代碼畢竟不是咒語,一個c程序在被運行之前其實經過了四個步驟,兩次編譯,匯編和鏈接。
1.預編譯
這里我們只需要知道有一個叫預處理器(preprocessor也稱預編譯器)的程序會對c程序進行一些預處理工作。比如對一些宏定義的變量進行替換。
2.編譯
編譯的過程中,編譯器(compiler)把C語言程序翻譯成匯編語言程序,一條c語句通常需要好幾條匯編代碼來取代,C編譯器為了提高程序執行的效率有時候會對程序進行優化,這就是為什么即使在c程序中聲明了register變量,程序也不一定會用。了解編譯器這個特性對程序員來說也很重要,比如程序員可以通過指令告訴編譯器是生成“易于調試”(debug)還是“代碼盡可能小”(release)的版本。
3.匯編
編譯得到的匯編語言通過匯編器(assembler)再匯編成可重定位目標程序hello.o,與之相反的一個過程被稱為反匯編(disassemble)。
4.鏈接(Link)
hello.0不能被系統直接運行,而且通常情況下.o有很多個,程序中還要包含一些共享目標文件,比如hello程序中的printf函數,它位于靜態庫中,需要用鏈接器(linker)進行鏈接,Unix的連接器就是大名鼎鼎的ld。
printf的聲明在頭文件stdio.h中,如果在安裝vc6.0時選擇了“安裝CRT源碼”就可以在VC98\CRT\SRC目錄找到printf.c,里面函數完整的定義。事實上很多編譯器套裝(比如gcc)為了提高編譯的效率,已經把這個頭文件中牽涉到的所有函數分別編譯成單獨模塊并最后打包成了一個文件(放在系統固定的文件夾中),這個文件就是所謂的靜態庫,windows中后綴名是.lib,unix是.a,當我們link的時候,只需要在指定庫中找到printf對應的那部分二進制代碼添加到程序中就行了。從理論上講hello.c中有幾個printf,就會在可執行文件中嵌入幾次printf的二進制模塊,而且當系統內有多個hello同時運行時每個hello都會維護一段屬于自己的printf,這樣做顯然是一種浪費。
使用共享庫(shared library)可以解決這個問題,共享庫也是一個目標模塊(后綴名.so),它在程序運行之前會被加載到存儲器中某一個特定的區域(linux中,是從地址0×40000000開始的一段區域),并和用到它的程序鏈接起來,這個過程被稱為動態鏈接,因此共享庫在windows中又被稱為動態鏈接庫(DLL)。比如hello在鏈接時其實并沒有把printf模塊加到可執行程序中,而只是告訴我們的hello一聲,待會要用到printf的時候去共享庫里找xx就行了。鏈接是程序再被真正執行前一個極其重要的步驟,但由于IDE給別人造成的錯覺,很多程序員居然不知道有這么一步。
經過以上幾個步驟,hello.c已經變成了可執行程序hello,我們在shell中輸入./hello,屏幕上打印出“hello,world”。gcc提供了以上這些工具的一個集合,我們通常把gcc叫做一個編譯器,其實是不完整的,編譯器只是gcc的一個部分,gcc的全稱應該是gnu編譯器套裝(GNU Compiler Collection)。
二、存儲器中的hello
我們知道可執行程序在被CPU執行以前存在于內存中,于是我們很快就有了新的疑問,二進制代碼在內存中長什么樣?內存其實是個模棱兩可的叫法,如果說世界上只有兩種存儲設備,那么說其中一個是內存另一個是外存就不會有爭議,但是站在CPU角度看,cache明顯要比我們的內存條要內多了,而站在U盤的角度,硬盤也頓時變成了內存。內和外永遠是相對的,比較科學的稱呼應該是dram(讀音為/draem/,即動態隨機存儲器)。既然有動態隨機存儲器(dynamic ram),也就有靜態隨機存儲器(static ram),CPU內部的高速緩存用的就是用sram。
在了解存儲器之前我們先來區分一下進程和程序這兩個概念,維基百科上找到定義是:進程是程序執行的一個實例(instance)。這種說法解釋了為什么同一個程序在內存中能有很多個進程。有些書上寫,進程是程序執行的一個過程,也沒有錯,但問題是進程本來和過程就是同一個東西(process),我們怎么能用饅頭去解釋饃饃呢?
因此hello程序和hello進程是兩個東西,前者是留在磁盤中的一些磁信號,而后者是系統各種資源(cpu、存儲器、IO設備……)共同作用的結果。如果我們要徹底理解hello是怎么運行的,首先就必須hello在內存中的布局有一個比較理性的認識。下面來看一個程序在存儲器中的圖像。
可能有人要問了,圖中存儲器的地址空間為什么有4G?(0到0xffffffff),如果計算機的只有1G主存,那豈不是溢出了?事實上現代操作系統采取了一種叫虛擬存儲器(virtual memory)的機制來有效地管理存儲器,即把系統的存儲設備全部隱藏在背后,無論實際的物理存儲器(dram)有多大都提供給我們一個固定虛擬的線性空間(32位操作系統就有4G空間),系統在幕后對實際的地址進行映射(可能在dram中,也可能在磁盤上),而我們就感覺自己在使用一臺存儲器很大的計算機,盡管當實際的dram很小時我們還是感覺很慢,于此同時硬盤燈在不停閃爍。
Linux將虛擬存儲器高端的1/4留給內核,剩下3/4全留給用戶進程。虛擬存儲器上中的程序主要由以下幾個重要組成部分:
1.進程控制塊(process control block,簡稱PCB)
PCB中保存了進程hello的運行時的存儲器圖像和寄存器信息,它幫助操作系統在內存中找到我們的hello程序,如果沒有它,hello只是和其它程序雜亂無章地分布在內存中就亂套了。
2.棧(stack)和堆(heap)
程序中的自動變量都位于棧上,而堆則用來讓程序員自己手動分配(malloc)和釋放(free)的內存空間,如果程序員忘了釋放,則有垃圾收集器gc代勞。除此以外,棧還是程序轉移中一個很重要的概念,程序的返回地址通常也保存在棧上。
3.文本段(text segment)和數據段(data segment)
所謂的文本段和數據段對應的就是程序的代碼部分和全局變量,把程序的代碼和數據分開處理是有好處的,比如我們在windows打開好幾個word,這些進程只是數據段不同罷了(它們都擁有相同的代碼),因此內存中永遠只要有一份word的代碼就行了。
4.共享庫的映射區域
操作系統通過將共享的對象映射到虛擬存儲器的“共享區域”來使得代碼能夠共享,一方面提高存儲器的利用率,一方面可以使得進程能夠共享一些數據。
如果某一時刻系統中有20個程序正在運行,而這些程序都需要在屏幕上打印東西,系統就沒有必要為每個程序都維護一段printf的代碼,只要分別從各自的.bss中取出字符串然后用同一個printf完成輸出就行了。同樣的道理,當有多個hello在系統中運行時,它們也完全可以共享同一個文本段。這也就是為什么會把進程定義為程序的一個實例的原因。不妨回想一下面向對象中對象的概念,我們在寫class的時候定義成員字段不就是在分配數據?而定義方法字段不就是在操作這些數據?在對象被實例化以前,這些定義只不過是一些“白紙黑字”,而只有經過實例化,實例們才在存儲器中有了自己的映像。而多個實例之間可以共享“方法”(文本)但是獨有“成員”(數據)的特點,也和進程如出一轍。
現在我們可以描述hello在存儲器中圖像了。hello的代碼位于文本段中,字符串“hello,world”在只讀段中,printf位于共享庫的映射區域,程序在執行時用到了用戶棧,用戶棧從0xbfffffff開始,向下生長。以上的圖景只發生在一瞬間,我們難以追蹤,要想看清hello的本來模樣,還是得在目標文件上做文章。
三、目標文件的格式
1.可重定位目標文件hello.o
這是書上典型的一個elf格式的可重定位目標文件:
ELF Header .text .rodata .data .bss .symtab .rel.text .rel.data .debug .line .strtab Section Headers有興趣的朋友可以在Unix/Linux下使用readelf這個工具來查看hello.o的具體格式。
2.可執行目標文件a.out
可重定位目標文件(hello.o)離最終的可執行目標文件(a.out)只有一步之遙,這關鍵的一步就是前面說的鏈接。
鏈接通常有兩步,第一步是解析符號,符號解析主要用來解決多個模塊之間全局字段的協調問題,比如我們在兩個.c的文件中都定義了全局變量x,或者引用了不曾定義過的函數foo(),鏈接器都會報錯(link error)。第二步就是重定位,重定位將每個目標模塊的節最終合并成一個大的節(section),并且根據rel.text來修改調用外部函數(printf)或者引用任何全局變量(“hello,world”)的指令。hello.o和a.out最大的區別在于,a.out的節頭目表為每個節都分配了真實地址,而hello.o中的節頭目表只在重定位時為鏈接器提供了一個快速定位節的方式。
下面是一個典型可執行目標文件(但實際上要復雜得多):
ELF Header .init .text .rodata .data .bss .symtab .debug .line .strtab Section Headers筆者在學習c的時候就聽到過這么一句話——“main是程序的入口”,真的是這樣嗎?嘗試一下這條命令:
ld hello.o -lc ld: warning: cannot find entry symbol _start; defaulting to 080481a4這說明編譯器在_main之前會先去找一個_start符號。事實上程序在運行的初期還需要做一些初始化和清理的工作,這些代碼位于crt1.o模塊中,即c運行時(runtime)庫,它包含了程序的入口函數_start,由它負責調用__libc_start_main初始化libc,并且調用main函數進入真正的程序主體,這部分代碼必須在鏈接時加進來(對我們來說是透明的),否則程序根本運行不到的main。
3.printf
printf的機器碼位于/lib/libc.so.6的共享庫中,它將在程序運行時被加載到存儲器的共享庫映射區域。printf中又用到了系統調用write來輸出格式串,所謂系統調用可以看成是操作提供給程序員的一個編程接口,我們可以調用它來獲取操作系統提供的一些服務,完成一些和輸入輸出有關的操作。
原地址為;http://www.chengyichao.info/principles-of-the-program-running/
總結
以上是生活随笔為你收集整理的c语言程序-hello world-运行原理简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于C++有限状态机的实现技术
- 下一篇: 计算机程序是怎样运行的-hello wo