JVM和垃圾回收:從C代碼執行過程看編譯器和作業系統協同工作

大數據架構師 發佈 2022-11-11T13:19:30.642992+00:00

從C代碼執行過程看編譯器和作業系統協同工作本節通過一個簡單的C代碼在Linux下執行的過程,介紹編譯器和OS是如何分工、合作完成代碼的執行。

從C代碼執行過程看編譯器和作業系統協同工作

本節通過一個簡單的C代碼在Linux下執行的過程,介紹編譯器和OS是如何分工、合作完成代碼的執行。

從原始碼到目標代碼

一個簡單的C示例如下:

int global_count = 10;

int add(int i, int j){

return i + j;

}

main(){

int i = 3;

int j = 5;

int result = add(i, j);

}

該示例非常簡單,不存在動態連結,編譯、連結完成後即可在OS中執行。但是程序要在OS上執行,需要符合OS的執行要求,主要包括:

產生的可執行文件必須符合格式規範(如Linux中必須符合ELF格式)。

可執行文件中內容的組織符合OS執行程序的約定規範,例如程序在執行時由數據段(data segment)、代碼段(text segment)等組成。

以Linux系統為例,上面的原始碼和編譯生成的可執行文件(ELF格式)的對應關係如下圖1-3所示。

以Linux/X86-64為例,通過gcc編譯器對上述代碼進行編譯,產生目標文件。文件格式為ELF,可以使用objdump命令(或readelf命令)對編譯後的目標文件進行解析。首先可以確認一下數據段的信息,如下所示:

Disassembly of section .data:

0000000000600868 <__data_start>:

600868: 00 00 add %al,(%rax)

……

000000000060086c <global_count>:

60086c: 0a 00 or (%rax),%al

在這個數據段中有兩個變量__data_start和global_count,其中global_count是代碼中定義的全局變量,可以看到該變量占用的空間為4位元組,初始值為10;而__data_start是gcc在連結時創建的一個全局變量,該變量指向數據段開始的位置,該變量的大小也是4位元組。

編譯器除了滿足OS對於可執行文件的約定規範外,其中一個重要的功能就是針對代碼進行編譯優化(當然也包含了內存數據的布局等)。

接下來看一下代碼段的內容。代碼段非常長,這裡只關注add函數的彙編代碼,如下所示:

220000000000400474 <add>:

400474: 55 push %rbp

400475: 48 89 e5 mov %rsp,%rbp

400478: 89 7d fc mov %edi,-0x4(%rbp)

40047b: 89 75 f8 mov %esi,-0x8(%rbp)

40047e: 8b 45 f8 mov -0x8(%rbp),%eax

400481: 8b 55 fc mov -0x4(%rbp),%edx

400484: 8d 04 02 lea (%rdx,%rax,1),%eax

400487: c9 leaveq

400488: c3 retq

注意:

在gcc編譯過程中採用的是默認編譯優化級別(默認編譯優化級別為O0),如果採用不同的編譯優化級別,生成的代碼會略有不同。

在C/C++中,編譯優化體現在原始碼的編譯時間長短不同,同時不同的編譯代碼執行效率也會不同。在JVM的執行過程中也存在同樣的問題,並且因為JVM在編譯代碼執行過程中需要先等待編譯代碼完成後才能執行,所以編譯時長會直接影響應用執行的性能。

作業系統如何執行目標代碼

OS首先讀取ELF文件,按照進程執行時內存的布局把ELF文件的信息加載到內存中。在64位Linux環境下,文件到內存的映射以及加載後內存的布局如圖1-4所示。

代碼的入口地址位於0x00400000處(32位系統位於0x08048000),本程序真正執行的地址開始於0x00400390(可以從objdump中看到該信息,此處對0進行了省略)。

architecture: i386:X86-64, flags 0x00000112:

EXEC_P, HAS_SYMS, D_PAGED

start address 0x0000000000400390

該地址對應的代碼可以在代碼段中找到。彙編代碼如下:

0000000000400390 <_start>:

400390: 31 ed xor %ebp,%ebp

400392: 49 89 d1 mov %rdx,%r9

400395: 5e pop %rsi

400396: 48 89 e2 mov %rsp,%rdx

400399: 48 83 e4 f0 and

$0xfffffffffffffff0,%rsp

40039d: 50 push %rax

40039e: 54 push %rsp

40039f: 49 c7 c0 c0 04 40 00 mov $0x4004c0,%r8

4003a6: 48 c7 c1 d0 04 40 00 mov $0x4004d0,%rcx

4003ad: 48 c7 c7 89 04 40 00 mov $0x400489,%rdi

4003b4: e8 c7 ff ff ff callq 400380

24<__libc_start_main@plt>

4003b9: f4 hlt

該代碼是gcc生成的,它作為入口地址,從此處開始執行。它將通過glibc的庫函數_libc_start_main執行到原始碼中的main函數中(具體細節可以參考其他書籍)。

在上面的代碼示例中,main函數調用了add函數,這裡簡單演示一下從main函數到add函數的執行過程,主要關注棧的變化情況。main函數的彙編代碼如下:

0000000000400489 <main>:

400489: 55 push %rbp

40048a: 48 89 e5 mov %rsp,%rbp

40048d: 48 83 ec 10 sub $0x10,%rsp

400491: c7 45 f4 03 00 00 00 movl $0x3,-0xc(%rbp)

400498: c7 45 f8 05 00 00 00 movl $0x5,-0x8(%rbp)

40049f: 8b 55 f8 mov -0x8(%rbp),%edx

4004a2: 8b 45 f4 mov -0xc(%rbp),%eax

4004a5: 89 d6 mov %edx,%esi

4004a7: 89 c7 mov %eax,%edi

4004a9: e8 c6 ff ff ff callq 400474 <add>

4004ae: 89 45 fc mov %eax,-0x4(%rbp)

4004b1: c9 leaveq

4004b2: c3 retq

從main函數到執行callq指令之前,棧的情況如圖1-5所示。

從圖1-5中可以看到,在調用add之前,main函數需要將參數以及add函數後的下一條指令地址入棧(由於此處add函數需要傳遞的參數比較少,因此直接使用寄存器傳遞。但是需要注意的是main函數中仍然有局部遍歷i和j,它們也在棧中分配),其中傳遞的參數被add函數使用,返回地址用於add函數執行完成後繼續返回main函數執行。當進入add函數中後,棧的情況如圖1-6所示。

棧幀的變化是OS根據晶片的調用約定組織的,不同的晶片有不同的調用約定。在JVM編譯優化中也需要按照調用約定實現相關的代碼。

本文給大家講解的內容是Java虛擬機和垃圾回收基礎知識:Java代碼執行過程介紹, 從C代碼執行過程看編譯器和作業系統協同工作

  1. 下篇文章給大家講解的內容是Java虛擬機和垃圾回收基礎知識:Java代碼執行過程介紹,  從C++代碼的執行過程看編譯器支持面向對象語言
  2. 覺得文章不錯的朋友可以轉發此文關注小編;
  3. 感謝大家的支持
關鍵字: