www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當前位置:首頁 > 公眾號精選 > Linux閱碼場
[導(dǎo)讀]因為圖片比較大,微信公眾號上壓縮的比較厲害,所以很多細節(jié)都看不清了,我單獨傳了一份到github上,想要原版圖片的,可以點擊下方的閱讀原文,或者直接使用下面的鏈接,來訪問github:https://github.com/wangyuntao/linux-kernel-illus...



因為圖片比較大,微信公眾號上壓縮的比較厲害,所以很多細節(jié)都看不清了,我單獨傳了一份到github上,想要原版圖片的,可以點擊下方的閱讀原文,或者直接使用下面的鏈接,來訪問github:




另外,精致全景圖系列文章,以及之后的linux內(nèi)核分析文章,我都會整理到這個github倉庫里,歡迎大家star收藏。





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



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



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



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



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



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



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



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



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



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



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



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



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



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



之后,load_elf_binary函數(shù)會調(diào)用start_thread,進而會調(diào)用start_thread_common函數(shù)。



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



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



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



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



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



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



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



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



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



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



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



這些代碼對應(yīng)于上面全景圖中的粉色部分。



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



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



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



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



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





該函數(shù)里調(diào)用了reader_loop:





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



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





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





在shell主進程中,會調(diào)用waitpid函數(shù),阻塞等待子進程的完成:





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



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



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



當shell的子進程執(zhí)行execve函數(shù)時,linux內(nèi)核中對應(yīng)的系統(tǒng)調(diào)用被觸發(fā):





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





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



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



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





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



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



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



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





該函數(shù)最后會調(diào)用start_thread函數(shù),進而會調(diào)用start_thread_common函數(shù):





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



到這里內(nèi)核部分的代碼就都已經(jīng)結(jié)束了。



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



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



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





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





在_start函數(shù)中,先將rsp寄存器的值,即上面內(nèi)核新初始化的堆棧的棧頂?shù)刂?,賦值到rdi中,然后再使用call指令,調(diào)用_dl_start函數(shù)。



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



再看_dl_start函數(shù):





該函數(shù)調(diào)用了_dl_start_final,返回一個地址,這個地址就是我們自己程序的起始地址。



再看_dl_start_final





該函數(shù)又調(diào)用了_dl_sysdep_start:





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



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



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



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



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



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



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



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





該_start代碼段內(nèi)容如下:





它從堆棧中獲取到argc和argv,然后調(diào)用__libc_start_main:





__libc_start_main里,才真正的調(diào)用了我們寫的main函數(shù)。



當main函數(shù)返回之后,__libc_start_main里用main函數(shù)返回的值,作為該進程的退出碼,然后調(diào)用exit退出當前進程。



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



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





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



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



你,學廢了嗎?



能看到這里的,都是真愛了,給個贊再走吧。



另外,沒有關(guān)注我公眾號的也可以關(guān)注下,一起來探索linux內(nèi)核里的神秘世界。



本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
關(guān)閉
關(guān)閉