0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

一個程序是如何運行起來的

Linux閱碼場 ? 來源:卯時卯刻 ? 作者:KINGYT ? 2021-10-12 17:48 ? 次閱讀

相信很多同學都會有疑問,一個程序是如何運行起來的,為什么我們在shell中執(zhí)行了一個程序,它的main函數就會被調用呢?在main函數被調用之前及之后,又經歷了什么呢?

今天我們就來詳細的說下這個問題。

還是和之前一樣,我畫了一張程序運行的全景圖,在上圖中,一個程序運行所經歷的代碼段,我都標注了其所在的git倉庫、源文件、及函數名,想要自己看源碼的,可以參考下上圖中的這些信息

我們先從整體上講一下這張圖。

linux下,我們一般都是通過shell來執(zhí)行程序的。

shell其實也是一個普通的程序,它也有自己的main函數,它在正常運行后,會通過調用read_command函數,來等待用戶輸入命令。

在接收到用戶輸入的命令后,shell會先使用fork系統(tǒng)調用,創(chuàng)建一個子進程,然后再在這個子進程中,通過execve系統(tǒng)調用,執(zhí)行最終的用戶程序。

在子進程執(zhí)行用戶程序期間,shell主進程會調用waitpid函數,阻塞等待子進程的完成,子進程完成之后,waitpid從阻塞狀態(tài)中返回,且status參數中會帶著子進程的退出碼,這個退出碼會在后續(xù)的邏輯中被保存起來,供用戶查詢。

之后,shell主進程進入到下一次循環(huán),繼續(xù)等待用戶輸入命令并執(zhí)行。

以上就是shell的主體邏輯,對應于上面全景圖中的藍色部分。

下面我們再來看下linux內核中有關execve系統(tǒng)調用的代碼,也就是上面全景圖中的綠色部分。

shell通過execve系統(tǒng)調用,告知linux內核,要在當前進程中執(zhí)行目標程序,linux內核經過層層代碼,最終到達load_elf_binary函數。

該函數是整個系統(tǒng)調用中最核心的一段邏輯,它主要用來為目標程序準備各種執(zhí)行環(huán)境。

比如,映射代碼區(qū)、數據區(qū)等到當前進程的虛擬地址空間,將程序名、環(huán)境變量、程序參數、及各種其他數據,有規(guī)律的壓入到新分配的棧中,等等。

之后,load_elf_binary函數會調用start_thread,進而會調用start_thread_common函數。

在該函數里,會將返回到用戶區(qū)之后,要執(zhí)行的,用戶區(qū)程序的起始地址,設置到regs-》ip里,同時也會將上面新初始化好的,用戶堆棧的棧頂地址,設置到regs-》sp里。

當execve系統(tǒng)調用返回到用戶區(qū)之后,regs-》ip和regs-》sp里的值,會分別賦值到rip和rsp寄存器里,這樣指定的用戶程序就可以繼續(xù)執(zhí)行了。

這一流程我們在之前的文章 精致全景圖 | 系統(tǒng)調用是如何實現(xiàn)的 中講過,這里就不再贅述。

不過這里還是有一點需要注意,就是設置到regs-》ip中的地址,并不是我們自己程序的起始地址,而是動態(tài)鏈接器 /lib64/ld-linux-x86-64.so.2 的起始地址。

之所以要設置動態(tài)鏈接器的起始地址,是因為我們需要在返回到用戶區(qū)之后,讓其可以繼續(xù)為我們的程序準備執(zhí)行環(huán)境,比如,幫忙加載程序依賴的各種動態(tài)鏈接庫等。

在動態(tài)鏈接器為我們的程序準備好執(zhí)行環(huán)境之后,它會從進程堆棧的auxiliary vector區(qū),取出最終用戶程序的真正起始地址,并跳轉到該位置開始執(zhí)行。

auxiliary vector區(qū)存放的用戶程序的起始地址,是上面linux內核初始化堆棧時設置的。

動態(tài)鏈接器相關的代碼就是這些,它對應于上面全景圖中紫色的部分。

在跳轉到我們自己程序的起始地址后,首先執(zhí)行的并不是我們寫的main函數,而是glibc里名為_start的一段匯編代碼。

這段匯編代碼也比較簡單,主要是從堆棧中獲取main函數所需的argc,argv等參數,然后最終調用我們寫的main函數。

