匯編語言data segment,匯編@data_macOS上的匯編入門(十三)——從編譯到執行

 2023-12-10 阅读 28 评论 0

摘要:作為這一系列文章中的最后一篇,這篇文章我打算討論的是從編譯到執行的全過程。因為許多地方都是要有了匯編的基礎知識以后才方便討論,所以我把它放到了最后一篇。編譯匯編語言data segment。編譯并不是對匯編代碼來說的,而是對更高級的語言,如C、

作為這一系列文章中的最后一篇,這篇文章我打算討論的是從編譯到執行的全過程。因為許多地方都是要有了匯編的基礎知識以后才方便討論,所以我把它放到了最后一篇。

編譯

匯編語言data segment。編譯并不是對匯編代碼來說的,而是對更高級的語言,如C、C++來說的。如果一個語言最終的編譯結果是可執行文件,那么它一定會先被編譯為匯編語言,然后再被匯編、鏈接為可執行文件。對于C和C++來說,大部分的編譯器都支持輸出匯編結果。比如說對于test.c, 我們想查看其編譯后的匯編代碼,只需要在命令行中鍵入

clang test.c -S -o test.s

然后就會生成一個包含其匯編代碼的test.s文件。

arm匯編指令詳解、研究編譯器生成的匯編代碼很有意義。因為現代的編譯器,其都針對不同的平臺、架構有許多優化,這對于我們寫匯編代碼是很有意義的。比如說,對

return 0;

的編譯結果,是

xorl    %eax, %eax
retq

事實上,通過異或自身來清零這一操作,在任何架構上都是最高效的。

匯編

所謂匯編,就是輸入我們的匯編代碼,輸出目標文件。什么是目標文件呢?假設我們有一個匯編文件test.s, 然后我們利用

as test.s -o test.o

生成一個test.o文件。然后,我們在終端下利用file指令查看其文件類型:

$ file test.o
test.o: Mach-O 64-bit object x86_64

可以看到, 這個文件是object, 也就是目標文件。

那么,目標文件是做什么用的呢?要了解這個,首先我們需要知道「匯編」這一步驟究竟做了什么。

我們知道,匯編語言可以看作機器碼的human-readable版本。因此,從最直觀來看,匯編只需要把匯編代碼翻譯為機器碼就ok了,也就是匯編代碼直接變成可執行文件。這個粗略來看是對的,對于大多數代碼來說,確實直接翻譯為機器碼就好了。但是,如果真的是這樣,隨著人們寫的代碼越來越多,匯編器的有一項工作的負擔就越來越重——翻譯符號。我們之前在匯編語言中大量運用了標簽,一個標簽就對應一個地址。此外,我們也可以引用別的文件、動態鏈接庫的標簽。因此,對于一個標簽,其可能的情況有好多好多種。所以,人們就把這部分功能從匯編器中解放出來,同時,匯編器就變成了對于一個匯編文件,輸出其目標文件。目標文件幾乎包含的就是可執行文件中的機器碼,但是標簽部分卻是空缺的。其會把所有遇到的符號放到一個符號表中,以便查閱。

舉個例子,我們現在有兩個匯編程序test.stmp.s, 其代碼分別如下:

tmp.s:

# tmp.s.data.globl  tmp_var
tmp_var:    .quad   0x114514.text.globl  _tmp_func
_tmp_func:retq

test.s:

# test.s.data
var:    .asciz  "hello, world!n".text.globl  _main
_func:retq_main:pushq   %rbpcallq   _func   # internal callleaq    var(%rip), %rdi # internal variablemovb    $0, %alcallq   _printf # dylib callmovq    tmp_var(%rip), %rdi # external variablecallq   _tmp_func   # external variablepopq    %rbpmovq    $0, %raxretq

其中主函數位于test.s. 且test.s分別包含了對本文件下函數的調用、本文件下變量的訪問、動態鏈接庫中函數的調用、外部文件中函數的調用和外部文件中變量的訪問。

我們在終端中依次鍵入

as test.s -o test.o
as tmp.s -o tmp.o

得到兩個目標文件。我們利用

otool -v -t test.o

可以查看test.o文件中__TEXT__text節的代碼:

test.o:
(__TEXT,__text) section
_func:
0000000000000000    retq
_main:
0000000000000001    pushq   %rbp
0000000000000002    callq   0x7
0000000000000007    leaq    (%rip), %rdi
000000000000000e    movb    $0x0, %al
0000000000000010    callq   0x15
0000000000000015    movq    (%rip), %rdi
000000000000001c    callq   0x21
0000000000000021    popq    %rbp
0000000000000022    movq    $0x0, %rax
0000000000000029    retq

同時,我們在終端中鍵入

nm -n -m test.o

可以查看test.o的符號表:

(undefined) external _printf(undefined) external _tmp_func(undefined) external tmp_var
0000000000000000 (__TEXT,__text) non-external _func
0000000000000001 (__TEXT,__text) external _main
000000000000002a (__DATA,__data) non-external var

可以看到,對于本文件中定義的符號,符號表中已經有了位置,同時依據是否用.globl聲明區分為external和non-external. 對于未在本文件中定義的符號,都是undefined.

