<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          令人頭疼的Segmentation fault到底是怎么產(chǎn)生的

          共 5786字,需瀏覽 12分鐘

           ·

          2022-08-22 10:35

          底層原理系列文章鏈接如下,持續(xù)更新中:

          徹底理解Linux文件系統(tǒng)(一)

          徹底理解Linux文件系統(tǒng)(二)

          徹底理解Linux文件系統(tǒng)(三)

          徹底理解Linux文件系統(tǒng)(四)

          一道高頻騰訊面試題:tcp數(shù)據(jù)發(fā)送問(wèn)題

          Linux swap area是個(gè)什么東西

          今天來(lái)聊聊Linux環(huán)境下開(kāi)發(fā)時(shí)經(jīng)常遇到的“Segmentation fault”的問(wèn)題其究竟是怎么產(chǎn)生的。

          Linux上開(kāi)發(fā)時(shí)最讓人頭疼的問(wèn)題之一就是遇到“Segmentation Fault”錯(cuò)誤。為什么這么說(shuō),很多人看到這個(gè)錯(cuò)誤后心里第一反應(yīng)是程序訪問(wèn)的非法的內(nèi)存,這固然沒(méi)錯(cuò),可這里有個(gè)比較模糊的概念了:什么叫“非法”的內(nèi)存?

          在Linux下,每個(gè)進(jìn)程都有自己的虛擬地址,理論上說(shuō)進(jìn)程應(yīng)該可以隨便使用才對(duì),為什么還會(huì)出現(xiàn)這個(gè)錯(cuò)誤呢?這里就涉及到程序的裝載過(guò)程及原理。

          做過(guò)C/C++開(kāi)發(fā)的同學(xué)應(yīng)該知道, Linux中可執(zhí)行文件的格式是ELF ,編譯過(guò)程中的中間文件*.o文件、動(dòng)態(tài)共享庫(kù)*.so文件其實(shí)也是ELF格式的。 在鏈接器看來(lái) ,它對(duì)*.o文件、動(dòng)態(tài)共享庫(kù)*.so文件這兩種ELF格式的文件以鏈接視圖(Linking View)進(jìn)行看待 。

          當(dāng)程序最終需要被裝載成進(jìn)程時(shí),裝載器就出場(chǎng)了,裝載器將可執(zhí)行文件以裝載視圖進(jìn)行對(duì)待處理。這些概念不太理解的同學(xué)可以去閱讀一本叫程序員的自我修養(yǎng)--鏈接、裝載與庫(kù)的書(shū)籍,這是國(guó)人為數(shù)不多寫(xiě)的比較好的一本技術(shù)書(shū)籍。

          前面說(shuō)了*.o、*.so和可執(zhí)行文件都是ELF格式的文件,那么鏈接器和裝載器是如何區(qū)分哪個(gè)ELF文件是可執(zhí)行文件,哪個(gè)文件又是.so文件?首先強(qiáng)調(diào)的是肯定不是通過(guò)文件名后綴來(lái)區(qū)分的,Linux系統(tǒng)下對(duì)文件名后綴是不敏感的,我們還是先看一個(gè)簡(jiǎn)單的例子:f8da950d822715e3d0f794b6986d3373.webp

          readelf –h命令用于輸出EFL文件的頭部信息。因?yàn)関iewobj.o是編譯時(shí)的中間文件,可以看到它的“Start of pgrogram headers”和“Number of program headers”都為0,這也證明它不是一個(gè)可執(zhí)行文件。取而代之的是它有9個(gè)section,所以它有“Start of section headers”和“Number of section headers”都有數(shù)據(jù)。

          再看一下動(dòng)態(tài)共享庫(kù)的EFL文件的頭部信息 46da45c9049b1b7f2a1011bf9b1aefe3.webp

          在Linux下動(dòng)態(tài)共享庫(kù)被當(dāng)作可執(zhí)行文件來(lái)處理,雖然它不能單獨(dú)執(zhí)行,但某些應(yīng)用程序的運(yùn)行離不了它。

          最后是可執(zhí)行文件EFL文件的頭部信息,還是看示例:28a6d54855485d1de41da9dfa15360f3.webp所以,我們可以得到這樣一個(gè)結(jié)論:一個(gè)具體的ELF文件,其文件頭部中的某些屬性值,指明了它到底是可執(zhí)行文件還是可重定位文件(o和.so的統(tǒng)稱(chēng))。這樣,鏈接器和裝載器通過(guò)分析ELF文件頭部就可以知道它該怎么處理該文件了。用比較直觀的、方便理解的圖來(lái)表示它們的區(qū)別就是:f39105cb16d8b37c92db6709bfa9ed92.webp

          也就是說(shuō)鏈接的時(shí)候Program Header Table是可選的,但Section Header Table是必須有的。 例如*.o就沒(méi)有Program Header Table,而*.so就有。 裝載的時(shí)候Program Header Table必須有,但Section Header Table是可選的,但即使有Section Header Table,裝載器也不會(huì)鳥(niǎo)它。

          那么,裝載器為什么要采取和鏈接器不同的處理策略呢?最主要的原因是為了提高內(nèi)存的利用率?,F(xiàn)代操作系統(tǒng)在裝載程序時(shí)都充分利用程序的局部性原理,那就是,當(dāng)進(jìn)程運(yùn)行時(shí),并不需要一下子將程序的所有代碼和數(shù)據(jù)都裝載到內(nèi)存里,而是先裝載程序的一部分到內(nèi)存里運(yùn)行。當(dāng)進(jìn)程將要執(zhí)行的指令不在內(nèi)存里的話,CPU便會(huì)觸發(fā)一個(gè)缺頁(yè)異常,操作系統(tǒng)捕獲到這樣的異常后便接管進(jìn)程,然后將需要的指令“弄”到內(nèi)存里,再將執(zhí)行權(quán)限還給進(jìn)程。

          進(jìn)程運(yùn)行的時(shí)候,它虛擬地址空間和對(duì)應(yīng)的物理內(nèi)存到底是怎么關(guān)聯(lián)上的了?一般是通過(guò)一個(gè)叫做MMU(內(nèi)存管理單元)的東東完成了從進(jìn)程虛擬地址到物理地址之間的映射。

          進(jìn)程虛擬地址空間的任何地址,在使用前都必須通過(guò)MMU將其映射到物理內(nèi)存上一個(gè)實(shí)實(shí)在在的存儲(chǔ)單元上才是合法有效的。而對(duì)于那些沒(méi)有經(jīng)過(guò)MMU映射過(guò)的虛擬空間的地址,在對(duì)其進(jìn)行讀寫(xiě)操作時(shí),對(duì)于操作系統(tǒng)來(lái)說(shuō)都是一個(gè)錯(cuò)誤的非法內(nèi)存訪問(wèn),就會(huì)報(bào)“Segmentation Fault”的錯(cuò)誤提示信息并強(qiáng)行終止進(jìn)程。

          那么,問(wèn)題又來(lái)了,到底哪些地址才是合法有效或者能被MMU映射的呢?下面是一個(gè)Linux進(jìn)程虛擬地址空間分布圖:b25a9fbd06e0511a47f11622c3971f25.webp

          上圖是32位系統(tǒng)下Linux進(jìn)程虛擬地址空間的布局結(jié)構(gòu)圖,其中0x0804800為進(jìn)程運(yùn)行時(shí)的地址入口(你的程序的第一條指令的入口地址),當(dāng)進(jìn)程運(yùn)行時(shí),完成進(jìn)程環(huán)境空間的初始化工作之后就會(huì)跳到這個(gè)地址執(zhí)行我們程序里的第一條指令。

          0x0804800這個(gè)地址一般由鏈接器在生成可執(zhí)行文件時(shí)就已經(jīng)固定了,通常無(wú)需更改。當(dāng)然,如果你一定要修改也是有辦法修改的,這個(gè)不在本文的討論范圍內(nèi)。上圖中,當(dāng)用戶的程序直接訪問(wèn)0x084800以前的地址、0xC0000000以后的地址或者free空間里的地址都會(huì)觸發(fā)“Segmentation Fault”。原因如下:

          • ? 0x084800以前的地址、0xC0000000以后的地址:由于權(quán)限的問(wèn)題, 不允許進(jìn)程直接訪問(wèn) ,操作系統(tǒng)對(duì)其進(jìn)行保護(hù)。

          • ? free地址段的空間就是前面說(shuō)的,由于沒(méi)有經(jīng)過(guò)MMU將其映射到物理內(nèi)存的實(shí)際存儲(chǔ)單元上,當(dāng)程序訪問(wèn)System break(也就是常說(shuō)的brk)之后的地址就會(huì)引發(fā)段錯(cuò)誤。brk一般是進(jìn)程堆空間結(jié)束的地方。那么,我們?nèi)绾沃喇?dāng)前進(jìn)程的brk在什么地方呢?可以通過(guò)庫(kù)函數(shù)sbrk()來(lái)獲取。與此配套的還有一個(gè)函數(shù)brk()用來(lái)設(shè)置System break的位置。

          現(xiàn)代很多操作系統(tǒng)實(shí)現(xiàn)中,為了防止溢出攻擊,都做了隨機(jī)地址保護(hù):當(dāng)程序運(yùn)行時(shí),代碼段、堆棧段的裝載起始地址并不是固定不變的,而是每次運(yùn)行進(jìn)程時(shí)都會(huì)加上一個(gè)隨機(jī)的偏移量,這會(huì)影響我們的測(cè)試效果。

          為了不影響我們的測(cè)試效果,后續(xù)的實(shí)驗(yàn)中,我們將內(nèi)核的隨機(jī)地址保護(hù)機(jī)制關(guān)掉,關(guān)閉它的方法很簡(jiǎn)單:
                
                  [root@localhost?~]#echo?"0"?>?/proc/sys/kernel/randomize_va_space
                
              
          如果/proc/sys/kernel/randomize_va_space為0則表示,進(jìn)程每次啟動(dòng)運(yùn)行時(shí),其虛擬地址空間里的值就是它在ELF文件里所指定的值,下面的一段代碼是網(wǎng)上流傳比較多的一段很典型的代碼,這里就直接借鑒過(guò)來(lái)用了:8d25c01cf8b6805664e106d1987dad90.webp由于全局變量bssvar未初始化,所以當(dāng)程序運(yùn)行時(shí)它會(huì)被放置在.bss段,占4字節(jié)。sbrk(0)函數(shù)返回當(dāng)前brk的值。為了便于觀察,我們?cè)诔绦蛑姓{(diào)用了sleep函數(shù)睡眠幾秒。然后用readelf看一下可執(zhí)行文件被裝載時(shí),Segement的情況將會(huì)是什么樣子:bf24c5c318bf08d392e7b4ead4877fde.webp還需要說(shuō)明一點(diǎn)的是,內(nèi)存分配時(shí)是以頁(yè)為單位,一般情況下頁(yè)大小為4096字節(jié),所以從0x08048000開(kāi)始是代碼段,共占內(nèi)存0x00628,即1576個(gè)字節(jié),雖然不足一個(gè)頁(yè),但是數(shù)據(jù)頁(yè)的分配也必須從下一個(gè)頁(yè)開(kāi)始,即從0x0804900開(kāi)始。但上面顯示卻說(shuō)數(shù)據(jù)頁(yè)從0x08049628開(kāi)始,但注意最后一列Allign,指明了對(duì)其方式,正好是4096字節(jié)。驗(yàn)證一下:cec633a7fbdd735350baf7c96173ca3a.webp這里我們看到操作系統(tǒng)確實(shí)是以頁(yè)為單位進(jìn)行內(nèi)存分配。這里需要注意的一點(diǎn)是,對(duì)應(yīng)heap來(lái)說(shuō),默認(rèn)情況,.bss段結(jié)束地址就是heap的開(kāi)始地址。當(dāng)你的代碼中沒(méi)有調(diào)用malloc()之類(lèi)的動(dòng)態(tài)內(nèi)存分配函數(shù)時(shí),在查看進(jìn)程的內(nèi)存映射時(shí)是看不到heap的。此時(shí)的進(jìn)程虛擬地址空間的布局下:2f9348da0b23e8e7dce7f60b8d3c47a2.webp我們可以知道,當(dāng)程序訪問(wèn)0x0848000~0x0849FFF之間的所有數(shù)據(jù)都是OK的,當(dāng)訪問(wèn)到0x084A000及其之后的地址就會(huì)報(bào)“Segmentation Fault”,因?yàn)槲覀兊腷rk剛好到這里。不信??好吧,把上面程序簡(jiǎn)單調(diào)整一下:
                
                  #include?<stdio.h>
          #include?<stdlib.h>
          #include?<unistd.h>

          int?bssvar;

          int?main(int?argc,char**?argv)
          {
          ????????void?*ptr;

          ????????printf("main?start?=?%p\n",main);
          ????????printf("bss?end?=?%p\n",(long)&bssvar+4);
          ????????ptr=sbrk(0);
          ????????printf("current?brk?=?%p\n",(long*)ptr);
          ????????sleep(8);

          ????????int?i=0x08049628;
          ????????for(;;i++)
          ????????????????printf("At:0x%x-0x%x\n",i,*((char*)i));
          }
          重新編譯運(yùn)行該程序,最后出現(xiàn)“Segmentation Fault”時(shí)應(yīng)該是下面這個(gè)樣子:f5ad3b6188f3b2407c77707afb6ce3df.webp

          當(dāng)你的源代碼中有用到諸如malloc()之類(lèi)的動(dòng)態(tài)內(nèi)存申請(qǐng)函數(shù)時(shí),brk的值會(huì)被相應(yīng)的往高端內(nèi)存的位置進(jìn)行調(diào)整,這樣調(diào)整出來(lái)的一段內(nèi)存就被所謂的內(nèi)存管理器,也就是著名的buddy system納入管理范圍了。這樣當(dāng)我們?cè)僭L問(wèn)這些地址時(shí),就不會(huì)報(bào)“Segmentation Fault”了。其實(shí)如果你看過(guò)Glibc源碼你就會(huì)驚奇的發(fā)現(xiàn),malloc()最終也是通過(guò)調(diào)用brk()系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)堆的管理。所以,如果我們把上述代碼再做一下簡(jiǎn)單修改:

                
                  #include?<stdio.h>
          #include?<stdlib.h>
          #include?<unistd.h>

          int?bssvar;

          int?main(int?argc,char**?argv)
          {
          ????????void?*ptr = NULL;

          ????????printf("main?start?=?%p\n",main);
          ????????printf("bss?end?=?%p\n",(long)&bssvar+4);
          ????????ptr=sbrk(0);
          ????????printf("current?brk?=?%p\n",(long*)ptr);
          ????????sleep(8);
          ????????int?i=0x08049628;
          ????????brk((char*)0x804A123);?//注意這行
          ????????for(;;i++)
          ????????????????printf("At:0x%x-0x%x\n",i,*((char*)i));
          }
          我們用brk()系統(tǒng)調(diào)用,手動(dòng)把brk調(diào)整到0x804A123處,再編譯運(yùn)行,你就會(huì)得到下面這樣的結(jié)果:7a1ef0385ab20aa0a5fc4c6b989a14d3.webp

          至于是為什么不在0x804A123處報(bào)“Segmentation Fault”而是要跑到0x804B000處才報(bào),原因已經(jīng)不止一次的強(qiáng)調(diào)了,如果還是不清楚的同學(xué)建議從頭再看一遍。

          好了,現(xiàn)在我們知道了,程序之所以會(huì)出現(xiàn)“Segmentation Fault”,其原因就是因?yàn)檫M(jìn)程訪問(wèn)到了沒(méi)有訪問(wèn)權(quán)限的地方。

          所以,對(duì)于程序員特別是C/C++程序員來(lái)說(shuō),良好的關(guān)于指針的使用習(xí)慣是,使用之前先判斷其是否為NULL,所有已經(jīng)歸還給操作系統(tǒng)的內(nèi)存,其訪問(wèn)指針都要及時(shí)置為NULL,防止所謂的“野指針”到處飛的情況,代碼中盡量使用智能指針等等。

          本文示例代碼環(huán)境:
          內(nèi)核:2.6.32-279
          glibc版本:2.12
          GCC版本: 4.4.6

          —??—

          歡迎關(guān)注原創(chuàng)技術(shù)號(hào)↓ ↓↓ 如有幫助,辛苦點(diǎn)贊和在看
          瀏覽 179
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  香蕉国产2024 | 黄片视频2019 | 欧美丰满熟妇XXX | 大香蕉18| 伊人网成人综合 |