當main函數返回之后,glibc里的后續(xù)代碼,會將main函數的返回值,當作該進程的退出碼,然后調用exit結束該進程。

這些代碼對應于上面全景圖中的粉色部分。

進程調用exit退出之后,shell主進程也會從waitpid的阻塞狀態(tài)中返回,然后繼續(xù)進行下一次循環(huán)。

以上就是程序完整的啟動和結束流程。

下面我們來看下具體的源碼實現(xiàn)。

注意,為了方便理解,很多代碼我們都做了刪減。

首先是shell部分,shell是一個普通的程序,它也有自己的main函數:

372bd54e-22a0-11ec-82a8-dac502259ad0.png

該函數里調用了reader_loop:

373c6454-22a0-11ec-82a8-dac502259ad0.png

reader_loop的主體邏輯是,在while循環(huán)里不斷的使用read_command函數讀取用戶輸入的命令,然后使用execute_command執(zhí)行該命令。

execute_command函數經過層層代碼后,會使用下圖中的fork,創(chuàng)建一個子進程:

3788521a-22a0-11ec-82a8-dac502259ad0.png

然后在該子進程中,使用execve系統(tǒng)調用,告知linux內核,用當前子進程執(zhí)行新的用戶程序:

37c7b0b8-22a0-11ec-82a8-dac502259ad0.png

在shell主進程中,會調用waitpid函數,阻塞等待子進程的完成:

37d9e45e-22a0-11ec-82a8-dac502259ad0.png

當子進程退出后,waitpid會從阻塞狀態(tài)中返回,并在status里攜帶子進程的退出碼,之后shell主進程又返回上面的read_command函數,繼續(xù)等待用戶下一條命令的輸入。

以上就是bash的主體邏輯,對應于上面全景圖中的藍色部分。

下面我們繼續(xù)看全景圖中的綠色部分,也就是linux內核中有關execve的代碼。

當shell的子進程執(zhí)行execve函數時,linux內核中對應的系統(tǒng)調用被觸發(fā):

37eb0efa-22a0-11ec-82a8-dac502259ad0.png

沿著函數的調用鏈,我們會找到一個名為do_execveat_common的函數,在該函數中,會將目標程序的文件名、環(huán)境變量、及各種程序參數等字符串,拷貝到新創(chuàng)建的用戶堆棧區(qū):

3822e884-22a0-11ec-82a8-dac502259ad0.png

此時,新創(chuàng)建的堆棧區(qū)里內容,就如上面全景圖中右下角的a1-a9, b1-b8部分構成的二維網格區(qū)域里所示的內容。

其中,黃色區(qū)域里存放的是程序參數 。/a.out hello world,藍色區(qū)域里存放的是環(huán)境變量 SHLVL=2, HOME=/, TERM=linux, PWD=/,橘黃色區(qū)域里存放的是要執(zhí)行的程序文件名 。/a.out。

這些內容和我們執(zhí)行的測試程序,及其所處的環(huán)境也正好一樣:

3857cfe0-22a0-11ec-82a8-dac502259ad0.png

繼續(xù)沿著內核函數調用鏈,我們最終會來到load_elf_binary函數,該函數是整個系統(tǒng)調用的核心。

由于linux上執(zhí)行的程序基本上都是elf格式,所以內核選擇的加載函數是load_elf_binary,看這個函數時,可以參考elf格式的man文檔:

https://man.archlinux.org/man/elf.5

該函數比較復雜,我對其做了大量刪減,并添加了很多注釋:

387b14fa-22a0-11ec-82a8-dac502259ad0.png

該函數最后會調用start_thread函數,進而會調用start_thread_common函數:

38a84222-22a0-11ec-82a8-dac502259ad0.png

這個函數重點需要注意的是對regs-》ip和regs-》sp的賦值,其作用在load_elf_binary函數的截圖中已經注釋過了,就是在返回到用戶區(qū)之后,這兩個字段的值會被分別拷貝到rip和rsp寄存器里,所以這里的賦值,就相當于在返回用戶區(qū)之后,對rip和rsp寄存器的賦值,這個在 精致全景圖 | 系統(tǒng)調用是如何實現(xiàn)的 有講。

到這里內核部分的代碼就都已經結束了。

由load_elf_binary函數截圖中可見,regs-》ip中設置的地址是elf_entry,即動態(tài)鏈接器的起始地址,而不是我們自己程序的起始地址。