鏈接

之前我們講到的符號定位的功能,就是鏈接的作用。鏈接器接收多個目標文件,最終輸出為一個可執行文件。對于剛剛我們生成的兩個目標文件test.otmp.o, 我們在終端中鍵入

ld test.o tmp.o -o test -lSystem

得到可執行文件test. 我們利用otool查看其__TEXT__text節的代碼為:

test:
(__TEXT,__text) section
_func:
0000000100000f6b    retq
_main:
0000000100000f6c    pushq   %rbp
0000000100000f6d    callq   0x100000f6b
0000000100000f72    leaq    0x1097(%rip), %rdi
0000000100000f79    movb    $0x0, %al
0000000100000f7b    callq   0x100000f96
0000000100000f80    movq    0x1098(%rip), %rdi
0000000100000f87    callq   0x100000f95
0000000100000f8c    popq    %rbp
0000000100000f8d    movq    $0x0, %rax
0000000100000f94    retq
_tmp_func:
0000000100000f95    retq

可以看到,鏈接器將兩個目標文件的段合并了。同一個段同一個節中的代碼被放在了一起。此外,之前標簽處占位的地址,現在也變成了正確的地址。

接著,我們利用nm查看其符號表:

(undefined) external _printf (from libSystem)(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f6b (__TEXT,__text) non-external _func
0000000100000f6c (__TEXT,__text) external _main
0000000100000f95 (__TEXT,__text) external _tmp_func
0000000100002008 (__DATA,__data) non-external __dyld_private
0000000100002010 (__DATA,__data) non-external var
000000010000201f (__DATA,__data) external tmp_var

其中多出來的dyld_stub_binder等只是為了動態鏈接,我們暫時不考慮。我們發現,之前處于undefined狀態的_tmp_functmp_var現在已經被定義了。而且_printf這樣的動態鏈接庫中的函數,也被確定是from libSystem了。這就是鏈接器的主要作用。

動態鏈接

我剛剛上面多次提到了動態鏈接庫,那么,動態鏈接究竟是什么呢?

首先,我們考慮一個問題。我們知道,有許多庫函數如_printf等都是十分常用的,所以許多文件在鏈接時都要鏈接包含這些庫函數的文件。那么,如果我們的這些庫函數像上面的匯編過程一樣,包含在某些.o文件中,比如說lib.o. 那么,作為鏈接器,ld會將這些實現_printf的匯編代碼合并到最終的可執行文件中。當可執行文件執行的時候,又會將這部分代碼放到內存中。那么,假設我們同時運行10個鏈接了lib.o的可執行文件,那么,內存中同樣的代碼有10份。這顯然是不可以接受的。

此外,還有一個問題。我們知道,系統是不斷升級的。那么,系統提供的庫函數也會隨著時間的變化而不斷升級。如果所有的庫函數都像上面描述的那樣,作為代碼直接寫死到可執行文件里面去,那么,每次升級過后,之前鏈接了這些庫函數的可執行文件,使用的依然是老舊的庫函數。如果要使用新的庫函數,還得重新鏈接。這顯然也是不可以接受的。

為了解決這兩個問題,動態鏈接就應運而生了。與匯編、鏈接不同,動態鏈接是在執行階段的。我們的庫函數,都被放到了一個以.dylib結尾的動態鏈接庫中。我們在使用ld鏈接的時候,也可以鏈接動態鏈接庫,如-lSystem選項實質上就是鏈接了動態鏈接庫libSystem.dylib. 鏈接器如果遇到動態鏈接庫,那么只會給符號重定位,而不會將代碼整合到可執行文件中。同時,可執行文件中會包含其鏈接的動態鏈接庫。我們也可以利用otool查看某個可執行文件鏈接的動態鏈接庫,比如說,對于上述的可執行文件test, 我們在終端下鍵入:

otool -L test

然后就會出現其鏈接的動態鏈接庫(實際上libSystem.dyliblibSystem.B.dylib的一個軟鏈接,說不定以后庫文件大規模升級以后,就會軟鏈接到libSystem.C.dylib):

test:/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)

然后,到程序執行的時候,就是動態鏈接器dyld發揮的時候了。順便一提,Apple的dyld是開源的,可以去opensource-apple/dyld上查看。

當程序執行的時候,首先,內核將代碼裝載入其邏輯地址空間,然后,又裝載了動態鏈接器。接著,內核就把控制權轉交給dyld. 動態鏈接器做的,是找到這個可執行文件鏈接的動態鏈接器,然后把它們裝載入邏輯地址空間。用一個圖表示如下:

8013683df0fa4ece7699fafa75f35b36.png

注意到,我們提到的是將動態鏈接庫裝載入邏輯地址空間。事實上,在物理內存中,動態鏈接庫只有一份。而內存映射單元MMU將同一個動態鏈接庫的不同邏輯地址映射入同一個物理地址中,這樣就解決了在內存中多個拷貝的問題。

同時,由于是在執行時才裝載,因此,就解決了升級不便的問題。

可以在哪看到這系列文章

我在我的GitHub上,知乎專欄上和CSDN上同步更新。

上一篇文章:macOS上的匯編入門(十二)——調試

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/194366.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息