Linux中main是如何执行的
Linux中main是如何執行的
這是一個看似簡單的問題,但是要從Linux底層一點點研究問題比較多。找到了一遍研究這個問題的文章,但可能比較老了,還是在x86機器上進行的測試。
原文鏈接
開始
問題很簡單:linux是怎么執行我的main()函數的?
在這片文檔中,我將使用下面的一個簡單c程序來闡述它是如何工作的。這個c程序的文件叫做"simple.c"
編譯
gcc -o simple simple.c生成可執行文件simple.
在可執行文件中有些什么?
為了看到在可執行文件中有什么,我們使用一個工具"objdump"
objdump -f simplesimple: file format elf32-i386 architecture: i386, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x080482d0輸出給出了一些關鍵信息。首先,這個文件的格式是"ELF64"。其次是給出了程序執行的開始地址 "0x080482d0"
什么是ELF?
ELF是執行和鏈接格式(Execurable and Linking Format)的縮略詞。它是UNIX系統的幾種可執行文件格式中的一種。對于我們的這次探討,有關ELF的有意思的地方是它的頭格式。每個ELF可執行文件都有ELF頭,像下面這個樣子:
typedef struct {unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */Elf32_Half e_type; /* Object file type */Elf32_Half e_machine; /* Architecture */Elf32_Word e_version; /* Object file version */Elf32_Addr e_entry; /* Entry point virtual address */Elf32_Off e_phoff; /* Program header table file offset */Elf32_Off e_shoff; /* Section header table file offset */Elf32_Word e_flags; /* Processor-specific flags */Elf32_Half e_ehsize; /* ELF header size in bytes */Elf32_Half e_phentsize; /* Program header table entry size */Elf32_Half e_phnum; /* Program header table entry count */Elf32_Half e_shentsize; /* Section header table entry size */Elf32_Half e_shnum; /* Section header table entry count */Elf32_Half e_shstrndx; /* Section header string table index */ } Elf32_Ehdr;上面的結構中,"e_entry"字段是可執行文件的開始地址。
地址"0x080482d0"上存放的是什么?是程序執行的開始地址么?
對于這個問題,我們來對"simple"做一下反匯編。有幾種工具可以用來對可執行文件進行反匯編。我在這里使用了objdump:
objdump --disassemble simple輸出結果有點長,我不會分析objdump的所有輸出。我們的意圖是看一下地址0x080482d0上存放的是什么。下面是輸出:
080482d0 <_start>:80482d0: 31 ed xor %ebp,%ebp80482d2: 5e pop %esi80482d3: 89 e1 mov %esp,%ecx80482d5: 83 e4 f0 and $0xfffffff0,%esp80482d8: 50 push %eax80482d9: 54 push %esp80482da: 52 push %edx80482db: 68 20 84 04 08 push $0x804842080482e0: 68 74 82 04 08 push $0x804827480482e5: 51 push %ecx80482e6: 56 push %esi80482e7: 68 d0 83 04 08 push $0x80483d080482ec: e8 cb ff ff ff call 80482bc <_init+0x48>80482f1: f4 hlt 80482f2: 89 f6 mov %esi,%esi看上去開始地址上存放的是叫做"_start"的啟動例程。它所做的是清空寄存器,向棧中push一些數據并且調用一個函數。
Stack Top -------------------0x80483d-------------------esi-------------------ecx-------------------0x8048274-------------------0x8048420-------------------edx-------------------esp-------------------eax-------------------三個問題
現在,可能你已經想到了,關于這個棧幀我們有一些問題。
- 這些16進制數是什么?
- 地址80482bc上存放的是什么,哪個函數被_start調用了?
- 看起來這些匯編指令并沒有用一些有意義的值來初始化寄存器。那么誰來初始化這些寄存器?
讓我們來一個一個回答這個問題。
Q1>關于16進制數
如果你仔細研究了用objdump得到的反匯編輸出,你就能很容易回答這個問題。
下面是這個問題的回答:
0x80483d0: 這是main()函數的地址。
0x8048274: _init()函數的地址。
0x8048420: _finit()函數地址。
_init和_finit是GCC提供的initialization/finalization 函數。
現在,我們不要去關心這些東西。基本上所有這些16進制數都是函數指針。
Q2>地址80482bc上存放的是什么?
讓我們再次在反匯編輸出中尋找地址80482bc。
如果你看到了,匯編代碼如下:
這里的*0x8049548是一個指針操作。它跳到地址0x8049548存儲的地址值上。
更多關于ELF和動態鏈接
使用ELF,我們可以編譯出一個可執行文件,它動態鏈接到幾個libraries上。這里的"動態鏈接"意味著實際的鏈接過程發生在運行時。否則我們就得編譯出一個巨大的可執行文件,這個文件包含了它所調用的所有libraries("一個『靜態鏈接的可執行文件』")。如果你執行下面的命令:
ldd simplelibc.so.6 => /lib/i686/libc.so.6 (0x42000000)/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)你就能看到simple動態鏈接的所有libraries。所有動態鏈接的數據和函數都有『動態重定向入口(dynamic relocation entry)』。
這個概念粗略的講述如下:
我們通過使用objdump命令可以看到所有的動態鏈接入口:
objdump -R simplesimple: file format elf32-i386DYNAMIC RELOCATION RECORDSOFFSET TYPE VALUE 0804954c R_386_GLOB_DAT __gmon_start__08049540 R_386_JUMP_SLOT __register_frame_info08049544 R_386_JUMP_SLOT __deregister_frame_info08049548 R_386_JUMP_SLOT __libc_start_main這里的地址0x8049548被叫做"JUMP SLOT",非常貼切。根據這個表,實際上我們想調用的是 __libc_start_main。
__libc_start_main是什么?
我們在玩一個接力游戲,現在球被傳到了libc的手上。__libc_start_main是libc.so.6中的一個函數。如果你在glibc中查找__libc_start_main的源碼,它的原型可能是這樣的:
extern int BP_SYM (__libc_start_main) (int (*main) (int, char **, char **),int argc,char *__unbounded *__unbounded ubp_av,void (*init) (void),void (*fini) (void),void (*rtld_fini) (void),void *__unbounded stack_end) __attribute__ ((noreturn));所有匯編指令需要做的就是建立一個參數棧然后調用__libc_start_main。這個函數需要做的是建立/初始化一些數據結構/環境然后調用我們的main()。讓我們看一下關于這個函數原型的棧幀,
Stack Top -------------------0x80483d0 main------------------- esi argc------------------- ecx argv ------------------- 0x8048274 _init------------------- 0x8048420 _fini------------------- edx _rtlf_fini------------------- esp stack_end------------------- eax this is 0-------------------根據這個棧幀我們得知,esi,ecx,edx,esp,eax寄存器在函數 __libc_start_main()被執行前需要被填充合適的值。很清楚的是這些寄存器不是被前面我們所展示的啟動匯編指令所填充的。那么,誰填充了這些寄存器呢?現在只留下唯一的一個地方了——內核。現在讓我們回到第三個問題上。
Q3>內核做了些什么?
當我們通過在shell上輸入一個名字來執行一個程序時,下面是Linux接下來會發生的:
- ebx:執行程序名字的字符串
- ecx:argv數組指針
- edx:環境變量數組指針
當執行_start匯編指令時,棧幀會是下面這個樣子。
Stack Top -------------argc-------------argv pointer-------------env pointer-------------匯編指令通過以下方式從棧中獲取所有信息:
pop %esi <--- get argc move %esp, %ecx <--- get argvactually the argv address is the same as the currentstack pointer.現在所有東西都準備好了,可以開始執行了。
其他的寄存器呢?
對于esp來說,它被用來當做應用程序的棧底。在彈出所有必要信息之后,_start例程簡單的調整了棧指針(esp)——關閉了esp寄存器4個低地址位,這完全是有道理的,對于我們的main程序,這就是棧底。對于edx,它被rtld_fini使用,這是一種應用析構函數,內核使用下面的宏定義將它設為0:
#define ELF_PLAT_INIT(_r) do { \_r->ebx = 0; _r->ecx = 0; _r->edx = 0; \_r->esi = 0; _r->edi = 0; _r->ebp = 0; \_r->eax = 0; \ } while (0)0意味著在x86 Linux上我們不會使用這個功能。
關于匯編指令
這些匯編codes來自哪里?它是GCC codes的一部分。這些code的目標文件通常在/usr/lib/gcc-lib/i386-redhat-linux/XXX 和 /usr/lib下面,XXX是gcc版本號。文件名為crtbegin.o,crtend.o和gcrt1.o。
總結
我們總結一下整個過程。
結論
在Linux中,我們的C main()函數由GCC,libc和Linux二進制加載器的共同協作來執行。
參考
objdump "man objdump" ELF header /usr/include/elf.h __libc_start_main glibc source ./sysdeps/generic/libc-start.c sys_execve linux kernel source code arch/i386/kernel/process.c do_execve linux kernel source code fs/exec.c struct linux_binfmt linux kernel source code include/linux/binfmts.h load_elf_binary linux kernel source codefs/binfmt_elf.c create_elf_tables linux kernel source code fs/binfmt_elf.c start_thread linux kernel source code include/asm/processor.h
作者: HarlanC
博客地址: http://www.cnblogs.com/harlanc/
個人博客: http://www.harlancn.me/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出, 原文鏈接
如果覺的博主寫的可以,收到您的贊會是很大的動力,如果您覺的不好,您可以投反對票,但麻煩您留言寫下問題在哪里,這樣才能共同進步。謝謝!
總結
以上是生活随笔為你收集整理的Linux中main是如何执行的的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: df命令,du命令,磁盘分区
- 下一篇: 学习笔记(11月03日)