科普:程序是怎麼執行的

Docker是一個建立在操作系統+編譯器基礎之上的系統,所以瞭解操作系統,編譯器以及程序運行機制對我們理解Docker來說非常重要。本文是一個自己的體會,有很多不精確的地方,目的是希望大家多關注低層,多修煉內功,多讀好書。

一直想寫篇文章來說明在程序運行過程中操作系統都幹了些什麼事。下面我試着說明:

首先,任何程序都是有格式的,所謂無規矩不成方圓,任何美的,精巧的事物都是精密組織的,程序也一樣。我之前用的最多的是c#與java,有趣的是,當時很多人嘲笑java與c#們一直在用腳本寫程序,大概在他們眼裏c與c++纔是真正的程序。但是,現實就是現實,其實我們都是在一個叫做虛擬機的程序下寫託管代碼,它掌握着程序的編譯,鏈接,加載,映射與最終執行與終止。它就是操作系統,準確的講是操作系統+編譯器。他們是真正的元虛擬機。

然後我來解釋下如何運行一個程序:

程序是精巧與複雜的,熟悉它以後你也會覺得它是脆弱的,因爲只要有一個bit發生錯誤,整個系統就會崩潰。這個系統就是執行文件格式,在linux下叫elf(executable linkable format)而windows下叫pe(portable execute)。我想寫操作系統第一步就是制定這個規則,不然一切都沒有規律。所以我想linux牛,但是ken tomason有過之而無不及,畢竟你是在人家基礎之上發展而來的,計算機世界就是如此沒辦法,誰讓你在人家下面呢?

我以linux系統爲例,簡單講講程序由編譯鏈接裝載與執行。elf文件格式分爲很多段—section,總體分爲只讀可執行的代碼段與可讀可寫的數據段。.txt就是典型的代碼段,.data .rodata .symbl .rel .got .plt都是數據段。那麼,編譯器負責將程序員寫的程序,編譯成elf文件,代碼,注視,代碼行對應機器碼信息,就是調試信息啦會進去.txt .code .comment .debug段,常量與靜態變量進入.data .rodata .bss。接下來,編譯器將引用的頭文件中的代碼(特指靜態編譯)與引用的glibc中的庫函數打包(鏈接)到整個可執行文件中,然後在elf文件中設置文件頭信息,如段表位置,程序入口位置等信息。當然,這裏不得不提的是符號表,與重定位表,他們是整個程序最終能跑起來的關鍵。gcc是靠符號,或者說程序是靠符號來鏈接的,不管是函數還是變量,都是符號而已,所以從側面講,寫程序跟寫文章沒啥區別。程序就像個圖書館,每個函數與變量都是書,鏈接程序好比在圖書館看書,當你看到一個點時,就會叫你去某某位置拿另一本書,翻到特定位置開始繼續讀,如果沒找到就會爆出鏈接錯誤。而重定位表就是一次性講所有對需要跳轉的位置進行更改,以確保程序中不存在沒有拿到手的書。

現在程序已經鏈接好了,接下來就是操作系統進行裝載與執行了。當然這是靜態的鏈接,動態鏈接會稍微複雜,會寫很多,這裏不討論。操作系統會打開elf文件的裝載視圖,它能根據裝載視圖的段表—segment這跟section在中文都是段,沒辦法!這個視圖是將數據與代碼分開的,相似section鏈接在一起,所以數量也比section少很多,目的是在裝載時節約內存。因爲,段映射到內存是要地址對齊的,如按照地址4096(一般簇大小爲4k)整除來對齊,這樣做是有好處的,能減少內存碎片,加快磁盤讀寫速度,磁盤最小扇區512byte,所以整數倍讀取能少一次尋址,當然效率更高。這在遊戲引擎,數據庫設計領域比較多見,畢竟io是最大瓶頸,所以再這程序時也要考慮對象佔用內存大小是否是操作系統最小簇的整數倍來判斷一個程序是否是高人所做。

回來,操作系統會最先讀取可執行的文件頭,因爲裏面有運行程序的信息,如段表位置,程序入口,程序類型等。對於操作系統最重要的是段表與程序入口。其中段表就是elf中有多少段,每個段在文件中的偏移,入口則是常說得main函數的虛擬地址。這裏就出現一個問題,程序非得以main函數開始嗎?其實看出來了,不用!只是gcc認定符號main爲c語言的入口,其他程序照抄罷了,當然你可以加入編譯條件更改入口即可。gcc是stallman寫的,他是個黑客,全世界只要運行c的地方,他都能黑,呵呵。

操作系統在讀取可執行程序頭時做了三件事:

1、創建虛擬內存空間來容納一個進程;

2、根據文件頭內容建立程序虛擬內存地址與elf文件的映射關係表,vma(virtual memory area)結構;

3、初始化程序的棧空間與堆空間。

下面解釋下這三個過程:

1、虛擬內存。虛擬內存是編譯器與操作系統的一個約定。任何程序在編譯無鏈接時得地址都是虛擬地址。爲什麼要用虛擬地址這個問題說來話長。話說在很久以前,大家都很窮,都沒內存,但是要運行的程序很多,系統不可能爲每個程序分配單獨的內存,同時領導還要求同時所有程序都要運行,咋辦呢?辦法總比問題多,咱可以分時嘛,你上完cpu我再上,但是大家各自在用cpu時,其他只能看着,直到一個人說”下一個”,這個人不管在幹嘛都得放棄,讓其他人用cpu。這樣對所有人都公平,而且每個人在用cpu是能感覺到cpu只被它獨有,用戶體驗還挺好。所以一次解決可所有問題。而,這個組織人,就是那個喊“下一個”的傢伙就是操作系統。那,說這麼多,跟虛擬地址有啥關係呢?其實仔細想想如果大家都是用物理地址,而彼此在運行時都獨佔系統資源,那前一個程序修改了我的數據咋辦,得了,都由操作系統說了算吧,它做內存映射的維護,大家都用統一的地址空間,但是運行時映射到不同的物理內存互不干擾來。所以你可以看到所有linux程序都從相同的虛擬地址開始執行。

2、建立內存到文件得映射。我們知道,程序都不是一次性加載到內存的,而是一段段的,這是由著名的copy on write規則約束而來的。而這一段也是規定好大小的一般是操作系統簇的大小,也叫一頁。當程序運行過程中發現某個數據在內存中沒有則會報一個頁讀取錯誤,並觸發操作系統的缺頁中斷。這時就要靠操作系統通過讀取elf文件頭建立的從文件系統到虛擬內存的映射來獲取了。它等於是程序運行時到程序得一個索引結構,存儲了運行時程序虛擬內存地址到文件地址的對應表。

3、第三步最簡單,就是操作系統載人main函數後面跟的那個char argc與char*argv了。他們是程序啓動參數。還要載入程序運行的環境變量,棧空間,堆空間,也就是靜態數據與全局變量部分。然後把程序執行寄存器指向程序開始的地方。開始執行!看似簡單,但是很複雜的過程開始了!

好了,這就是簡單的程序如何被操作系統執行的簡單描述,當然這只是靜態鏈接程序的加載,動態鏈接稍微複雜點,原理差不多。

轉自:http://lr3800.com/%E9%BB%91%E5%AE%A2%E6%8A%80%E6%9C%AF/304.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章