原文轉自:https://gist.github.com/CMCDragonkai/10ab53654b2aa6ce55c11cfc5b2432a4
瞭解Linux可執行文件的內存佈局
調試內存所需的工具:
- hexdump
- objdump
- readelf
- xxd
- gcore
- strace
- diff
- cat
我們將通過這個:https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/和http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/
實際上有很多C內存分配器。不同的內存分配器將以不同的方式佈局內存。目前glibc的內存分配器是ptmalloc2。它是從dlmalloc分叉的。在fork之後,添加了線程支持,並於2006年發佈。集成後,代碼更改直接轉換爲glibc的malloc源代碼本身。所以glibc的malloc有很多變化,與原版不同ptmalloc2。
glibc中的malloc在內部調用brk或者mmap系統調用來從OS獲取內存。的brk系統調用通常用於增加堆的大小,而mmap將用於加載的共享庫,線程,以及許多其他的事情創建新區域。它實際上切換到使用mmap而不是brk當請求的內存量大於MMAP_THRESHOLD。我們通過使用查看正在進行的調用strace。
在過去的使用中dlmalloc,當2個線程同時調用malloc時,只有一個線程可以進入臨界區,內存塊的freelist數據結構在所有可用線程之間共享。因此,內存分配是一種全局鎖定操作。
但是在ptmalloc2中,當2個線程同時調用malloc時,會立即分配內存,因爲每個線程都維護一個單獨的堆,以及它們自己的freelist塊數據結構
爲每個線程維護單獨的堆和空閒列表的行爲稱爲“每線程競技場”。
在上一個會話中,我們發現程序內存佈局通常在:
User Stack
|
v
Memory Mapped Region for Shared Libraries or Anything Else
^
|
Heap
Uninitialised Data (.bss)
Initialised Data (.data)
Program Text (.text)
0
出於理解的目的,大多數調查內存的工具將低地址放在頂部,將高地址放在底部。
因此,更容易想到這樣:
0
Program Text (.text)
Initialised Data (.data)
Uninitialised Data (.bss)
Heap
|
v
Memory Mapped Region for Shared Libraries or Anything Else
^
|
User Stack
問題是,我們沒有正確掌握究竟發生了什麼。上面的圖表太簡單,無法完全理解。
讓我們編寫一些C程序並研究它們的內存結構。
請注意,直接編譯或彙編實際上都不會生成可執行文件。這是由鏈接器完成的,它接受編譯/彙編產生的各種目標代碼文件,解析它們包含的所有名稱並生成最終的可執行二進制文件。http://stackoverflow.com/a/845365/582917
這是我們的第一個程序(使用gcc -pthread memory_layout.c -o memory_layout以下程序編譯:
#包括 < stdio.h中> //標準IO
#包括 < stdlib.h中> // C標準庫
#包括 < pthread.h > //線程
#包括 < unistd.h中> // UNIX標準庫
#包括 < SYS / types.h > //用於linux的系統類型
// getchar基本上就像“讀取”
//它提示用戶輸入
//在這種情況下,輸入被丟棄
//這類似於“暫停”延續原語
//但是通過用戶解決的暫停輸入,我們立即扔掉!
void * thread_func( void * arg){
printf(“在線程1中的malloc之前\ n ”);
getchar();
char * addr =(char *)malloc(1000);
printf(“在malloc之後和在線程1之前釋放之前\ n ”);
getchar();
free(addr);
printf(“在帖子1 \ n中釋放後”);
getchar();
}
int main(){
char * addr;
printf(“歡迎使用每個線程競技場示例:: %d \ n ”,getpid());
printf(“主線程中的malloc之前\ n ”);
getchar();
addr =(char *)malloc(1000);
printf(“在malloc之後和主線程之前釋放\ n ”);
getchar();
free(addr);
printf(“在主線程中釋放後\ n ”);
getchar();
//指向線程的指針1
pthread_t thread_1;
// pthread_ *函數在成功時返回0,其他數字在失敗時返回
int pthread_status;
pthread_status = pthread_create(&thread_1,NULL,thread_func,NULL);
if(pthread_status!= 0){
printf(“線程創建錯誤\ n ”);
返回 - 1 ;
}
//從thread_1返回狀態代碼
void * thread_1_status;
pthread_status = pthread_join(thread_1,&thread_1_status);
if(pthread_status!= 0){
printf(“ Thread join error \ n ”);
返回 - 1 ;
}
返回 0 ;
}
getchar以上的用法是基本上暫停等待用戶輸入的計算。這允許我們在檢查其內存佈局時逐步執行該程序。
的用途pthread是用於創建POSIX線程,這是真正的內核線程被調度上的Linux操作系統。事情si,線程的使用對於檢查過程內存佈局如何用於許多線程很有意義。事實證明,每個線程都需要自己的堆和堆棧。
這些pthread函數很奇怪,因爲它們在成功時返回基於0的狀態代碼。這是一項pthread操作的成功,它確實會對底層操作系統產生副作用。
正如我們在上面所看到的,參考錯誤模式有很多用途,也就是說,我們使用引用容器來存儲額外的元數據或僅存儲數據本身,而不是返回多個值(通過元組)。
現在,我們可以運行該程序./memory_layout(嘗試使用Ctrl + Z暫停程序):
$ ./memory_layout
Welcome to per thread arena example::1255
Before malloc in the main thread
此時,程序暫停,我們現在可以通過查看來檢查內存內容/proc/1255/maps。這是一個內核提供的虛擬文件,顯示程序的確切內存佈局。它實際上總結了每個內存部分,因此它有助於理解內存的佈局方式,而無需查看特定的字節地址。
/ proc / $ PID / maps中的每一行描述進程中連續虛擬內存的區域。每行都有以下字段:
- address - 區域進程地址空間中的起始和結束地址
- perms - 描述如何訪問頁面,rwxp或者rwxs在s私有或共享頁面的含義,如果進程試圖訪問權限不允許的內存,則會發生分段錯誤
- offset - 如果區域由文件映射使用mmap,則這是映射開始的文件中的偏移量
- dev - 如果區域是從文件映射的,這是文件所在的十六進制中的主要和次要設備編號,主要編號指向設備驅動程序,次編號由設備驅動程序解釋,或次要編號是設備驅動程序的特定設備,如多個軟盤驅動器
- inode - 如果區域是從文件映射的,則這是文件編號
- pathname - 如果區域是從文件映射的,這是文件的名稱,有特殊區域的名稱如[heap],[stack]和[vdso],[vdso]代表虛擬動態共享對象,其使用通過系統調用切換到內核模式
某些區域在路徑名字段中沒有任何文件路徑或特殊名稱,這些是匿名區域。匿名區域由mmap創建,但不附加到任何文件,它們用於雜項,如共享內存,不在堆上的緩衝區,pthread庫使用匿名映射區域作爲新線程的堆棧。
沒有100%保證連續的虛擬內存意味着連續的物理內存。爲此,您必須使用沒有虛擬內存系統的操作系統。但是連續的虛擬內存確實等於連續的物理內存是一個很好的機會,至少沒有指針追逐。仍處於硬件級別,有一種特殊的虛擬到物理內存轉換設備。所以它仍然非常快。
使用該bc工具非常重要,因爲我們需要在這裏經常轉換十六進制和十進制。我們可以使用它bc <<< 'obase=10; ibase=16; 4010000 - 4000000',它基本上4010000 - 4000000使用十六進制數字進行減法,然後將結果轉換爲十進制數10。
關於主要次要數字的旁註。您可以使用ls -l /dev | grep 252或lsblk | grep 252查找與major:minor數字對應的設備。哪裏0d252 ~ 0xfc。
這列出了Linux設備驅動程序的所有主要和次要編號分配:http://www.lanana.org/docs/device-list/devices-2.6+.txt
它還顯示240到254之間的任何東西都用於本地/實驗用例。232 - 239也未分配。並保留255。我們現在可以確定有問題的設備是設備映射器設備。因此它使用保留用於本地/實驗用途的範圍。主要和次要數字最多隻能達到255,因爲它是單個字節中最大的十進制數。單個字節是:0b11111111或0xFF。單個十六進制數字是半字節。2個十六進制數字是一個字節。
首先要意識到的是,內存地址從低到高開始,但每次運行此程序時,許多區域都會有不同的地址。這意味着對於某些地區,地址不是靜態分配的。這實際上是由於安全功能,通過隨機化某些區域的地址空間,使攻擊者更難以獲取他們感興趣的特定內存。但是有些區域總是固定的,因爲你需要它們要修復,以便您知道如何加載程序。我們可以看到程序數據和可執行內存始終是固定的vsyscall。實際上可以創建人們稱之爲“PIE”(位置無關的可執行文件)的東西,它實際上甚至使程序數據和可執行內存也隨機化,但是默認情況下不會啓用它,並且它也會阻止編譯程序靜態地,強制它被鏈接(https://sourceware.org/ml/binutils/2012-02/msg00249.html)。此外,“PIE”可執行文件會引發一些性能問題(32位與64位計算機上的不同類型的問題)。某些區域的地址隨機化稱爲“PIC”(位置無關代碼),並且已在Linux上默認啓用了相當長的一段時間。有關更多信息,請參閱:http://blog.fpmurphy.com/2008/06/position-independent-executables.html和http://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries
可以使用gcc -fPIE -pie ./hello.c -o hello生成“PIE”可執行文件來編譯上述程序。有一些關於nixpkgs的討論,默認情況下爲64位二進制文件編譯爲“PIE”,但由於嚴重的性能問題,32位二進制文件仍然是unPIEd。請參閱:https://github.com/NixOS/nixpkgs/issues/7220
順便說一句:如果我們有一個工具可以檢查/proc/$PID/maps並給出精確的人類可讀字節大小,那不是很好嗎?
讓我們詳細介紹每個地區。請記住,這仍然是程序的開始,沒有malloc發生,所以沒有[heap]區域。
0 - 400000 - 4194304 B - 4096 KiB ~ 4 MiB - NOT ALLOCATED
400000 - 401000 - 4096 B - 4 KiB
600000 - 601000 - 4096 B - 4 KiB
601000 - 602000 - 4096 B - 4 KiB
這是我們最初的記憶範圍。我添加了一個額外的組件,從0地址開始併到達40 00 00地址。地址似乎是包容性的,右側是獨佔的。但請記住,地址從0開始。因此,使用它bc <<< 'obase=10;ibase=16 400000 - 0'來獲取該範圍內的實際字節數而不添加或減去1 是合法的。在這種情況下,第一個未分配的區域大約爲4 MiB。當我說未分配時,我的意思是它沒有代表/proc/$PID/maps。這可能意味着兩件事情中的任何一件,要麼文件沒有顯示所有已分配的內存,要麼它不認爲這樣的內存值得顯示,或者實際上沒有分配內存。
我們可以找出是否真的有記憶有,通過創建介於兩者之間的指針內存地址0和400000,並嘗試取消引用它。這可以通過將整數轉換爲指針來完成。我之前嘗試過它,它會導致段錯誤,這意味着之間確實沒有分配內存0-400000
#包括 < stdio.h中>
int main(){
// 0x0是十六進制文字,默認爲有符號整數
//這裏我們將它轉換爲一個void指針
//然後將它賦值給一個聲明爲void指針的值
//這是創建一個任意指針的正確方法C
void * addr =( void *) 0x0 ;
//爲了打印出該指針存在的內容,我們必須取消引用指針
//但是C不知道如何處理void類型的值
//這意味着,我們重新設置了一個指向char指針的void指針
// char是一些任意字節,所以希望它是一個可打印的ASCII值
//實際上,我們不需要希望,因爲我們已經使printf專門打印了char的十六進制表示,因此它不需要是一個可打印的ascii value
printf( “ 0x %x \ n ”,(( char *)addr)[ 0 ]); //打印0x0
printf(“ 0x %x \ n ”,(( char *)addr)[ 1 ]); //打印0x1
printf( “ 0x %x \ n ”,(( char *)addr)[ 2 ]); //打印0x2
}
運行上面給我們一個簡單的segmentation fault。因此,證明這/proc/$PID/maps是給我們真相的,之間確實沒有任何關係0-400000。
問題變成了,爲什麼這個大約有4個MiB差距?爲什麼不從0開始分配內存?那麼這只是malloc和鏈接器實現者的任意選擇。他們只是決定在64位ELF可執行文件上,非PIE可執行文件的入口點應該是0x400000,而對於32位ELF可執行文件,入口點是0x08048000。一個有趣的事實是,如果您生成與位置無關的可執行文件,則起始地址將改爲0x0。
看到:
- http://stackoverflow.com/questions/7187981/whats-the-memory-before-0x08048000-used-for-in-32-bit-machine
- http://stackoverflow.com/questions/12488010/why-the-entry-point-address-in-my-executable-is-0x8048330-0x330-being-offset-of
- http://stackoverflow.com/questions/14314021/why-linux-gnu-linker-chose-address-0x400000
輸入地址由鏈接編輯器在創建可執行文件時設置。加載程序將程序文件映射到ELF頭指定的地址,然後將控制權轉移到入口地址。
加載地址是任意的,但是使用SYSV for x86進行了標準化。每種架構都有所不同。上面和下面的內容也是任意的,並且通常在庫和mmap()區域中鏈接。
它基本上意味着程序可執行文件在開始執行之前就被加載到內存中。可執行文件的入口點可以通過獲取readelf。但這是另一個問題,爲什麼要給出的入口點readelf,而不是0x400000。事實證明,該入口點是考慮OS應該開始執行的實際點,而0x400000入口點和入口點之間的位置用於EHDR和PHDR,這意味着ELF頭和程序頭。我們稍後會詳細研究這個問題。
$ readelf --file-header ./memory_layout | grep 'Entry point address'
Entry point address: 0x400720
接下來我們有:
400000 - 401000 - 4096 B - 4 KiB
600000 - 601000 - 4096 B - 4 KiB
601000 - 602000 - 4096 B - 4 KiB
正如您所看到的,我們有3個內存部分,每個部分都有4個KiB,並從中分配/home/vagrant/c_tests/memory_layout。
這些部分是什麼?
第一部分:“文本段”。
第二部分:“數據部分”。
第三部分:“BSS細分”。
文本段存儲進程的二進制映像。例如,數據段存儲由程序員初始化的靜態變量static char * foo = "bar";。例如,BSS段存儲未初始化的靜態變量,這些變量用零填充static char * username;。
我們的程序現在非常簡單,每個看起來都非常適合4 KiB。怎麼這麼完美!?
那麼,Linux OS和許多其他操作系統的頁面大小默認設置爲4 KiB。這意味着最小可尋址存儲器段是4 KiB。請參閱:https://en.wikipedia.org/wiki/Page_%28computer_memory%29
頁面,內存頁面或虛擬頁面是固定長度的連續虛擬內存塊,由頁表中的單個條目描述。它是虛擬內存操作系統中內存管理的最小數據單元。
運行getconf PAGESIZE顯示4096字節。
因此,這意味着每個段可能遠小於4096字節,但它最多可填充4096個字節。
如前所示,可以創建一個任意指針,並打印出該字節存儲的值。我們現在可以爲上面顯示的段執行此操作。
但是,嘿,我們可以做得更好。而不僅僅是黑客攻擊個別字節。我們可以認識到這些數據實際上是按結構組織的。
什麼樣的結構?我們可以查看readelf源代碼來揭示相關的結構。這些結構似乎不是標準C庫的一部分,因此我們不能只包含一些東西來實現這一點。但代碼很簡單,所以我們可以複製和粘貼。請參閱:http://rpm5.org/docs/api/readelf_8h-source.html
看一下這個:
//用gcc編譯-std = C99 -o elfheaders ./elfheaders.c
#包括 < stdio.h中>
#包括 < stdint.h >
//來自:http://rpm5.org/docs/api/readelf_8h-source.html
//這裏我們只關注64位可執行文件,32位可執行文件有不同大小的標題
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Off;
typedef uint64_t Elf64_Xword;
typedef uint32_t Elf64_Word;
typedef uint16_t Elf64_Half;
typedef uint8_t Elf64_Char;
#定義 EI_NIDENT 16
//這個結構正好是64個字節
//這意味着它來自0x400000 - 0x400040
typedef struct {
Elf64_Char e_ident [EI_NIDENT]; // 16 B
Elf64_Half e_type; // 2 B
Elf64_Half e_machine; // 2 B
Elf64_Word e_version; // 4 B
Elf64_Addr e_entry; // 8 B
Elf64_Off e_phoff; // 8 B
Elf64_Off e_shoff; // 8 B
Elf64_Word e_flags; // 4 B
Elf64_Half e_ehsize; // 2 B
Elf64_Half e_phentsize; // 2 B
Elf64_Half e_phnum; // 2 B
Elf64_Half e_shentsize; // 2 B
Elf64_Half e_shnum; // 2 B
Elf64_Half e_shstrndx; // 2 B
Elf64_Ehdr;
//這個結構正好是56個字節
//這意味着它來自0x400040 - 0x400078
typedef struct {
Elf64_Word p_type; // 4 B
Elf64_Word p_flags; // 4 B
Elf64_Off p_offset; // 8 B
Elf64_Addr p_vaddr; // 8 B
Elf64_Addr p_paddr; // 8 B
Elf64_Xword p_filesz; // 8 B
Elf64_Xword p_memsz; // 8 B
Elf64_Xword p_align; // 8 B
} Elf64_Phdr;
int main(int argc,char * argv []){
//從objdump的檢查和/ PROC / ID /地圖,我們可以看到,這是加載到內存中的第一件事
//最早在虛擬存儲器地址空間,對於64位的ELF可執行
//%LX需要64位十六進制,而%x僅適用於32位十六進制
Elf64_Ehdr * ehdr_addr =(Elf64_Ehdr *)0x400000 ;
printf(“魔術:0x ”);
for(unsigned int i = 0 ; i <EI_NIDENT; ++ i){
printf(“ %x ”,ehdr_addr-> e_ident [i]);
}
printf(“ \ n ”);
printf(“ Type:0x %x \ n ”,ehdr_addr-> e_type);
printf(“機器:0x %x \ n ”,ehdr_addr-> e_machine);
printf(“ Version:0x %x \ n ”,ehdr_addr-> e_version);
printf(“條目: %p \ n ”,(void *)ehdr_addr-> e_entry);
printf(“ Phdr Offset:0x %lx \ n ”,ehdr_addr-> e_phoff);
printf(“ Section Offset:0x %lx \ n ”,ehdr_addr-> e_shoff);
printf(“ Flags:0x %x \ n ”,ehdr_addr-> e_flags);
printf(“ ELF標題大小:0x %x \ n ”,ehdr_addr->e_ehsize);
printf(“ Phdr Header Size:0x %x \ n ”,ehdr_addr-> e_phentsize);
printf(“ Phdr Entry Count:0x %x \ n ”,ehdr_addr-> e_phnum);
printf(“ Section Header Size:0x %x \ n ”,ehdr_addr-> e_shentsize);
printf(“ Section Header Count:0x %x \ n ”,ehdr_addr-> e_shnum);
printf(“ Section Header Table Index:0x %x \ n ”,ehdr_addr-> e_shstrndx);
Elf64_Phdr * phdr_addr =(Elf64_Phdr *)0x400040 ;
printf(“ Type: %u \ n ”,phdr_addr-> p_type); // 6 - PT_PHDR - 段類型
printf(“標誌: %u \ n ”,phdr_addr-> p_flags); // 5 - PF_R + PF_X - rx權限等於chmod binary 101
printf(“ Offset:0x %lx \ n ”,phdr_addr-> p_offset); // 0x40 - 從第一個段所在的文件開頭的字節偏移量
printf(“程序虛擬地址: %p \ n ”,(void *)phdr_addr-> p_vaddr); // 0x400040 - 第一個段位於內存
printf中的虛擬地址(“ Program Physical Address:%p \ n ”,(void *)phdr_addr-> p_paddr); // 0x400040 -在該第一區段位於存儲器(不相干在Linux上)的物理地址
的printf(“加載文件大小:0X %LX \ n ”,phdr_addr-> p_filesz);// 504 - 從PHDR
printf 文件加載的字節數( “ Loaded mem size:0x %lx \ n ”,phdr_addr-> p_memsz); // 504 - 爲PHDR
printf 加載到內存中的字節( “ Alignment: %lu \ n ”,phdr_addr-> p_align); // 8 - 使用模運算對齊(mod p_vaddr palign)===(mod p_offset p_align)
返回 0 ;
}
運行上面給出:
$ ./elfheaders
Magic: 0x7f454c46211000000000
Type: 0x2
Machine: 0x3e
Version: 0x1
Entry: 0x400490
Phdr Offset: 0x40
Section Offset: 0x1178
Flags: 0x0
ELF Header Size: 0x40
Phdr Header Size: 0x38
Phdr Entry Count: 0x9
Section Header Size: 0x40
Section Header Count: 0x1e
Section Header Table Index: 0x1b
Type: 6
Flags: 5
Offset: 0x40
Program Virtual Address: 0x400040
Program Physical Address: 0x400040
Loaded file size: 0x1f8
Loaded mem size: 0x1f8
Alignment: 8
將上述輸出與:
$ readelf --file-header ./elfheaders
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400490
Start of program headers: 64 (bytes into file)
Start of section headers: 4472 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 27
我們基本上只是寫了自己的小readelf程序。
因此,它開始有意義地確定實際位於開頭的是什麼0x400000 - 0x401000,它是告訴操作系統如何使用該程序的所有ELF可執行標頭,以及所有其他有趣的元數據。具體來說,這是關於程序的實際入口點(for ./elfheader:0x400490和for ./memory_layout:) 0x400720與實際內存開始之間的位置0x400000。有更多的程序標題要研究,但現在已經足夠了。見:http://www.ouah.org/RevEng/x430.htm
但是操作系統從哪裏獲得這些數據呢?在將數據放入內存之前,它必須獲取這些數據。事實證明答案很簡單。它只是文件本身。
讓我們使用它hexdump來查看文件的實際二進制內容,以及稍後用於objdump將其反彙編到程序集以瞭解機器代碼。
顯然啓動內存地址,不會啓動文件地址。因此0x400000,文件最有可能始於0x0。
它是一段相當長的文本,所以將其融入其中less是一個好主意。注意,這*意味着“與上面的行相同”。
首先檢查前16個字節:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00。
注意這與下面顯示的魔術字節是一樣的readelf:
$ readelf -h ./memory_layout | grep Magic
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
事實證明,可以說,0x400000對於gccLinux上編譯的非PIE 64位ELF可執行文件0x0,與實際可執行文件本身的起點完全相同。
文件頭確實被加載到內存中。但我們可以判斷整個文件是否已加載到內存中?我們先來檢查文件大小。
$ stat memory_layout | grep Size
Size: 8932 Blocks: 24 IO Block: 4096 regular file
顯示該文件是8932字節,大約是8.7 KiB。
我們的內存佈局顯示,從memory_layout可執行文件中映射出最多4 KiB + 4 KiB + 4 KiB 。
有足夠的空間,當然足以適應文件的全部內容。
但我們可以通過迭代整個內存內容來證明這一點,並檢查內存中的相關偏移量,看它們是否與文件中的內容相匹配。
爲此,我們需要進行調查/proc/$PID/mem。但是,它不是一個普通的文件,你可以從中獲取,但你必須做一些有趣的系統調用來從中獲取一些輸出。沒有標準的unix工具可以從中讀取,而是我們需要編寫一個C程序來讀取它。這裏有一個示例程序:http://unix.stackexchange.com/a/251769/56970
幸運的是,有一個叫做的東西gdb,我們可以gcore用來將進程的內存轉儲到磁盤上。它需要超級用戶權限,因爲我們實際上是訪問進程的內存,而內存通常是隔離的!
$ sudo gcore 1255
Program received signal SIGTTIN, Stopped (tty input).
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f849c407350 in read () from /lib/x86_64-linux-gnu/libc.so.6
Saved corefile core.1255
[1]+ Stopped ./memory_layout
這會生成一個名爲的文件core.1255。這個文件是內存轉儲,所以要查看它,我們必須使用hexedit。
$ hexdump -C ./core.1255 | less
現在我們擁有了整個內存內容。讓我們嘗試將它與可執行文件本身進行比較。在我們能夠做到這一點之前,我們必須將二進制文件轉換爲可打印的ASCII。基本上是ASCII裝甲二進制程序。的xxd原因是爲了這個目的更好的hexdump爲我們提供了|字符時,我們使用它可以給混亂的輸出diff。
$ xxd ./core.1255 > ./memory.hex
$ xxd ./memory_layout > ./file.hex
我們馬上可以看到2種尺寸不一樣。在./memory.hex〜1.1 MIB是遠遠大於較大./file.hex〜37 KIB。這是因爲內存轉儲還包含所有共享庫和匿名映射的區域。但我們並不期望它們是相同的,只是整個文件本身是否存在於內存中。
現在可以使用兩種文件進行比較diff。
這告訴我們,即使存儲器內容和文件內容之間存在一些相似之處,它們也不完全相同。事實上,我們看到從17個字節開始的2個轉儲之間的偏差,這只是通過ELF魔術字節。
這表明即使存在從文件到內存的映射,它也不是完全相同的字節。無論是那個,還是轉儲和十六進制轉換中的某個地方,字節都被改變了。現在很難說出來。
無論如何,我們還可以使用它objdump來反彙編可執行文件,以查看文件中存在的實際彙編指令。需要注意的一點是,objdump使用程序的虛擬內存地址,就像要執行一樣。它沒有使用文件中的實際地址。由於我們知道了內存區域/proc/$PID/maps,我們可以檢查第一個400000 - 401000區域。
$ objdump --disassemble-all --start-address=0x000000 --stop-address=0x401000 ./memory_layout # use less of course
./memory_layout: file format elf64-x86-64
Disassembly of section .interp:
0000000000400238 <.interp>:
400238: 2f (bad)
400239: 6c insb (%dx),%es:(%rdi)
40023a: 69 62 36 34 2f 6c 64 imul $0x646c2f34,0x36(%rdx),%esp
400241: 2d 6c 69 6e 75 sub $0x756e696c,%eax
400246: 78 2d js 400275 <_init-0x3d3>
400248: 78 38 js 400282 <_init-0x3c6>
40024a: 36 ss
40024b: 2d 36 34 2e 73 sub $0x732e3436,%eax
400250: 6f outsl %ds:(%rsi),(%dx)
400251: 2e 32 00 xor %cs:(%rax),%al
Disassembly of section .note.ABI-tag:
0000000000400254 <.note.ABI-tag>:
400254: 04 00 add $0x0,%al
400256: 00 00 add %al,(%rax)
400258: 10 00 adc %al,(%rax)
40025a: 00 00 add %al,(%rax)
40025c: 01 00 add %eax,(%rax)
40025e: 00 00 add %al,(%rax)
400260: 47 rex.RXB
400261: 4e 55 rex.WRX push %rbp
400263: 00 00 add %al,(%rax)
400265: 00 00 add %al,(%rax)
400267: 00 02 add %al,(%rdx)
400269: 00 00 add %al,(%rax)
...
與gcore手動取消引用任意指針不同,我們可以看到objdump不能或不會向我們顯示內存內容400000 - 400238。相反,它開始顯示400238。這是因爲來自的東西400000 - 400238不是彙編指令,它們只是元數據,因此objdump不會打擾它們,因爲它是爲了轉儲彙編代碼而設計的。另一件需要理解的是,elipsis ...(在上面的例子中未顯示)(不要與我自己的...意思相混淆輸出是一個摘錄)意味着空字節。該objdump顯示逐字節機器代碼及其反編譯的等效彙編指令。這是一個反彙編程序,因此輸出程序集並不完全是人類寫的東西,因爲可以進行優化,並且丟棄了大量的語義信息。重要的是要注意右邊的十六進制地址表示起始字節地址,如果右邊有多個十六進制字節數字,則表示它們作爲單個彙編指令連接起來。所以兩者之間的差距400251 - 400254由3個十六進制字節表示2e 32 00。
讓我們跳到一個有趣的地方,比如0x400720報道的實際“入口點” readelf --file-header ./memory_layout。
$ objdump --disassemble-all --start-address=0x000000 --stop-address=0x401000 ./memory_layout | less +/400720
...
Disassembly of section .text:
0000000000400720 <_start>:
400720: 31 ed xor %ebp,%ebp
400722: 49 89 d1 mov %rdx,%r9
400725: 5e pop %rsi
400726: 48 89 e2 mov %rsp,%rdx
400729: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40072d: 50 push %rax
40072e: 54 push %rsp
40072f: 49 c7 c0 a0 09 40 00 mov $0x4009a0,%r8
400736: 48 c7 c1 30 09 40 00 mov $0x400930,%rcx
40073d: 48 c7 c7 62 08 40 00 mov $0x400862,%rdi
400744: e8 87 ff ff ff callq 4006d0 <__libc_start_main@plt>
400749: f4 hlt
40074a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
...
向上滾動,我們看到objdump將此報告爲實際.text部分,並且在400720此處,這是程序的入口點。我們這裏有的是由CPU執行的真正的第一個“過程”,即函數背後的main功能。當你避開運行時庫以生成一個獨立的C可執行文件時,我認爲你可以直接在C中使用它。這裏的程序集是x86 64位程序集(https://en.wikipedia.org/wiki/X86_assembly_language),我想這是爲了在向後兼容的Intel / AMD 64位處理器上運行。我不知道這個特定的程序集,所以我們稍後將在http://www.cs.virginia.edu/~evans/cs216/guides/x86.html中進行研究。
那麼我們的其他兩個部分(我們可以看到有一個跳過401000 - 600000,這也可以是鏈接器實現中的任意選擇):
600000 - 601000 - 4096 B - 4 KiB
601000 - 602000 - 4096 B - 4 KiB
$ objdump --disassemble-all --start-address=0x600000 --stop-address=0x602000 ./memory_layout | less
現在談的不多。它似乎0x600000包含更多的數據和彙編。但實際的地址.data和.bss似乎是:
Disassembly of section .data:
0000000000601068 <__data_start>:
...
0000000000601070 <__dso_handle>:
...
Disassembly of section .bss:
0000000000601078 <__bss_start>:
...
事實證明,我們沒有任何東西.data和.bss。這是因爲我們的./memory_layout.c程序中沒有任何靜態變量!
總結一下,我們對內存佈局的初步瞭解是:
0
Program Text (.text)
Initialised Data (.data)
Uninitialised Data (.bss)
Heap
|
v
Memory Mapped Region for Shared Libraries or Anything Else
^
|
User Stack
現在我們意識到它實際上是:
0
Nothing here, because it was just an arbitrary choice by the linker
ELF and Program and Section Headers - 0x400000 on 64 bit
Program Text (.text) - Entry Point as Reported by readelf
Nothing Here either
Some unknown assembly and data - 0x600000
Initialised Data (.data) - 0x601068
Uninitialised Data (.bss) - 0x601078
Heap
|
v
Memory Mapped Region for Shared Libraries or Anything Else
^
|
User Stack
好的,繼續吧。在我們的可執行文件內存之後,我們有一個巨大的跳躍601000 - 7f849c31b000。
這大概是127 Tebibytes的一大步。爲什麼地址空間如此大的跳躍?這就是malloc實現的用武之地。本文檔https://github.com/torvalds/linux/blob/master/Documentation/x86/x86_64/mm.txt以這種方式顯示了內存的結構:
我們可以看到,Linux的內存映射保留了第一個0000000000000000 - 00007fffffffffff用戶空間內存。事實證明,47位足以保留大約128 TiB。http://unix.stackexchange.com/a/64490/56970
好吧,如果我們看看這些內存的第一個和最後一個:
7f849c31b000-7f849c4d6000 r-xp 00000000 fc:00 1579071 /lib/x86_64-linux-gnu/libc-2.19.so
...
7fffb5dfe000-7fffb5e00000 r-xp 00000000 00:00 0 [vdso]
看起來這些區域幾乎處於爲用戶空間存儲器保留的128 TiB範圍的最底部。考慮到有一個127 TiB間隙,這基本上意味着我們的malloc使用0000000000000000 - 00007fffffffffff兩端的用戶空間範圍。從低端開始,它會向上擴展堆(在地址編號中向上)。在高端時,它會向下增加堆棧(在地址編號中向下)。
同時,堆棧實際上是內存的固定部分,因此它實際上不能像堆一樣增長。在高端,但低於堆棧,我們看到爲共享庫和共享庫可能使用的匿名緩衝區分配了大量內存區域。
我們還可以查看可執行文件正在使用的共享庫。這確定了在啓動時哪些共享庫也將加載到內存中。但請記住,庫和代碼也可以動態加載,鏈接器無法看到。順便說一下ldd,“列出動態依賴關係”。
$ ldd ./memory_layout
linux-vdso.so.1 => (0x00007fff1a573000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f361ab4e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f361a788000)
/lib64/ld-linux-x86-64.so.2 (0x00007f361ad7a000)
你會注意到,如果你ldd多次運行,每次打印共享庫的不同地址。這對應於多次重新運行程序並檢查/proc/$PID/maps它還顯示共享庫的不同地址。這是由於上面討論的“PIE”位置無關代碼。基本上每次使用ldd它都會調用鏈接器,鏈接器會執行地址隨機化。有關地址空間隨機化背後原因的更多信息,請參閱:ASLR。您還可以通過運行檢查內核是否已啓用ASLR cat /proc/sys/kernel/randomize_va_space。
我們可以看到實際上有4個共享庫。該vdso庫不是從文件系統加載的,而是由OS提供的。
另外:/lib64/ld-linux-x86-64.so.2 => /lib/x86_64-linux-gnu/ld-2.19.so,它是一個符號鏈接。
最後我們在最後幾個地區:
7fffb5d61000-7fffb5d82000 rw-p 00000000 00:00 0 [stack]
7fffb5dfe000-7fffb5e00000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
以下是每個地區的相關尺寸:
7fffb5d61000 - 7fffb5d82000 - [stack] - 135168 B - 132 KiB
7fffb5dfe000 - 7fffb5e00000 - [vdso] - 8192 B - 8 KiB
ffffffffff600000 - ffffffffff601000 - [vsyscall] - 4096 B - 4 KiB
我們的初始堆棧大小分配爲132 KiB。我懷疑這可以通過運行時或編譯器標誌來改變。
那是什麼vdso和vsyscall?兩者都是允許更快系統調用的機制,即在用戶空間和內核空間之間沒有上下文切換的系統調用。在vsyscall現在已經換成了vdso,但vsyscall留給有兼容性的原因。主要區別在於:
- vsyscall- ffffffffff600000即使啓用了PIC或PIE,也始終固定爲最大尺寸爲8 MiB
- vdso - 沒有修復,但行爲像共享庫,因此其地址受ASLR(地址空間佈局隨機化)的影響
- vsyscall- 提供3個系統調用: gettimeofday( 0xffffffffff600000),time(0xffffffffff600400),getcpu(0xffffffffff600800),即使它ffffffffff600000 - ffffffffffdfffff在64位ELF可執行文件中給出了Linux 的保留範圍8 MiB。
- vdso-提供了4個系統調用:__vdso_clock_gettime,__vdso_getcpu,__vdso_gettimeofday和__vdso_time,然而更多的系統調用可以被添加到vdso在未來。
有關更多信息vdso,並vsyscall請參閱:https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-3.html
值得指出的是,現在我們已經超過了爲用戶空間內存保留的128 TiB,我們現在正在查看由操作系統提供和管理的內存段。如下所示:https://github.com/torvalds/linux/blob/master/Documentation/x86/x86_64/mm.txt這些部分是我們正在討論的內容。
ffff800000000000 - ffff87ffffffffff (=43 bits) guard hole, reserved for hypervisor
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole
ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space
ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole
ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB)
... unused hole ...
ffffec0000000000 - fffffc0000000000 (=44 bits) kasan shadow memory (16TB)
... unused hole ...
ffffff0000000000 - ffffff7fffffffff (=39 bits) %esp fixup stacks
... unused hole ...
ffffffef00000000 - ffffffff00000000 (=64 GB) EFI region mapping space
... unused hole ...
ffffffff80000000 - ffffffffa0000000 (=512 MB) kernel text mapping, from phys 0
ffffffffa0000000 - ffffffffff5fffff (=1525 MB) module mapping space
ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls
ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole
在上述部分中,我們目前只看到該vsyscall地區。其餘的還沒有出現。
現在讓我們繼續該程序,並分配我們的第一個堆。我們/proc/$PID/maps現在(注意地址已經改變,因爲我重新編寫了程序):
我們現在看到我們的第一個[heap]區域 它確切地說:135168 B - 132 KiB。(目前與我們的堆棧大小相同!)記住我們特別分配了1000個字節:addr = (char *) malloc(1000);在開頭。那麼一個1000字節怎麼變成132千比特?正如我們之前所說的,任何小於MMAP_THRESHOLD使用brk系統調用的東西。似乎使用填充大小調用brk/ sbrk,以減少系統調用的數量和上下文切換的數量。大多數程序最有可能需要超過1000字節的堆,因此係統也可以填充brk調用以緩存一些堆內存,並且只有在耗盡132 KiB的填充堆後纔會出現新的brk或mmap增加堆的調用。填充計算完成:
/* Request enough space for nb + pad + overhead */
size = nb + mp_.top_pad + MINSIZE;
凡mp_.top_pad被默認設置爲128 * 1024 = 128昆明植物研究所。我們仍然有4個KiB差異。但請記住,我們的頁面大小getconf PAGESIZE爲4096,意味着每頁爲4 KiB。這意味着在我們的程序中分配1000字節時,分配的整頁爲4 KiB。並且4 KiB + 128 KiB是132 KiB,這是我們堆的大小。此填充不是固定大小的填充,而是總是添加到通過brk/ 分配的數量的填充sbrk。這意味着默認情況下128 KiB總是會添加到您嘗試分配的內存中。然而,這個填充僅適用於brk/ sbrk,而不是mmap,記住過去MMAP_THRESHOLD,mmap從brk/ 接管sbrk。這意味着將不再應用填充。但是我不確定是否MMAP_THRESHOLD在填充之前或填充之後檢查。它似乎應該在填充之前。
可以通過調用來更改填充大小mallopt(M_TOP_PAD, 1);,將其更改M_TOP_PAD爲1字節。Mallocing 1000 Bytes現在只會創建一個4 KiB的頁面。
有關更多信息,請參閱:http://stackoverflow.com/a/23951267/582917
當分配等於或大於?時,爲什麼舊的brk/ sbrk被更新的替換?那麼/ 調用只允許連續增加堆的大小。如果你只是用於小事情,它應該能夠在堆中連續分配,當它到達堆端時,堆可以擴展而沒有任何問題。但是對於更大的allocatiosn,使用了,並且這個堆空間確實需要與/ heap空間連續地連接。所以它更靈活。在這種情況下,小型對象的內存碎片會減少。另外,通話更加靈活,從而使/ 可以實現,而mmapMMAP_THRESHOLDbrksbrkmallocmmapbrksbrkmmapbrksbrkmmapmmap無法用brk/ 實現sbrk。brk/的一個限制sbrk是,如果未釋放brk/ sbrkheap空間中的頂部字節,則不能減小堆大小。
讓我們看一個分配超過的簡單程序MMAP_THRESHOLD(它也可以被覆蓋使用mallopt):
#包括 < stdlib.h中>
#包括 < stdio.h中>
int main(){
printf(“查看/ proc / %d / maps \ n ”,getpid());
//分配200 KiB,強制使用mmap而不是brk
char * addr =( char *) malloc( 204800);
getchar();
free(addr);
返回 0 ;
}
運行上面的代碼strace給我們:
mmap在上面的strace中有很多電話。我們如何找到mmap我們的程序調用,而不是共享庫或鏈接器或其他東西?最近的電話就是這個:
mmap(NULL, 208896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3b11972000
它實際上是204800 B + 4096 B = 208 896 B.我不確定爲什麼要添加額外的4 KiB,因爲200 KiB可以被4 KiB的頁面大小完全整除。這可能是另一個特點。需要注意的一點是,沒有任何明顯的方法可以確定我們的程序的系統調用超過其他一些系統調用,但我們可以查看調用的上下文,即前一行和後續行以查找確切的控制流。使用像這樣的東西getchar也可以暫停strace。在最終被調用之前,在映射204800個字節之後,可以考慮進行fstat另一個mmap調用getchar。我不知道這些調用來自哪裏,所以在將來,我們應該尋找一些簡單的方法來標記系統調用,以便我們可以更快地找到它們。該strace告訴我們這個內存映射區域被映射到0x7f3b11972000。看看這個過程/proc/$PID/maps:
我們可以在這裏找到mmapped堆:
7f3b11972000-7f3b119a8000 rw-p 00000000 00:00 0
如您所見,當malloc切換到使用時mmap,獲取的區域不是所謂[heap]區域的一部分,該區域僅由brk/ sbrkcalls提供。它沒有標籤!我們還可以看到,這種堆不與brk/ sbrkheap 放在同一區域,我們理解它是從低端開始並在地址空間中向上增長。相反,這個mmapped堆與共享庫位於同一區域,將其置於保留用戶空間地址範圍的高端。然而,如圖所示的這個區域/proc/$PID/maps實際上是221184 B - 216 KiB。它從208896年正好是12 KiB。另一個謎!爲什麼我們有不同的字節大小,即使mmap在被strace稱爲完全208896?
看另一個mmap電話也表明相應的區域/proc/$PID/maps有12 KiB的差異。這裏12 KiB可能代表某種內存映射開銷,malloc用它來跟蹤或理解這裏可用的內存類型。或者它也可以是額外的填充。所以我們可以在這裏說,有些東西一直在爲我們的mmapping添加12 KiB,而且我的200 KiB還增加了4 KiB malloc。
順便說一句,還有一個工具叫做binwalk,它對於檢查可能包含多個可執行文件和元數據的固件映像非常有用。記住,事實上你可以將文件嵌入到可執行文件中。這有點像病毒的工作方式。我用它來檢查NixOS的initrd並弄清楚它是如何構造的。與它結合使用dd可以輕鬆切割和切割和拼接二進制blob!
此時,我們可以繼續從原始程序中調查堆,並且線程也是如此。但我現在停在這裏。