原因是,我們還需要動態(tài)鏈接器繼續(xù)幫我們準備執(zhí)行環(huán)境,比如幫我們加載程序依賴的動態(tài)鏈接庫等。

所以在execve系統(tǒng)調用返回到用戶區(qū)之后,代碼流程就進入到了動態(tài)鏈接器里的邏輯,即上面全景圖中的紫色區(qū)域:

38b7ad3e-22a0-11ec-82a8-dac502259ad0.png

上圖中的_start是動態(tài)鏈接器的起始執(zhí)行地址,這個可以通過下面的方式來確認:

38fd1e50-22a0-11ec-82a8-dac502259ad0.png

在_start函數中,先將rsp寄存器的值,即上面內核新初始化的堆棧的棧頂地址,賦值到rdi中,然后再使用call指令,調用_dl_start函數。

之所以要賦值到rdi寄存器中,是因為c語言的calling convention約定好的,用此方式來傳遞參數。

再看_dl_start函數:

391105f0-22a0-11ec-82a8-dac502259ad0.png

該函數調用了_dl_start_final,返回一個地址,這個地址就是我們自己程序的起始地址。

再看_dl_start_final:

393b5134-22a0-11ec-82a8-dac502259ad0.png

該函數又調用了_dl_sysdep_start:

3974678a-22a0-11ec-82a8-dac502259ad0.png

在這里,動態(tài)鏈接器通過內核初始化的堆棧區(qū)中的auxiliary vector,找到最終用戶程序的起始執(zhí)行地址。

再之后,動態(tài)鏈接器的函數調用鏈依次退出,最終返回到上面的_start函數。

_start函數之后會順序執(zhí)行_dl_start_user,相關代碼也在上面的_start函數的截圖里。

其邏輯是,先將rax中的值,即_dl_start函數返回的最終用戶程序的起始地址,賦值到r12寄存器中,然后再jmp到r12寄存器指向的地址,即開始執(zhí)行最終的用戶程序邏輯。

至于rax中的值,為什么是_dl_start函數返回的地址,這個其實也是 c calling convention 中的約定,感興趣可以自己查下。

以上就是動態(tài)鏈接器的全部邏輯,其對應于全景圖中的紫色部分。

最后,邏輯進入到了全景圖中的粉色部分。

動態(tài)鏈接器從內核設置的auxiliary vector中,獲取的用戶程序的起始地址,還并不是我們的main函數,而是glibc中一段名為_start的代碼,這個可以通過下面的方式確認:

39c36312-22a0-11ec-82a8-dac502259ad0.png

該_start代碼段內容如下:

39f2140a-22a0-11ec-82a8-dac502259ad0.png

它從堆棧中獲取到argc和argv,然后調用__libc_start_main:

3a222078-22a0-11ec-82a8-dac502259ad0.png

在__libc_start_main里,才真正的調用了我們寫的main函數。

當main函數返回之后,__libc_start_main里用main函數返回的值,作為該進程的退出碼,然后調用exit退出當前進程。

當該進程退出后,shell主進程也從waitpid的阻塞狀態(tài)返回,并攜帶用戶程序的退出碼。

在上面全景圖這個示例中,返回碼為99:

3a58d370-22a0-11ec-82a8-dac502259ad0.png

之后,shell主進程又進入到下一次循環(huán),繼續(xù)等待用戶命令并執(zhí)行,也就是說,又進入到全景圖中的藍色部分。

至此,在linux上執(zhí)行程序的流程,就形成了一個完整閉環(huán)。

你,學廢了嗎?

責任編輯:haq

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 程序
    +關注

    關注

    115

    文章

    3747

    瀏覽量

    80667
  • Shell
    +關注

    關注

    1

    文章

    361

    瀏覽量

    23234

原文標題:精致全景圖 | 程序是如何運行起來的

