Linux内存管理内存映射以及通过反汇编定位内存错误问题
提到C語言,我們知道C語言和其他高級語言的最大的區別就是C語言是要操作內存的!
?????我們需要知道——變量,其實是內存地址的一個抽像名字罷了。在靜態編譯的程序中,所有的變量名都會在編譯時被轉成內存地址。機器是不知道我們取的名字的,只知道地址。
????內存的使用時程序設計中需要考慮的重要因素之一,這不僅由于系統內存是有限的(尤其在嵌入式系統中),而且內存分配也會直接影響到程序的效率。因此,我們要對C語言中的內存管理,有個系統的了解。
????在C語言中,定義了4個內存區間:代碼區;全局變量和靜態變量區;局部變量區即棧區;動態存儲區,即堆區;具體如下:
????1、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似于數據結構中的棧。
????2、堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似于鏈表,呵呵。
????3、全局區(靜態區)(static)—全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的 另一塊區域。 - 程序結束后由系統釋放。
???4、常量區 —常量字符串就是放在這里的。 程序結束后由系統釋放
???5、程序代碼區—存放函數體的二進制代碼。
我們來看張圖:
首先我們要知道,源代碼編譯成程序,程序是放在硬盤上的,而非內存里!只有執行時才會被調用到內存中!
我們來看看程序結構,ELF是是Linux的主要可執行文件格式。ELF文件由4部分組成,分別是ELF頭(ELF header)、程序頭表(Program header table)、節(Section)和節頭表(Section header table)。具體如下:
1、Program header描述的是一個段在文件中的位置、大小以及它被放進內存后所在的位置和大小。即要加載的信息;
2、Sections保存著object 文件的信息,從連接角度看:包括指令,數據,符號表,重定位信息等等。在圖中,我們可以看到Sections中包括:
??????(1)??.text???文本結 存放指令;
??????(2)??.rodata???數據結??readonly;
??????(3)??.data??數據結?可讀可寫;?
3、Section頭表(section header table)包含了描述文件sections的信息。每個section在這個表中有一個入口;每個入口給出了該section的名字,大小,等等信息。相當于 索引!
?????而程序被加載到內存里面,又是如何分布的呢?我們看看上圖中:
1、正文和初始化的數據和未初始化的數據就是我們所說的數據段,正文即代碼段;
2、正文段上面是常量區,常量區上面是全局變量和靜態變量區,二者占據的就是初始化的數據和未初始化的數據那部分;
3、再上面就是堆,動態存儲區,這里是上增長;
4、堆上面是棧,存放的是局部變量,就是局部變量所在代碼塊執行完畢后,這塊內存會被釋放,這里棧區是下增長;
5、命令行參數就是$0 $1之類的,環境變量什么的前面的文章已經講過,有興趣的可以去看看。
?
????我們知道,內存分為動態內存和靜態內存,我們先講靜態內存。
一、靜態內存
內存管理---存儲模型
???????存儲模型決定了一個變量的內存分配方式和訪問特性,在C語言中主要有三個維度來決定:1、存儲時期 2、作用域 3、鏈接
1、存儲時期
? ? 存儲時期:變量在內存中的保留時間(生命周期)
? ? 存儲時期分為兩種情況,關鍵是看變量在程序執行過程中會不會被系統自動回收掉。
?1)??靜態存儲時期 Static
????????在程序執行過程中一旦分配就不會被自動回收。
????????通常來說,任何不在函數級別代碼塊內定義的變量。
????????無論是否在代碼塊內,只要采用static關鍵字修飾的變量。
?2) 自動存儲時期??Automatic
????????除了靜態存儲以外的變量都是自動存儲時期的,或者說只要是在代碼塊內定義的非static的變量,系統會肚臍自動非配和釋放內存;
?
2、作用域
??????作用域:一個變量在定義該變量的自身文件中的可見性(訪問或者引用)
???????在C語言中,一共有3中作用域:
1)??代碼塊作用域
?????在代碼塊中定義的變量都具有該代碼的作用域。從這個變量定義地方開始,到這個代碼塊結束,該變量是可見的;
2)??函數原型作用域
????出現在函數原型中的變量,都具有函數原型作用域,函數原型作用域從變量定義處一直到原型聲明的末尾。
3)??文件作用域
????一個在所有函數之外定義的變量具有文件作用域,具有文件作用域的變量從它的定義處到包含該定義的文件結尾處都是可見的;
?
3、鏈接
??????鏈接:一個變量在組成程序的所有文件中的可見性(訪問或者引用);
??????C語言中一共有三種不同的鏈接:
1)??外部鏈接
??????如果一個變量在組成一個程序的所有文件中的任何位置都可以被訪問,則稱該變量支持外部鏈接;
2)??內部鏈接
????如果一個變量只可以在定義其自身的文件中的任何位置被訪問,則稱該變量支持內部鏈接。
3)??空鏈接???
??????如果一個變量只是被定義其自身的當前代碼塊所私有,不能被程序的其他部分所訪問,則成該變量支持空鏈接
我們來看一個代碼示例:
二、動態內存
? ? 當程序運行到需要一個動態分配的變量時,必須向系統申請取得堆中的一塊所需大小的存儲空間,用于存儲該變量。當不在使用該變量時,也就是它的生命結束時,要顯示釋放它所占用的存儲空間,這樣系統就能對該空間?進行再次分配,做到重復使用有線的資源。下面介紹動態內存申請和釋放的函數。
1.1 malloc 函數
?malloc函數原型:
size是需要動態申請的內存的字節數。若申請成功,函數返回申請到的內存的起始地址,若申請失敗,返回NULL。我們看下面這個例子:
int *get_memory(int n) {int *p;p = (int *)malloc(sizeof(int));if(p == NULL){printf("malloc error\n");return p;}memset(p,0,n*sizeof(int)); }使用該函數時,有下面幾點要注意:
1)只關心申請內存的大小;
2)申請的是一塊連續的內存。記得一定要寫出錯判斷;
3)顯示初始化。即我們不知這塊內存中有什么東西,要對其清零;
?
1.2 free函數
????在堆上分配的額內存,需要用free函數顯示釋放,函數原型如下:
使用free(),也有下面幾點要注意:
1)必須提供內存的起始地址;
????調用該函數時,必須提供內存的起始地址,不能夠提供部分地址,釋放內存中的一部分是不允許的。
2)malloc和free配對使用;
? ? 編譯器不負責動態內存的釋放,需要程序員顯示釋放。因此,malloc與free是配對使用的,避免內存泄漏。
free(p);
p = NULL;
p = NULL是必須的,因為雖然這塊內存被釋放了,但是p仍指向這塊內存,避免下次對p的誤操作;
3)不允許重復釋放
因為這塊內存被釋放后,可能已另分配,這塊區域被別人占用,如果再次釋放,會造成數據丟失;
?
2、我們經常將堆和棧相比較:
2.1申請方式??
stack:??由系統自動分配。 例如,聲明在函數中一個局部變量 int b; 系統自動在棧中為b開辟空間??
heap:??需要程序員自己申請,并指明大小,在c中malloc函數 ,如p1 = (char *)malloc(10);??
??
2.2??申請后系統的響應??
棧:只要棧的剩余空間大于所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。??
堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,并將該結點的空間分配給程序,另外,對于大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。?
2.3申請大小的限制??
棧:棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。?
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。?
2.4申請效率的比較:??
棧由系統自動分配,速度較快。但程序員是無法控制的。??
堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便。?
2.5堆和棧中的存儲內容??
棧: 在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。??當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。?
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容由程序員安排。?
2.6存取效率的比較?
char s1[] = "aaaaaaaaaaaaaaa";??
char *s2 = "bbbbbbbbbbbbbbbbb";??
aaaaaaaaaaa是在運行時刻賦值的;??
而bbbbbbbbbbb是在編譯時就確定的;??
但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。??
比如:??
對應的匯編代碼?
0: a = c[1]; ? 00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] ? 0040106A 88 4D FC mov byte ptr [ebp-4],cl ? 11: a = p[1]; ? 0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] ? 00401070 8A 42 01 mov al,byte ptr [edx+1] ? 00401073 88 45 FC mov byte ptr [ebp-4],al ?第一種在讀取時直接就把字符串中的元素讀到寄存器cl中,而第二種則要先把指針值讀到edx中,再根據edx讀取字符,顯然慢了。?
2.7小結:??
???堆和棧的區別可以用如下的比喻來看出:??
???棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。??
???堆就象是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大。
補充知識點一:
線程和進程各自有什么區別和優劣呢?
? ? ?進程是資源分配的最小單位,線程是程序執行的最小單位。
? ? ?進程有自己的獨立地址空間,每啟動一個進程,系統就會為它分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,這種操作非常昂貴。而線程是共享進程中的數據的,使用相同的地址空間,因此CPU切換一個線程的花費遠比進程要小很多,同時創建一個線程的開銷也比進程要小很多。
? ? ?線程之間的通信更方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通信需要以通信的方式(IPC)進行。不過如何處理好同步與互斥是編寫多線程程序的難點。
? ? ?但是多進程程序更健壯,多線程程序只要有一個線程死掉,整個進程也死掉了,而一個進程死掉并不會對另外一個進程造成影響,因為進程有自己獨立的地址空間。
補充知識點二:
? ? BSS段通常是指用來存放程序中未初始化的全局變量和靜態變量的一塊內存區域。特點是可讀寫的,在程序執行之前BSS段會自動清0。
? ? 可執行程序包括BSS段、數據段、代碼段(也稱文本段)。
? ? BSS(Block Started by Symbol)通常是指用來存放程序中未初始化的全局變量和靜態變量的一塊內存區域。特點是:可讀寫的,在程序執行之前BSS段會自動清0。所以,未初始的全局變量在程序執行之前已經成0了。
? ? 數據段包括初始化的數據和未初始化的數據(BSS)兩部分 。BSS段存放的是未初始化的全局變量和靜態變量。
? ? 可使用size命令查看可執行文件的段大小信息。如size a.out。
Linux驅動mmap內存映射
mmap在linux哪里?
什么是mmap?
上圖說了,mmap是操作這些設備的一種方法,所謂操作設備,比如IO端口(點亮一個LED)、LCD控制器、磁盤控制器,實際上就是往設備的物理地址讀寫數據。
但是,由于應用程序不能直接操作設備硬件地址,所以操作系統提供了這樣的一種機制——內存映射,把設備地址映射到進程虛擬地址,mmap就是實現內存映射的接口。
操作設備還有很多方法,如ioctl、ioremap
mmap的好處是,mmap把設備內存映射到虛擬內存,則用戶操作虛擬內存相當于直接操作設備了,省去了用戶空間到內核空間的復制過程,相對IO操作來說,增加了數據的吞吐量。
什么是內存映射?
既然mmap是實現內存映射的接口,那么內存映射是什么呢?看下圖
每個進程都有獨立的進程地址空間,通過頁表和MMU,可將虛擬地址轉換為物理地址,每個進程都有獨立的頁表數據,這可解釋為什么兩個不同進程相同的虛擬地址,卻對應不同的物理地址。
什么是虛擬地址空間?
每個進程都有4G的虛擬地址空間,其中3G用戶空間,1G內核空間(linux),每個進程共享內核空間,獨立的用戶空間,下圖形象地表達了這點
驅動程序運行在內核空間,所以驅動程序是面向所有進程的。
用戶空間切換到內核空間有兩種方法:
(1)系統調用,即軟中斷
(2)硬件中斷
虛擬地址空間里面是什么?
了解了什么是虛擬地址空間,那么虛擬地址空間里面裝的是什么?看下圖
虛擬空間裝的大概是上面那些數據了,內存映射大概就是把設備地址映射到上圖的紅色段了,暫且稱其為“內存映射段”,至于映射到哪個地址,是由操作系統分配的,操作系統會把進程空間劃分為三個部分:
(1)未分配的,即進程還未使用的地址
(2)緩存的,緩存在ram中的頁
(3)未緩存的,沒有緩存在ram中
操作系統會在未分配的地址空間分配一段虛擬地址,用來和設備地址建立映射,至于怎么建立映射,后面再揭曉。
現在大概明白了“內存映射”是什么了,那么內核是怎么管理這些地址空間的呢?任何復雜的理論最終也是通過各種數據結構體現出來的,而這里這個數據結構就是進程描述符。從內核看,進程是分配系統資源(CPU、內存)的載體,為了管理進程,內核必須對每個進程所做的事情進行清楚的描述,這就是進程描述符,內核用task_struct結構體來表示進程,并且維護一個該結構體鏈表來管理所有進程。該結構體包含一些進程狀態、調度信息等上千個成員,我們這里主要關注進程描述符里面的內存描述符(struct mm_struct mm)
內存描述符
具體的結構,請參考下圖
現在已經知道了內存映射是把設備地址映射到進程空間地址(注意:并不是所有內存映射都是映射到進程地址空間的,ioremap是映射到內核虛擬空間的,mmap是映射到進程虛擬地址的),實質上是分配了一個vm_area_struct結構體加入到進程的地址空間,也就是說,把設備地址映射到這個結構體,映射過程就是驅動程序要做的事了。
內存映射的實現
以字符設備驅動為例,一般對字符設備的操作都如下框圖
而內存映射的主要任務就是實現內核空間中的mmap()函數,先來了解一下字符設備驅動程序的框架
以下是mmap_driver.c的源代碼
//所有的模塊代碼都包含下面兩個頭文件?? #include?<linux/module.h>?? #include?<linux/init.h>??#include?<linux/types.h>?//定義dev_t類型?? #include?<linux/cdev.h>?//定義struct?cdev結構體及相關操作?? #include?<linux/slab.h>?//定義kmalloc接口?? #include?<asm/io.h>//定義virt_to_phys接口?? #include?<linux/mm.h>//remap_pfn_range?? #include?<linux/fs.h>??#define?MAJOR_NUM?990?? #define?MM_SIZE?4096??static?char?driver_name[]?=?"mmap_driver1";//驅動模塊名字?? static?int?dev_major?=?MAJOR_NUM;?? static?int?dev_minor?=?0;?? char?*buf?=?NULL;?? struct?cdev?*cdev?=?NULL;??static?int?device_open(struct?inode?*inode,?struct?file?*file)?? {??printk(KERN_ALERT"device?open\n");??buf?=?(char?*)kmalloc(MM_SIZE,?GFP_KERNEL);//內核申請內存只能按頁申請,申請該內存以便后面把它當作虛擬設備??return?0;?? }??static?int?device_close(struct?inode?*indoe,?struct?file?*file)?? {??printk("device?close\n");??if(buf)??{??kfree(buf);??}??return?0;?? }??static?int?device_mmap(struct?file?*file,?struct?vm_area_struct?*vma)?? {??vma->vm_flags?|=?VM_IO;//表示對設備IO空間的映射??vma->vm_flags?|=?VM_RESERVED;//標志該內存區不能被換出,在設備驅動中虛擬頁和物理頁的關系應該是長期的,應該保留起來,不能隨便被別的虛擬頁換出??if(remap_pfn_range(vma,//虛擬內存區域,即設備地址將要映射到這里??vma->vm_start,//虛擬空間的起始地址??virt_to_phys(buf)>>PAGE_SHIFT,//與物理內存對應的頁幀號,物理地址右移12位??vma->vm_end?-?vma->vm_start,//映射區域大小,一般是頁大小的整數倍??vma->vm_page_prot))//保護屬性,??{??return?-EAGAIN;??}??return?0;?? }??static?struct?file_operations?device_fops?=?? {??.owner?=?THIS_MODULE,??.open??=?device_open,??.release?=?device_close,??.mmap?=?device_mmap,?? };??static?int?__init?char_device_init(?void?)?? {??int?result;??dev_t?dev;//高12位表示主設備號,低20位表示次設備號??printk(KERN_ALERT"module?init2323\n");??printk("dev=%d",?dev);??dev?=?MKDEV(dev_major,?dev_minor);??cdev?=?cdev_alloc();//為字符設備cdev分配空間??printk(KERN_ALERT"module?init\n");??if(dev_major)??{??result?=?register_chrdev_region(dev,?1,?driver_name);//靜態分配設備號??printk("result?=?%d\n",?result);??}??else??{??result?=?alloc_chrdev_region(&dev,?0,?1,?driver_name);//動態分配設備號??dev_major?=?MAJOR(dev);??}??if(result?<?0)??{??printk(KERN_WARNING"Cant't?get?major?%d\n",?dev_major);??return?result;??}??cdev_init(cdev,?&device_fops);//初始化字符設備cdev??cdev->ops?=?&device_fops;??cdev->owner?=?THIS_MODULE;??result?=?cdev_add(cdev,?dev,?1);//向內核注冊字符設備??printk("dffd?=?%d\n",?result);??return?0;?? }??static?void?__exit?char_device_exit(?void?)?? {??printk(KERN_ALERT"module?exit\n");??cdev_del(cdev);??unregister_chrdev_region(MKDEV(dev_major,?dev_minor),?1);?? }??module_init(char_device_init);//模塊加載?? module_exit(char_device_exit);//模塊退出??MODULE_LICENSE("GPL");?? MODULE_AUTHOR("ChenShengfa");?下面是測試代碼test_mmap.c
#include?<stdio.h>?? #include?<fcntl.h>?? #include?<sys/mman.h>?? #include?<stdlib.h>?? #include?<string.h>??int?main(?void?)?? {??int?fd;??char?*buffer;??char?*mapBuf;??fd?=?open("/dev/mmap_driver",?O_RDWR);//打開設備文件,內核就能獲取設備文件的索引節點,填充inode結構??if(fd<0)??{??printf("open?device?is?error,fd?=?%d\n",fd);??return?-1;??}??/*測試一:查看內存映射段*/??printf("before?mmap\n");??sleep(15);//睡眠15秒,查看映射前的內存圖cat?/proc/pid/maps??buffer?=?(char?*)malloc(1024);??memset(buffer,?0,?1024);??mapBuf?=?mmap(NULL,?1024,?PROT_READ|PROT_WRITE,?MAP_SHARED,?fd,?0);//內存映射,會調用驅動的mmap函數??printf("after?mmap\n");??sleep(15);//睡眠15秒,在命令行查看映射后的內存圖,如果多出了映射段,說明映射成功??/*測試二:往映射段讀寫數據,看是否成功*/??strcpy(mapBuf,?"Driver?Test");//向映射段寫數據??memset(buffer,?0,?1024);??strcpy(buffer,?mapBuf);//從映射段讀取數據??printf("buf?=?%s\n",?buffer);//如果讀取出來的數據和寫入的數據一致,說明映射段的確成功了??munmap(mapBuf,?1024);//去除映射??free(buffer);??close(fd);//關閉文件,最終調用驅動的close??return?0;?? }??下面是makefile文件
ifneq?($(KERNELRELEASE),)??obj-m?:=?mmap_driver.o??else?? KDIR?:=?/lib/modules/3.2.0-52-generic/build??all:??make?-C?$(KDIR)?M=$(PWD)?modules?? clean:??rm?-f?*.ko?*.o?*.mod.o?*.mod.c?*~?*.symvers?*.order??endif??下面命令演示一下驅動程序的編譯、安裝、測試過程(注:其他用戶在mknod之后還需要chmod改變權限)
# make??? //編譯驅動
# insmod mmap_driver.ko??? //安裝驅動
# mknod?/dev/mmap_driver c 999 0??? //創建設備文件
# gcc test_mmap.c -o test.o??? //編譯應用程序
# ./test.o??? //運行應用程序來測試驅動程序
拓展:
關于這個過程,涉及一些術語
(1)設備文件:linux中對硬件虛擬成設備文件,對普通文件的各種操作均適用于設備文件
(2)索引節點:linux使用索引節點來記錄文件信息(如文件長度、創建修改時間),它存儲在磁盤中,讀入內存后就是一個inode結構體,文件系統維護了一個索引節點的數組,每個元素都和文件或者目錄一一對應。
(3)主設備號:如上面的999,表示設備的類型,比如該設備是lcd還是usb等
(4)次設備號:如上面的0,表示該類設備上的不同設備
(5)文件(普通文件或設備文件)的三個結構
??????? ①文件操作:struct file_operations
??????? ②文件對象:struct file
??????? ③文件索引節點:struct inode
關于驅動程序中內存映射的實現,先了解一下open和close的流程
(1)設備驅動open流程
①應用程序調用open("/dev/mmap_driver", O_RDWR);
②Open就會通過VFS找到該設備的索引節點(inode),mknod的時候會根據設備號把驅動程序的file_operations結構填充到索引節點中(關于mknod?/dev/mmap_driver c 999 0,這條指令創建了設備文件,在安裝驅動(insmod)的時候,會運行驅動程序的初始化程序(module_init),在初始化程序中,會注冊它的主設備號到系統中(cdev_add),如果mknod時的主設備號999在系統中不存在,即和注冊的主設備號不同,則上面的指令會執行失敗,就創建不了設備文件)
③然后根據設備文件的索引節點中的file_operations中的open指針,就調用驅動的open方法了。
④生成一個文件對象files_struct結構,系統維護一個files_struct的鏈表,表示系統中所有打開的文件
⑤返回文件描述符fd,把fd加入到進程的文件描述符表中
(2)設備驅動close流程
應用程序調用close(fd),最終可調用驅動的close,為什么根據一個簡單的int型fd就可以找到驅動的close函數?這就和上面說的三個結構(struct file_operations、struct file、struct inode)息息相關了,假如fd = 3
(3)設備驅動mmap流程
由open和close得知,同理,應用程序調用mmap最終也會調用到驅動程序中mmap方法
①應用程序test.mmap.c中mmap函數
void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:映射后虛擬地址的起始地址,通常為NULL,內核自動分配
length:映射區的大小
prot:頁面訪問權限(PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE)
flags:參考網絡資料
fd:文件描述符
offset:文件映射開始偏移量
②驅動程序的mmap_driver.c中mmap函數
上面說了,mmap的主要工作是把設備地址映射到進程虛擬地址,也即是一個vm_area_struct的結構體,這里說的映射,是一個很懸的東西,那它在程序中的表現是什么呢?——頁表,沒錯,就是頁表,映射就是要建立頁表。進程地址空間就可以通過頁表(軟件)和MMU(硬件)映射到設備地址上了
virt_to_phys(buf),buf是在open時申請的地址,這里使用virt_to_phys把buf轉換成物理地址,是模擬了一個硬件設備,即把虛擬設備映射到虛擬地址,在實際中可以直接使用物理地址。
總結
①從以上看到,內核各個模塊錯綜復雜、相互交叉
②單純一個小小驅動模塊,就涉及了進程管理(進程地址空間)、內存管理(頁表與頁幀映射)、虛擬文件系統(structfile、structinode)
③并不是所有設備驅動都可以使用mmap來映射,比如像串口和其他面向流的設備,并且必須按照頁大小進行映射。
?ELF文件格式分析
一、概述
1.ELF全稱Executable and Linkable Format,可執行連接格式,ELF格式的文件用于存儲Linux程序。ELF文件(目標文件)格式主要三種:
- 可重定向文件:文件保存著代碼和適當的數據,用來和其他的目標文件一起來創建一個可執行文件或者是一個共享目標文件。(目標文件或者靜態庫文件,即linux通常后綴為.a和.o的文件)
- 可執行文件:文件保存著一個用來執行的程序。(例如bash,gcc等)
- 共享目標文件:共享庫。文件保存著代碼和合適的數據,用來被下連接編輯器和動態鏈接器鏈接。(linux下后綴為.so的文件。)
目標文件既要參與程序鏈接又要參與程序執行:
一般的 ELF 文件包括三個索引表:ELF header,Program header table,Section header table。
- ELF header:在文件的開始,保存了路線圖,描述了該文件的組織情況。
- Program header table:告訴系統如何創建進程映像。用來構造進程映像的目標文件必須具有程序頭部表,可重定位文件不需要這個表。
- Section header table:包含了描述文件節區的信息,每個節區在表中都有一項,每一項給出諸如節區名稱、節區大小這類信息。用于鏈接的目標文件必須包含節區頭部表,其他目標文件可以有,也可以沒有這個表。
二、分析ELF文件頭(ELF header)
-
進入終端輸入:cd /usr/include 進入include文件夾后查看elf.h文件,查看ELF的文件頭包含整個文件的控制結構
-
寫一個小程序(hello 20135328)進行編譯,生成hello可執行文件。
使用‘readelf –a hello’命令,都得到下面的ELF Header頭文件的信息,如下圖: -
通過上圖信息,可以得出Elf Header的Size為52bytes,所以可以使用hexdump工具將頭文件的16進制表打開。
如下圖使用:‘hexdump -x hello -n 52 ’命令來查看hello文件頭的16進制表(前52bytes)對格式進行分析。 - 第一行
- 對應e_ident[EI_NIDENT]。實際表示內容為7f454c46010100010000000000000000,前四個字節7f454c46(0x45,0x4c,0x46是'e','l','f'對應的ascii編碼)是一個魔數,表示這是一個ELF對象。
- 接下來的一個字節01表示是一個32位對象,接下來的一個字節01表示是小端法表示,再接下來的一個字節01表示文件頭版本。剩下的默認都設置為0.
- 第二行
- e_type值為0x0002,表示是一個可執行文件
- 。e_machine值為0x003e,表示是Advanced Micro Devices X86-64處理器體系結構。
- e_version值為0x00000001,表示是當前版本。
- e_entry值為0x 08048320,表示入口點。
- 第三行
- e_phof f值為0x1178,表示程序頭表。
- e_shoff值為0x0034,表示段表的偏移地址。
- 第四行
- e_flags值為0x001e,表示未知處理器特定標志。
- e_ehsize值為0x0034,表示elf文件頭大小(正好是52bytes)。
- e_phentsize表示一個program header表中的入口的長度,值為0x0020。
- e_phnum的值為0x0009,給出program header表中的入口數目。
- e_shentsize值為0x0028表示段頭大小為40個字節。e_shnum值為0x001e,表示段表入口有30個。
-
e_shstrndx值為0x001b,表示段名串表的在段表中的索引號。
三、通過文件頭找到section header table,理解其內容
- file elf顯示生成的目標文件hello的類型
- elf是一個可執行文件。輸入:ls –l hello查看hello的大小:
- 如圖可知,hello大小為7336字節。
輸入:hexdump –x hello來用16進制的數字來顯示hello的內容
(其中,第二列是16進制表示的偏移地址) - 輸入:objdump –x hello來顯示hello中各個段以及符號表的相關信息:
- 輸入:readelf –a hello來查看各個段信息:
- ELF文件頭信息:
- 段表Section header table:
-
符號表 Symbol table:
四、通過section header table找到各section
在一個ELF文件中有一個section header table,通過它我們可以定位到所有的 section,而 ELF header 中的e_shoff 變量就是保存 section header table 入口對文件頭的偏移量。而每個 section 都會對應一個 section header ,所以只要在 section header table 中找到每個 section header,就可以通過 section header 找到你想要的 section。
下面以可執行文件hello為例,以保存代碼段的 section 為例來講解讀取某個section 的過程。
使用‘vi /usr/include/elf.h ’命令查看Sections Header的結構體:
由上面分析可知,section headers table中的每一個section header所占的size均為64字節,ELF header得到了e_shoff變量的值為0X0034,也就是table入口的偏移量,通過看e_shnum值為0x001e,表示段表入口有30個。
所以從0x00000034開始有30個段,每個段占40個字節大小,輸入 hexdump hello查看:
- 第一個段,其中內容全部為0,所以不表示任何段。
-
第二個段,為.interp段
-
第三個段,為.note.ABI-tag段
-
第四個段,為.note.gnu.build-i段
-
第五個段,為.gnu.hash段
.......
-
第十四個段,為.text段
-
第十六個段,為.rodata段
-
第二十五個段,為.data段
-
第二十六個段,為.bss段
-
第二十九個段, 為.symtab段
-
第三十個段, 為.strtab段
我們用readelf 命令去查看.text這個 section 中的內容,
輸入readelf –x 13 hello,(.text前面的標號為13)對13索引號的.text的section的內容進行查看:
下面用 hexdump 的方法去讀取.text這個 section 中的內容,通過看section header中.text中offset和size分別是0x320和0x192
輸入 hexdump –C hello
找到320后的192個
得到了和上面的readelf得到的相同。
使用下面命令對hello的文本段(.text)進行反匯編:
objdump –d hello 得到如下圖:
可以看出,使用反匯編的16進制數據和前面查找到的是相同的。
五、理解常見.text .strtab .symtab .rodata等section
readelf命令和ELF文件詳解?
ELF(Executable and Linking Format)是一個定義了目標文件內部信息如何組成和組織的文件格式。內核會根據這些信息加載可執行文件,內核根據這些信息可以知道從文件哪里獲取代碼,從哪里獲取初始化數據,在哪里應該加載共享庫,等信息。?
ELF文件有下面三種類型:?
1.目標文件
$ gcc -c test.c?
得到的test.o就是目標文件,目標文件通過鏈接可生成可執行文件。?
靜態庫其實也算目標文件,靜態庫是通過ar命令將目標打包為.a文件。?
如:ar crv libtest.a test.o
2.可執行文件?
$gcc -o test test.c?
得到的test文件就是可執行的二進制文件。
3.共享庫?
$ gcc test.c -fPIC -shared -o libtest.so?
得到的文件listtest.so就是共享庫。
可以通過readelf來區分上面三種類型的ELF文件,每種類型文件的頭部信息是不一樣的。?
$readelf -h test.o?
目標文件
$readelf -h test?
可執行文件
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: EXEC (Executable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x400420Start of program headers: 64 (bytes into file)Start of section headers: 2696 (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 8Size of section headers: 64 (bytes)Number of section headers: 30Section header string table index: 27$readelf -h libtest.so?
共享庫
下面是test.c文件內容:
#include<stdio.h>int global_data = 4; int global_data_2; int main(int argc, char **argv) { int local_data = 3; printf("Hello World\n"); printf("global_data = %d\n", global_data); printf("global_data_2 = %d\n", global_data_2); printf("local_data = %d\n", local_data); return (0); }$gcc -o test test.c?
生成可執行文件test,然后使用readelf對其進行分析。
$readelf -h test?
下面是輸出結果:
上面的信息可以告訴我們什么信息??
1.根據Class、Type和Machine,可以知道該文件在X86-64位機器上生成的64位可執行文件。?
2.根據Entry point address,可以知道當該程序啟動時從虛擬地址0x400420處開始運行。這個地址并不是main函數的地址,而是_start函數的地址,_start由鏈接器創建,_start是為了初始化程序。通過這個命令可以看到_start函數,objdump -d -j .text test?
3.根據Number of program headers,可以知道該程序有8個段。?
4.根據Number of section headers,可以知道該程序有30個區。?
區中存儲的信息是用來鏈接使用的,主要包括:程序代碼、程序數據(變量)、重定向信息等。比如:Code section保存的是代碼,data section保存的是初始化或未初始化的數據,等等。
Linux內核無法以區的概念來識別可執行文件。內核使用包括連續頁的VMA(virtual memory area)來識別進程。在每個VMA中可能映射了一個或多個區。每個VMA代表一個ELF文件的段。?
那么,內核如何知道哪個區屬于某個VMA(段)?映射關系保存在Program Header Table(PHT)中。
下面查看區的內容:?
$readelf -S test
There are 30 section headers, starting at offset 0xa88:Section Headers:[Nr] Name Type Address OffsetSize EntSize Flags Link Info Align[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0[ 1] .interp PROGBITS 0000000000400200 00000200000000000000001c 0000000000000000 A 0 0 1[ 2] .note.ABI-tag NOTE 000000000040021c 0000021c0000000000000020 0000000000000000 A 0 0 4[ 3] .note.gnu.build-i NOTE 000000000040023c 0000023c0000000000000024 0000000000000000 A 0 0 4[ 4] .gnu.hash GNU_HASH 0000000000400260 00000260000000000000001c 0000000000000000 A 5 0 8[ 5] .dynsym DYNSYM 0000000000400280 000002800000000000000078 0000000000000018 A 6 1 8[ 6] .dynstr STRTAB 00000000004002f8 000002f80000000000000044 0000000000000000 A 0 0 1[ 7] .gnu.version VERSYM 000000000040033c 0000033c000000000000000a 0000000000000002 A 5 0 2[ 8] .gnu.version_r VERNEED 0000000000400348 000003480000000000000020 0000000000000000 A 6 1 8[ 9] .rela.dyn RELA 0000000000400368 000003680000000000000018 0000000000000018 A 5 0 8[10] .rela.plt RELA 0000000000400380 000003800000000000000048 0000000000000018 A 5 12 8[11] .init PROGBITS 00000000004003c8 000003c80000000000000018 0000000000000000 AX 0 0 4[12] .plt PROGBITS 00000000004003e0 000003e00000000000000040 0000000000000010 AX 0 0 4[13] .text PROGBITS 0000000000400420 000004200000000000000238 0000000000000000 AX 0 0 16[14] .fini PROGBITS 0000000000400658 00000658000000000000000e 0000000000000000 AX 0 0 4[15] .rodata PROGBITS 0000000000400668 000006680000000000000053 0000000000000000 A 0 0 8[16] .eh_frame_hdr PROGBITS 00000000004006bc 000006bc0000000000000024 0000000000000000 A 0 0 4[17] .eh_frame PROGBITS 00000000004006e0 000006e0000000000000007c 0000000000000000 A 0 0 8[18] .ctors PROGBITS 0000000000600760 000007600000000000000010 0000000000000000 WA 0 0 8[19] .dtors PROGBITS 0000000000600770 000007700000000000000010 0000000000000000 WA 0 0 8[20] .jcr PROGBITS 0000000000600780 000007800000000000000008 0000000000000000 WA 0 0 8[21] .dynamic DYNAMIC 0000000000600788 000007880000000000000190 0000000000000010 WA 6 0 8[22] .got PROGBITS 0000000000600918 000009180000000000000008 0000000000000008 WA 0 0 8[23] .got.plt PROGBITS 0000000000600920 000009200000000000000030 0000000000000008 WA 0 0 8[24] .data PROGBITS 0000000000600950 000009500000000000000008 0000000000000000 WA 0 0 4[25] .bss NOBITS 0000000000600958 000009580000000000000018 0000000000000000 WA 0 0 8[26] .comment PROGBITS 0000000000000000 00000958000000000000002c 0000000000000001 MS 0 0 1[27] .shstrtab STRTAB 0000000000000000 0000098400000000000000fe 0000000000000000 0 0 1[28] .symtab SYMTAB 0000000000000000 000012080000000000000648 0000000000000018 29 46 8[29] .strtab STRTAB 0000000000000000 00001850000000000000021e 0000000000000000 0 0 1 Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings)I (info), L (link order), G (group), x (unknown)O (extra OS processing required) o (OS specific), p (processor specific).text區存儲的是程序的代碼(二進制指令),該區的標志為X表示可執行。
下面使用objdump反匯編查看.text的內容:?
$objdump -d -j .text test?
-d選項告訴objdump反匯編機器碼,-j選項告訴objdump只關心.text區。
下面使用objdump反匯編查看.data的內容:?
$objdump -d -j .data test?
.data區保存的是初始化的全局變量。
下面使用objdump反匯編查看.bss的內容:?
$objdump -d -j .bss test?
.bss區保存的是未初始化的全局變量,linux會默認將未初始化的變量置為0。
下面命令可以看到test文件中所有的符號:?
$readelf -s test?
Value的值是符號的地址。
下面命令來查看文件的段信息:?
$readelf -l test?
區到段的映射,基本上是按照區的順序進行映射。?
如果Flags為R和E,表示該段可讀和可執行。?
如果Flags為W,表示該段可寫。?
VirtAddr是每個段的虛擬起始地址。這個地址并不是位于真正內存上的地址(物理地址)。
如上所示,段有多種類型,下面介紹LOAD類型?
LOAD:該段的內容從可執行文件中獲取。Offset標識內核從文件讀取的位置。FileSiz標識讀取多少字節。
那么,執行test之后的進程的段布局是如何呢??
可以通過cat /proc/pid/maps來查看。pid是進程的pid。?
但是該test運行時間很短,可以使用gdb加斷點來運行,或者在return語句之前加上sleep()。
下面使用gdb加斷點的形式:
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-50.el6) Copyright (C) 2010 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-redhat-linux-gnu". For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>... Reading symbols from /data/readyao/qqlive_zb_prj/server/cgi_push_post_replay/lib/test...(no debugging symbols found)...done. (gdb) b main Breakpoint 1 at 0x400508 (gdb) r Starting program: /data/readyao/qqlive_zb_prj/server/cgi_push_post_replay/lib/test [Thread debugging using libthread_db enabled]Breakpoint 1, 0x0000000000400508 in main () Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.49.tl1.x86_64$cat /proc/6929/maps?
00400000-00401000 r-xp 00000000 ca:11 8626925 /test 00600000-00601000 rw-p 00000000 ca:11 8626925 /test 7ffff762d000-7ffff7644000 r-xp 00000000 ca:01 332328 /lib64/libpthread-2.12.so 7ffff7644000-7ffff7843000 ---p 00017000 ca:01 332328 /lib64/libpthread-2.12.so 7ffff7843000-7ffff7844000 r--p 00016000 ca:01 332328 /lib64/libpthread-2.12.so 7ffff7844000-7ffff7845000 rw-p 00017000 ca:01 332328 /lib64/libpthread-2.12.so 7ffff7845000-7ffff7849000 rw-p 00000000 00:00 0 7ffff7849000-7ffff784b000 r-xp 00000000 ca:01 332237 /lib64/libdl-2.12.so 7ffff784b000-7ffff7a4b000 ---p 00002000 ca:01 332237 /lib64/libdl-2.12.so 7ffff7a4b000-7ffff7a4c000 r--p 00002000 ca:01 332237 /lib64/libdl-2.12.so 7ffff7a4c000-7ffff7a4d000 rw-p 00003000 ca:01 332237 /lib64/libdl-2.12.so 7ffff7a4d000-7ffff7bd3000 r-xp 00000000 ca:01 332102 /lib64/libc-2.12.so 7ffff7bd3000-7ffff7dd3000 ---p 00186000 ca:01 332102 /lib64/libc-2.12.so 7ffff7dd3000-7ffff7dd7000 r--p 00186000 ca:01 332102 /lib64/libc-2.12.so 7ffff7dd7000-7ffff7dd8000 rw-p 0018a000 ca:01 332102 /lib64/libc-2.12.so 7ffff7dd8000-7ffff7ddd000 rw-p 00000000 00:00 0 7ffff7ddd000-7ffff7dfd000 r-xp 00000000 ca:01 332126 /lib64/ld-2.12.so 7ffff7ed9000-7ffff7edc000 rw-p 00000000 00:00 0 7ffff7eeb000-7ffff7eee000 r-xp 00000000 ca:01 336319 /lib64/libonion_security.so.1.0.13 7ffff7eee000-7ffff7fee000 ---p 00003000 ca:01 336319 /lib64/libonion_security.so.1.0.13 7ffff7fee000-7ffff7fef000 rw-p 00003000 ca:01 336319 /lib64/libonion_security.so.1.0.13 7ffff7fef000-7ffff7ffb000 rw-p 00000000 00:00 0 7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso] 7ffff7ffc000-7ffff7ffd000 r--p 0001f000 ca:01 332126 /lib64/ld-2.12.so 7ffff7ffd000-7ffff7ffe000 rw-p 00020000 ca:01 332126 /lib64/ld-2.12.so 7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0 7ffffffea000-7ffffffff000 rw-p 00000000 00:00 0 [stack] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]前面一部分是VMA的起始地址和結束地址。?
最后一部分是該區域內容所屬文件。?
在32位系統中,進程地址空間為4G,分為用戶空間和內核空間。?
從下面可以看到棧的地址是向下生長,堆的地址是向上生長。?
通過反匯編定位段錯誤:
段錯誤是程序員最討厭的問題之一,其發生往往很突然,且破壞巨大。典型的段錯誤是由于操作內存不當引起的(如使用野指針或訪問受保護的地址等),發生段錯誤時,內核以一個信號SIGSEGV強行終止進程,留下的出錯信息極少,從而導致難以定位。但利用gdb和反匯編工具,可以較準確地定位段錯誤產生的原因。但想用這種方法調試,一些準備工作和工具是必需的。
準備工作:
(1)coredump:進程異常中止時,內核生成的記錄文件,其中保存了進程異常時所占用的內存和CPU資源,如pc計數器、各個寄存器的值等。這個文件是調試段錯誤最重要的依據。要使內核生成coredump,需要在內核配置中打開CONFIG_ELF_CORE選項,如果沒有打開,將其選上后重新編譯內核即可。
此外,利用命令ulimit -c unlimited,可以設置coredump大小為不受限制,可以保存更完整的信息。文件/proc/sys/kernel/core_pattern可以配置生成coredump的命名格式,如果不設置格式,則coredump默認生成的位置在出錯進程的目錄下,且生成的core同名,也就意味著舊的coredump可能被新的coredump所覆蓋。如果我想在/tmp目錄下生成以core.pid格式命名的coredump文件,只需執行命令:
echo "/tmp/core.%p" > /proc/sys/kernel/core_pattern(2)編譯:為了利用gdb進行調試,在編譯程序時,需要在編譯選項中加入-g 選項,以將調試信息編譯到目標文件中。
(3)反匯編:顧名思義,反匯編就是將編譯好的二進制可執行文件翻譯成匯編文件。一般來說,編譯器會自帶一套反匯編工具,只有選擇正確的工具才能正確地進行反匯編,這不難理解。比如我是用gcc4.6.3編譯的用于mips的應用程序,那么,在編譯器的目錄下可以找到gcc463/usr/bin/mipsel-buildroot-linux-uclibc-objdump,這就是我要使用的反匯編工具。將二進制文件反匯編成匯編文件只需執行命令:XXXX-objdump -S XXXX(程序名),即可生成可以閱讀的、關聯到C代碼的匯編代碼,如下所示:
?? ?status = httpRpmPost(reqId);?? ?42f208:?? ?0320f809 ?? ?jalr?? ?t942f20c:?? ?00808021 ?? ?move?? ?s0,a042f210:?? ?8fbc0018 ?? ?lw?? ?gp,24(sp)if (RPM_OK != status && RPM_DONE != status)42f214:?? ?10400009 ?? ?beqz?? ?v0,42f23c <postDataApStatusJson.part.4+0x68>42f218:?? ?3c120061 ?? ?lui?? ?s2,0x6142f21c:?? ?24030002 ?? ?li?? ?v1,242f220:?? ?10430006 ?? ?beq?? ?v0,v1,42f23c <postDataApStatusJson.part.4+0x68>42f224:?? ?3c040061 ?? ?lui?? ?a0,0x61{RPM_AP_ERROR("httpRpmPost error!");42f228:?? ?2484c928 ?? ?addiu?? ?a0,a0,-1404042f22c:?? ?2645cc98 ?? ?addiu?? ?a1,s2,-1316042f230:?? ?8f999e50 ?? ?lw?? ?t9,-25008(gp)42f234:?? ?0810bca3 ?? ?j?? ?42f28c <postDataApStatusJson.part.4+0xb8>42f238:?? ?24060327 ?? ?li?? ?a2,807return RPM_ERROR;}?? ? ?httpStatusSet(reqId, HTTP_OK);42f23c:?? ?8f998088 ?? ?lw?? ?t9,-32632(gp)42f240:?? ?02002021 ?? ?move?? ?a0,s042f244:?? ?0320f809 ?? ?jalr?? ?t942f248:?? ?00002821 ?? ?move?? ?a1,zero42f24c:?? ?8fbc0018 ?? ?lw?? ?gp,24(sp)可以看到,C代碼下面跟著一串匯編代碼,而匯編代碼前面有一段地址,這個地址是什么呢?如果熟悉Linux進程空間的概念,很容易就可以聯想到,這個地址其實就是相應的匯編指令在.text段(即代碼段)中的地址。也就是說,這個地址就是我們用于定位具體出錯地點的依據。(4)gdb:可以說是Linux下調試程序最常用的工具,功能強大,操作也很簡單。對于mips程序調試,只需安裝相應的gdb:mips-linux-gdb即可。
開始調試:
上面的準備工作都完成后,就可以開始調試了。當進程再次異常終止時,就可以在/tmp目錄下找到coredump文件:比如core.126(進程id為126的進程生成的coredump)。
用gdb的-c選項打開coredump:mips-linux-gdb -c /tmp/core.126,可以看到如下信息:
前面是gdb的版本信息,不必理會。我們主要關注下面的內容:
<span style="font-size:14px;">Core was generated by `/usr/bin/httpd'.
Program terminated with signal 11, Segmentation fault.
#0 ?0x2b17ff50 in ?? ()</span>
表示這個coredump是為進程httpd生成的,而進程退出的原因是signal 11,即SIGSEGV,這正是我們想要的。最后一行,0x2b17ff50是一個地址,這里??的地方本來應該顯示一個函數名,之所以這里沒有顯示,我猜想這應該是一個庫函數,而編譯這個庫時,并沒有帶入-g信息。
不要緊,接下來只需要輸入where,即可顯示信號產生時程序中止的位置:
? ? 至此,我們已經拿到最重要的信息:0x0045c034,就是進程中止時停留的位置。對照上面生成的反匯編文件,搜索45c034,即可找到:
? ? out = cJSON_Print(root);45c00c:?? ?8f9981ec ?? ?lw?? ?t9,-32276(gp)45c010:?? ?00000000 ?? ?nop45c014:?? ?0320f809 ?? ?jalr?? ?t945c018:?? ?02402021 ?? ?move?? ?a0,s245c01c:?? ?8fbc0010 ?? ?lw?? ?gp,16(sp)httpnPrintf(reqId, strlen(out) + 1, "%s\n", out);45c020:?? ?00402021 ?? ?move?? ?a0,v045c024:?? ?8f999fe4 ?? ?lw?? ?t9,-24604(gp)45c028:?? ?00000000 ?? ?nop45c02c:?? ?0320f809 ?? ?jalr?? ?t945c030:?? ?00408021 ?? ?move?? ?s0,v045c034:?? ?8fbc0010 ?? ?lw?? ?gp,16(sp)45c038:?? ?8fa47e48 ?? ?lw?? ?a0,32328(sp)45c03c:?? ?3c060062 ?? ?lui?? ?a2,0x6245c040:?? ?8f9981f0 ?? ?lw?? ?t9,-32272(gp)45c044:?? ?24450001 ?? ?addiu?? ?a1,v0,145c048:?? ?24c6cdb8 ?? ?addiu?? ?a2,a2,-1287245c04c:?? ?0320f809 ?? ?jalr?? ?t945c050:?? ?02003821 ?? ?move?? ?a3,s045c054:?? ?8fbc0010 ?? ?lw?? ?gp,16(sp)45c058:?? ?00000000 ?? ?nopRPM_MONITORAP_TRACE("\r\n%s\r\n\r\n", out);可以看到,出錯時,對應的C函數是httpnPrintf,對應的匯編代碼為:????lw????gp, 16(sp)。
在反匯編文件中再稍微對照上下文,即可知道具體是哪個模塊、哪個文件中的調用。如果看得懂匯編代碼,基本可以定位到函數中的具體語句,即使看不懂匯編,利用打印調試或者靜態代碼分析等常規調試手段也基本可以定位到具體的出錯原因了。在本例中,最終確定這個函數出錯的原因是操作了調用malloc(0)而獲取的一個空指針(malloc(0)返回什么),著實令人始料未及。
?
不能使用GDB的地方,也可以使用反匯編來定位段錯誤的位置。一般在發生段錯誤的時候,都會有些信息打印出來,如下:
Oops: Data Abort caused by READ instruction! Fault: Alignment fault pc: 0021034c r0: 20000053 r1: 00000001 r2: 00000000 r3: 20000053 r4: aaaaaaaa r5: 00256984 r6: aaaaaaaa r7: dddddddd r8: aaaaaaaa r9: dddddddd r10: 00060000 fp: 00000000 ip: 00000000 sp: 00240bc0 SPSR: 600000d3? ? 從上面看出,PC就是程序計數器,即程序運行到此位置發生錯誤,然后我們利用objdump將運行的程序反匯編,跟蹤查詢到問題出現的位置。
版權說明:文章內容轉載自下面文章
? ?《ELF文件格式分析》轉載自:https://www.cnblogs.com/cdcode/p/5551649.html
? ?《Linux驅動mmap內存映射》轉載自https://www.cnblogs.com/wanghuaijun/p/7624564.html
? ?《通過反匯編定位段錯誤》轉載自https://blog.csdn.net/ringrang/article/details/60596846
? ?《Linux C 內存管理》轉載自:?https://blog.csdn.net/zqixiao_09/article/details/50381476?
? ?《readelf命令和ELF文件詳解》轉載自:https://blog.csdn.net/linux_ever/article/details/78210089?
《Understanding_ELF.pdf》 是官方elf 文件格式的翻譯,對elf格式有做詳細的介紹。下載地址:https://download.csdn.net/download/li_wen01/12446168
?
----------------------------------------------------------------2022.08.28----------------------------------------------------------------
?新的文章內容和附件工程文件
已更新在博客首頁和:
gong 眾 hao :?liwen01
總結
以上是生活随笔為你收集整理的Linux内存管理内存映射以及通过反汇编定位内存错误问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OSPF配置及MD5验证
- 下一篇: 华为u8860刷机方法比较+变砖补救+r