歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

程序是怎麼執行的

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

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

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

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

  程序是精巧與復雜的,熟悉它以後你也會覺得它是脆弱的,因為只要有一個 bit 發生錯誤,整個系統就會崩潰。這個系統就是執行文件格式,在 linux 下叫 elf(executable linkable format)而 windows 下叫 pe (portable execute)。我想寫操作系統第一步就是制定這個規則,不然一切都沒有規律。所以我想 linus 牛,但是 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 了。他們是程序啟動參數。還要載入程序運行的環境變量,棧空間,堆空間,也就是靜態數據與全局變量部分。然後把程序執行寄存器指向程序開始的地方。開始執行!看似簡單,但是很復雜的過程開始了!

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

Copyright © Linux教程網 All Rights Reserved