文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    STM32L011F4P6運行起來的原因?

    stlink_v2仿真器,仿真程序 ,運行的很好,旦把仿真器去掉,再加電后,發(fā)現(xiàn) 程序沒有運行起來
    發(fā)表于 04-15 07:47

    程序運行后為什么沒有結果

    是最常見的問題之??赡艽嬖谄磳戝e誤、語法錯誤、缺少分號等問題,這些錯誤會導致程序無法正常運行。此時,可以使用調試工具(如IDE中的調試器)來逐行檢查代碼,找出錯誤所在,并進行修正。 其次,
    的頭像 發(fā)表于 01-09 11:00 ?2728次閱讀

    idea如何多次運行程序

    些實際的示例。 內容: 、使用循環(huán)結構 1.1 for循環(huán) 最常用的多次運行程序的方法之是使用循環(huán)結構。其中最常用的循環(huán)結構是for循環(huán)。for循環(huán)可以重復執(zhí)行
    的頭像 發(fā)表于 12-06 14:59 ?1236次閱讀

    運行Python程序的幾種常見方法

    Python是種高級編程語言,具有簡單易學,易于閱讀和調試的特點。當你完成了Python程序之后,你需要運行它以檢查
    的頭像 發(fā)表于 11-28 15:32 ?2147次閱讀

    vim編寫程序后怎么運行

    當你用 Vim 編寫完程序后,你需要執(zhí)行系列步驟來運行程序。下面是詳盡的說明: 保存文件:在 Vim 編輯器中,按下 Esc 鍵,然后
    的頭像 發(fā)表于 11-28 15:27 ?2999次閱讀

    運行c程序的基本步驟

    運行C程序的基本步驟可以分為以下四主要步驟:編寫程序、編譯程序、鏈接程序
    的頭像 發(fā)表于 11-27 16:21 ?5079次閱讀

    c程序運行環(huán)境和運行c程序的方法

    C語言是種通用的、高效的編程語言,被廣泛用于系統(tǒng)開發(fā)、嵌入式設備以及科學計算等領域。為了正確運行C程序,我們需要了解C程序運行環(huán)境,并掌
    的頭像 發(fā)表于 11-27 16:00 ?1835次閱讀

    如何運行Python程序

    運行Python程序非常簡單。Python是種解釋型語言,這意味著可以直接通過解釋器來執(zhí)行代碼。下面我將詳細介紹如何運行Python程序。
    的頭像 發(fā)表于 11-24 09:31 ?1070次閱讀

    python怎么運行程序

    Python是種廣泛使用的編程語言,它的簡易和可讀性使得它成為初學者和專業(yè)開發(fā)人員的首選。在運行Python程序之前,您需要安裝Python解釋器,然后按照以下步驟進行操作。 步驟1:安裝
    的頭像 發(fā)表于 11-24 09:25 ?2429次閱讀

    python運行次自動再次運行

    自動化是現(xiàn)代計算機科學和軟件開發(fā)的重要領域。在Python中,有許多方法可以實現(xiàn)自動運行程序的功能。本文將詳細介紹如何使用Python實現(xiàn)自動運行程序的不同方法。
    的頭像 發(fā)表于 11-23 15:52 ?1652次閱讀

    pycharm怎么看程序運行到哪里了

    查看程序運行狀態(tài)。 首先,我們需要確保已經在PyCharm中打開了要調試的項目。在項目中選擇運行的源代碼文件,右鍵點擊這個文件并選擇
    的頭像 發(fā)表于 11-22 11:18 ?4849次閱讀

    python寫完程序之后怎么運行

    Python是門簡潔、易學的編程語言,被廣泛應用于數據分析、人工智能等領域。在學習Python編程的過程中,了解程序運行機制是至關重要的。本文將詳盡解析Python程序
    的頭像 發(fā)表于 11-22 11:10 ?900次閱讀

    線程模擬單片機程序框架分享

    首先來個demo,該demo是使用電腦開兩線程:線程模擬單片機的定時器中斷產生時間片輪詢時鐘,另一個線程則模擬主函數中
    發(fā)表于 11-19 10:39 ?2263次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>個</b>線程模擬單片機<b class='flag-5'>程序</b>框架分享

    虛擬機如何運行c程序

    虛擬機(Virtual Machine,VM)是種模擬了物理計算機的軟件,可以在計算機上創(chuàng)建虛擬的硬件平臺,使得用戶可以在其中運行操作系統(tǒng)和應用程序。在虛擬機中運行C
    的頭像 發(fā)表于 11-17 10:14 ?4401次閱讀

    如何實現(xiàn)LED流水燈程序

    define是宏定義,程序在預處理階段將用define定義的內容進行了替換 。因此在程序運行時,常量表中并沒有用define定義的常量,系統(tǒng)不為它分配內存。
    的頭像 發(fā)表于 11-10 11:37 ?2172次閱讀
    如何實現(xiàn)<b class='flag-5'>一</b><b class='flag-5'>個</b>LED流水燈<b class='flag-5'>程序</b>