-
第一講:計(jì)算機(jī)的組織架構(gòu)和匯編語言介紹
- 匯編語言
-
計(jì)算機(jī)組織架構(gòu)
- 數(shù)字電路
- 術(shù)語回顧
- 數(shù)制
-
數(shù)字電路
- 硬件電路
- 數(shù)字電路的問題
-
匯編語言的開始
- 程序的節(jié)(sections)
- 調(diào)用操作系統(tǒng)的系統(tǒng)調(diào)用
- 列出文件(Listing files)
- 匯編和鏈接
- 調(diào)試匯編程序
- 反匯編現(xiàn)有的程序
-
附錄
- 課程資源
第一講:計(jì)算機(jī)的組織架構(gòu)和匯編語言介紹
匯編語言
匯編語言和C/C++語言的區(qū)別是什么?
-
匯編語言是底層語言,更接近于CPU本身可以理解的內(nèi)容。CPU可以理解的是純粹的字節(jié)流(機(jī)器語言)。幾乎不會(huì)有人愿意通過寫原始的字節(jié)流進(jìn)行編程。匯編語言處于機(jī)器語言的上層, 將CPU可以理解的操作碼(Opcode)抽象成了人類可以理解的命令, 例如
add
,mov
等等。這些名字被稱之為助記符。 -
和C/C++語言相比,匯編語言的工具較少。沒有所謂的"標(biāo)準(zhǔn)匯編語言庫"。如果你想要寫一個(gè)字符串處理的方法,你只能自己編寫。
-
匯編語言不能移植到其他類型的CPU上(x86 vs ARM)或者其他類型的操作系統(tǒng)上(Windows vs Linux), 甚至不能和其它類型的匯編語言(YASM vs MASM vs GAS)都無法兼容。
一般來說, 我們編譯一個(gè)C/C++ 程序的流程如下所示:
compile link
C/C++ source code --> object code --> executable
其中object code是指指令流, 這些指令將會(huì)被CPU直接運(yùn)行。C/C++中的一條語句可能會(huì)編譯成許多指令。即使是像下面這樣簡單的語句:
x = y;
上面的語句可能需要在CPU級(jí)別執(zhí)行大量的工作, 這取決于x和y在內(nèi)存中的位置,它們是否有相同的類型等等。
因此,我們使用高級(jí)語言(C/C++)編寫的"指令"的數(shù)量和CPU實(shí)際執(zhí)行的指令的數(shù)量上存在很大的差異。
但是匯編語言所編寫的指令和cpu實(shí)際執(zhí)行的指令是一一對(duì)應(yīng)的。匯編語言程序中的每一行代碼都保證翻譯成單個(gè) CPU 指令。一方面,這意味著匯編語言可以讓我們很好的掌握CPU正在執(zhí)行的工作, 另一方面,我們?cè)贑/C++語言中很方便實(shí)現(xiàn)的特性實(shí)際上在CPU層面都不存在。實(shí)際上,在CPU層面上并沒有所謂的for循環(huán),if-else條件分支,變量聲明等等。我們必須通過組合一些原始的操作來實(shí)現(xiàn)這些高級(jí)語言的特性。
由于匯編指令和CPU指令是一一對(duì)應(yīng)的,因此每種類型的CPU都有自己對(duì)應(yīng)的匯編語言。Intel CPU的匯編語言和ARM CPU(大多數(shù)智能手機(jī)使用ARM CPU)的匯編語言是完全不同的。并且與Arduino上使用的AVR CPU完全不同。與C/C++不同的是,匯編語言是無法做到移植性的。
即便我們使用同一種類型的CPU,也不能保證匯編器與操作系統(tǒng)之間的可移植性。與C/C++不同的是, C/C++由國際委員會(huì)決定C/C++的標(biāo)準(zhǔn),然而匯編語言卻不是這樣。因此,按照YASM(匯編器)寫出來的匯編代碼可能無法在GAS/NASM或者微軟的匯編器上進(jìn)行匯編。操作系統(tǒng)層面也會(huì)導(dǎo)致這樣的不兼容性,因?yàn)闆]有"標(biāo)準(zhǔn)匯編庫"。在一種操作系統(tǒng)下寫出的匯編程序可能無法移植到其他操作系統(tǒng)下。Windows下使用匯編語言寫出的程序移植到Linux下可能不能運(yùn)行,不僅僅是因?yàn)閰R編器不同,操作系統(tǒng)系統(tǒng)的接口不同也是一個(gè)重要原因。(Windows和Linux對(duì)于系統(tǒng)調(diào)用的定義不同)。
本課程使用的是 YASM 匯編器, 基于64位的Intel CPU(X86-64),在Linux系統(tǒng)下運(yùn)行。
我們會(huì)使用GDB調(diào)試器去調(diào)試你的匯編程序。在C++中,你最初用于調(diào)試程序的工具可能是在出錯(cuò)的位置附近添加cout,但是將打印添加到匯編程序中就可能需要重寫你需要打印的函數(shù),甚至重新所有的程序。顯示通過打印的方式調(diào)試程序在匯編語言的debug中是不可行的。所以我們?cè)谡n程中也會(huì)熟悉GDB工具。
計(jì)算機(jī)組織架構(gòu)
計(jì)算機(jī)組織架構(gòu)是指計(jì)算機(jī)的內(nèi)部結(jié)構(gòu)。內(nèi)存、CPU、I/O設(shè)備等如何連接在一起,如何配合起來工作。雖然我們主要關(guān)注我們實(shí)際使用的計(jì)算機(jī)的組織架構(gòu)(x86-64),但是有時(shí)我們也會(huì)去和其他的計(jì)算機(jī)系統(tǒng)進(jìn)行比較(MIPS, ARM等等)。當(dāng)然,記住這些不同的系統(tǒng)的區(qū)別也很重要。
數(shù)字電路
CPU通過數(shù)字電路來實(shí)現(xiàn),數(shù)字電路由邏輯門電路組成。這是比匯編語言更加底層的內(nèi)容。我們會(huì)稍微了解一下數(shù)字電路,僅僅是為了感受CPU是如何進(jìn)行工作的,但是課程的側(cè)重點(diǎn)還是在匯編語言上。
術(shù)語回顧
字節(jié)(Byte):可以單獨(dú)尋址的計(jì)算機(jī)內(nèi)存的最小單位。對(duì)于我們來說,一個(gè)Byte等于8個(gè)bit。但是需要了解的是并不是所有的系統(tǒng)都是這樣的。有一些奇怪的系統(tǒng),一個(gè)byte是10個(gè)bit或者7個(gè)bit。
每個(gè)byte中的每個(gè)bit的位置從右到左編號(hào)為0到7:
Bit value 0 0 1 0 1 1 0 1
Bit pos. 7 6 5 4 3 2 1 0
字(word): 兩個(gè)字節(jié)(16 bits)。
將一個(gè)字視作2個(gè)字節(jié)時(shí),我們將第一個(gè)字節(jié)(占據(jù)低8位的字節(jié))稱之為"低字節(jié)", 將第二個(gè)字節(jié)稱之為"高字節(jié)"。
類似的,如果我們對(duì)一個(gè)字節(jié)中的bit的位置進(jìn)行編號(hào),則低字節(jié)的bit將編號(hào)為0-7,而高字節(jié)中的bit將編號(hào)為8-15。
這個(gè)規(guī)則可以推廣到雙字(dword)的低位字和高位字、四字(qword)的低位和高位雙字等等。類似的,在一個(gè)字節(jié)中,比特0代表低位, 比特7代表高位。
雙字(double-words/dword): 4個(gè)bytes(32個(gè)bits)
四字(Quad-word/qword): 8個(gè)bytes(64個(gè)bits)。(這個(gè)quad可以用quadra kill四殺來輔助記憶)
依次類推還有,雙四字(double-quad-words),16bytes, 四四字(“quad-quad-words”), 32 bytes, 等等。但是這些很少見,不常用。
KB:kilo-bytes(千字節(jié)), 這里的"kilo"指的是二進(jìn)制的千, 2 10 = 1024 {2}^{10} = 1024 210=1024字節(jié)。K后面跟著的大寫的B代表我們的單位是字節(jié),如果是小寫的b則代表是比特。
Kb:kilo-bit(千比特)。這個(gè)單位通常使用的不太多,在通訊領(lǐng)域中使用很多。例如帶寬通常以兆比特為單位進(jìn)行測(cè)量。
MB:Mega-byte(兆字節(jié)), 2 20 = 1024 2 = 1048576 {2}^{20} = {1024}^{2} = 1048576 220=10242=1048576字節(jié)。這個(gè)數(shù)量級(jí)大約是100萬字節(jié)。
GB: Gigabytes(千兆字節(jié)), 2 30 = 1024 3 = 1073741824 {2}^{30} = {1024}^{3} = 1073741824 230=10243=1073741824字節(jié)。這個(gè)數(shù)量級(jí)大約是10億字節(jié)。
以此類推還有TB、PB等等。
二進(jìn)制的百萬(million 1048576)和十進(jìn)制的百萬(1000000)的區(qū)別就解釋了磁盤標(biāo)簽上的容量和系統(tǒng)中實(shí)際顯示的容量的區(qū)別。操作系統(tǒng)使用二進(jìn)制的度量方式,而標(biāo)簽上印刷的是十進(jìn)制的度量方式。所以區(qū)別磁盤標(biāo)簽上的500GB在你的操作系統(tǒng)中顯示的容量將會(huì)是下面的數(shù)值:
500,000,000,000
——————————————— = 465 GB
1,073,741,824
數(shù)制
十進(jìn)制(Decimal):十進(jìn)制以10為基數(shù),這是我們經(jīng)常使用的。數(shù)字范圍是0-9。
二進(jìn)制(Binary): 二進(jìn)制的數(shù)字范圍是0-1。
八進(jìn)制(Octal):八進(jìn)制的數(shù)字范圍是0-7。(通常八進(jìn)制的使用相對(duì)較少)
十六進(jìn)制(Hexadecimal):十六進(jìn)制的數(shù)字范圍是0-9,a(10), b(11), c(12), d(13), e(14), f(15)。
接下來我們將回顧二進(jìn)制和十六進(jìn)制算術(shù)。
注意,這些數(shù)制,沒有那種一定比其他的類型更好或更正確。
21 == 10101b == 0x15 == 025
decimal binary hex. octal
在計(jì)算機(jī)系統(tǒng)的內(nèi)部,計(jì)算機(jī)使用二進(jìn)制存儲(chǔ)內(nèi)容。但是這個(gè)通常對(duì)于上層語言(包括匯編語言)而言,并不感知。 我們可以很容易的加減二進(jìn)制的數(shù)字或者其他進(jìn)制的數(shù)字。所以大多數(shù)時(shí)候,計(jì)算機(jī)底層使用二進(jìn)制并不會(huì)太影響我們編程。
C/C++ 和匯編都允許我們?cè)谏鲜鋈魏螖?shù)字系統(tǒng)的源代碼中編寫數(shù)字,只需使用不同的格式:
符號(hào) | 數(shù)制 |
---|---|
21 | 十進(jìn)制 |
10101b | 二進(jìn)制,以b結(jié)尾 |
0x15 | 十六進(jìn)制,以0x開頭 |
025 | 八進(jìn)制,以0開頭 |
注意,b、0x等不是數(shù)字本身的一部分,它們僅僅用來區(qū)分不同的進(jìn)制。編譯器/匯編器負(fù)責(zé)將對(duì)應(yīng)的數(shù)字轉(zhuǎn)換為計(jì)算機(jī)使用的內(nèi)部格式。
例如,在下面的例子中,你可以這樣做:
int x = 21;
if(x == 0x15) {
?
}
上面的if語句中的語句總是為true。
類似地,當(dāng)我們打印一個(gè)數(shù)字(通過 cout 或 printf)時(shí),它通常打印為十進(jìn)制,但通過各種標(biāo)志我們可以要求十六進(jìn)制。運(yùn)行時(shí)庫負(fù)責(zé)將內(nèi)部表示形式轉(zhuǎn)換回十進(jìn)制/十六進(jìn)制。在本學(xué)期晚些時(shí)候,我們將有一個(gè)手動(dòng)打印數(shù)字的作業(yè)(因?yàn)閰R編語言沒有標(biāo)準(zhǔn)庫來為我們做這件事?。?/p>
數(shù)字電路
CPU 由一組復(fù)雜的數(shù)字電路實(shí)現(xiàn)。數(shù)字電路是由邏輯門構(gòu)建的(邏輯門又是使用晶體管構(gòu)建的)。在數(shù)字電路設(shè)計(jì)中,在數(shù)字電路設(shè)計(jì)中,我們展示邏輯信號(hào)(開/關(guān)值)如何從輸入流經(jīng)邏輯門到輸出。如果有電流流過邏輯信號(hào),則邏輯信號(hào)為高(開);如果沒有電流(或電流非常?。?,則邏輯信號(hào)為低(關(guān))。
邏輯門的基本類型有:
-
非門(NOT):單輸入、單輸出門,反轉(zhuǎn)其輸入。如果輸入為高電平,則輸出為低電平,反之亦然。
非(NOT) 在 C/C++ 中運(yùn)算符是
~
。這個(gè)符號(hào)是按位非,與邏輯非(!
)不同。 -
與門(AND):雙輸入、單輸出門:當(dāng)且僅當(dāng)兩個(gè)輸入均為高電平時(shí),輸出為高電平,否則為低電平。
AND 的 C/C++ 運(yùn)算符是 &(這是按位與,與 && 邏輯與 不同)。
-
或門(OR):雙輸入、單輸出門:如果其中一個(gè)或兩個(gè)輸入都為高電平,則輸出為高電平,否則(如果兩個(gè)輸入均為低電平)輸出為低電平。
C/C++ 中與運(yùn)算符是 | (這又是按位或,不同于邏輯或 ||)
-
異或門(XOR):雙輸入,單輸出門。如果其中一個(gè)輸入為高電平但不是兩個(gè)輸入都是高電平,則輸出為高電平。否則,當(dāng)兩個(gè)輸入都為高電平或者兩個(gè)輸入都是低電平,則輸出為低電平。實(shí)際上,如果輸入不同(一高一低),則輸出為高,如果輸入相同,則輸出為低。
C/C++中代表異或的運(yùn)算符是
^
(這個(gè)是按位異或, 沒有邏輯上的異或)。 注意^
不是求冪運(yùn)算符,C/C++中沒有求冪的運(yùn)算符。 -
與非門(NAND):輸出端帶有非門的與門。也就是說,如果兩個(gè)輸入都為高電平,則輸出為低電平,否則為高電平。
C/C++沒有直接的與非運(yùn)算符??梢允褂?code>&和
~
組合起來起到相同的效果。 -
或非(NOR):在或門的輸出端帶有一個(gè)非門。如果兩個(gè)輸入均為低電平,則輸出為高電平,否則為低電平。
C/C++沒有直接的或非運(yùn)算符??梢允褂?code>|和
~
組合起來起到相同的效果。 -
同或(XNOR):輸出端帶有非門的異或門。如果兩個(gè)輸入相同(均為低電平或均為高電平),則輸出為高電平,否則為低電平。
C/C++沒有直接的同或運(yùn)算符。可以使用
^
和~
組合起來起到相同的效果。
你可能會(huì)熟悉前三種邏輯門。有幾點(diǎn)需要注意:
- 與門(AND)和或門(OR)可以擴(kuò)展為超過2個(gè)輸入端,n輸入的與門,當(dāng)它的所有的n個(gè)輸入端都是高電平時(shí),則該與門輸出高電平,否則為低電平。同樣,一個(gè)n輸入端的或門,只要有一個(gè)輸入端是高電平,則該或門將輸出高電平。如果所有的輸入都是低電平,則該或門輸出低電平。
下圖說明了如何構(gòu)建 3 輸入與門:
問題:如果異或門以相同的配置排列,所得的 3 輸入、1 輸出電路會(huì)起什么作用?
-
與非門和或非門具有通用性:所有其他門都可以僅由 NAND 或 NOR 構(gòu)建。事實(shí)上,為了簡化制造,僅使用 NAND 門構(gòu)建電路是很常見的。
例如,下面是一個(gè)相當(dāng)于僅使用 NAND 門實(shí)現(xiàn)的 A OR B 的電路(您應(yīng)該驗(yàn)證該電路是否為輸入 A 和 B 的所有四種組合生成正確的輸出)
您可以在維基百科上找到有關(guān)如何將所有其他類型的邏輯門轉(zhuǎn)換為 NAND 和 NOR 門的完整參考。作業(yè) 1 將要求您將使用 NOT、AND 和 OR 的電路轉(zhuǎn)換為僅使用 NAND 門的電路。
電路真值表:
任何(無狀態(tài))m 輸入、n 輸出電路的行為也可以使用表格來說明,該表格顯示每個(gè)輸入組合如何映射到特定的輸出集。因?yàn)槊總€(gè)輸入可以是低 (0) 或高 (1),所以該表將有 2m 行和 m + n 列。例如,上面顯示的 3 輸入 AND:
Input | Output | ||
---|---|---|---|
A | B | C | Q |
0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 |
0 | 1 | 0 | 0 |
1 | 1 | 0 | 0 |
0 | 0 | 1 | 0 |
1 | 0 | 1 | 0 |
0 | 1 | 1 | 0 |
1 | 1 | 1 | 1 |
從表格中可以知道,僅當(dāng)所有三個(gè)輸入均為高電平 (1) 時(shí),輸出才為高電平 (1)。
硬件電路
如果您嘗試在實(shí)際電子硬件中實(shí)現(xiàn)邏輯電路,您會(huì)遇到上面未提及的幾個(gè)問題:
為了解決這個(gè)不可預(yù)測(cè)的時(shí)期,大多數(shù)數(shù)字電路都是同步的:他們使用時(shí)鐘來控制何時(shí)執(zhí)行計(jì)算。時(shí)鐘是一個(gè) 0 輸入、1 輸出的邏輯器件,它輸出一個(gè)信號(hào),該信號(hào)以規(guī)則的時(shí)鐘速率交替出現(xiàn)低、高、低、高……
通常,當(dāng)時(shí)鐘信號(hào)從低電平變?yōu)楦唠娖剑〞r(shí)鐘信號(hào)的“上升沿”)時(shí),電路的其余部分將執(zhí)行其計(jì)算,但直到下一個(gè)時(shí)鐘周期的上升沿才會(huì)讀取計(jì)算的輸出。
- 電流不僅僅從 A 點(diǎn)流到 B 點(diǎn)(如邏輯圖所示),而且僅在存在閉合電路時(shí)才流動(dòng)。為了使得電路在實(shí)際中能夠工作,必須提供電路的最終輸出返回到輸入電源之間的連接。在真實(shí)的電路中,這些連接當(dāng)然會(huì)存在,但在邏輯圖中,我們忽略它們,因?yàn)樗鼈儾粫?huì)影響電路的邏輯及其實(shí)際計(jì)算的內(nèi)容。
許多門電路都需要電源連接(始終為高電平的輸入)為其供電,這會(huì)使得現(xiàn)實(shí)中的電路更加復(fù)雜。 - 如果你想嘗試只購買一個(gè)或門, 你會(huì)發(fā)現(xiàn)你無法僅僅買一個(gè)或門。門電路通常是在集成電路上使用,通常會(huì)將多個(gè)相同類型的門電路綁定在一起。例如,您可以購買在單個(gè)芯片上具有四個(gè)、八個(gè)或更多 NAND 門的 IC(集成電路)。這是有道理的,因?yàn)樵趯?shí)際的電路設(shè)計(jì)中,您很少只需要一個(gè)門。(該芯片將具有一個(gè)由所有柵極共享的單電源輸入)。
- 理想情況下,我們將邏輯電路描述為信號(hào)瞬間從低電平切換到高電平,反之亦然,但在現(xiàn)實(shí)系統(tǒng)中這是不可能的。電路的上升時(shí)間是線路從低電平變?yōu)楦唠娖剿璧臅r(shí)間。在此過渡期間,流過連接的電流量介于 0 和 1 之間,這可能會(huì)導(dǎo)致電路輸出在短時(shí)間內(nèi)不可預(yù)測(cè)。
- 為了解決這個(gè)不可預(yù)測(cè)的時(shí)期,大多數(shù)數(shù)字電路都是同步的:他們使用時(shí)鐘來控制何時(shí)執(zhí)行計(jì)算。時(shí)鐘是一個(gè) 0 輸入、1 輸出的邏輯器件,它輸出一個(gè)信號(hào),該信號(hào)以規(guī)則的時(shí)鐘速率交替出現(xiàn)低、高、低、高……通常,當(dāng)時(shí)鐘信號(hào)從低電平變?yōu)楦唠娖剑〞r(shí)鐘信號(hào)的“上升沿”)時(shí),電路的其余部分將執(zhí)行其計(jì)算,但直到下一個(gè)時(shí)鐘周期的上升沿才會(huì)讀取計(jì)算的輸出。因此,輸出有 1 個(gè)完整時(shí)鐘周期來穩(wěn)定在正確的值。
事實(shí)上,即使信號(hào)很高,它仍然不會(huì)處于恒定水平;它只是高于某個(gè)標(biāo)記“低”和“高”之間分界線的閾值。 - 在電氣方面,單個(gè)輸出無法連接到無限數(shù)量的其他設(shè)備;輸出的“扇出”是有限制的。
- 邏輯門可以通過多種不同的方式以電子方式實(shí)現(xiàn),從而產(chǎn)生不同的邏輯系列,每個(gè)都有自己的電氣特性。例如,對(duì)于不同的系列,“低”與“高”的電壓水平可能非常不同。另請(qǐng)注意,在大多數(shù)系列中,“低”電平不是 0V,而是低于“高”電平的某些電壓電平。例如,晶體管-晶體管-邏輯 (TTL) 系列使用 0 至 0.8V(相對(duì)于地)之間的低電壓電平,以及 2 至 5V 的高電壓電平。 0.8 至 2V 之間的輸入信號(hào)處于“不可預(yù)測(cè)”范圍內(nèi),可能會(huì)被視為高或低,甚至在兩者之間波動(dòng)。
數(shù)字電路的問題
您可以嘗試構(gòu)建以下一些電路,以測(cè)試您對(duì)邏輯電路的理解:
- 使用你喜歡的任何邏輯門器件,去構(gòu)建一個(gè)4輸入 1輸出的電路, 當(dāng)且僅當(dāng)其中一個(gè)輸入為高電平時(shí)才輸出高電平。
- 使用你喜歡的任何邏輯門器件,去構(gòu)建一個(gè)4輸入 1輸出的電路, 當(dāng)且僅當(dāng)有兩個(gè)輸入為高電平時(shí)才輸出高電平。
- 僅使用 NAND,構(gòu)建一個(gè)比較器電路,一個(gè) 2 輸入、1 輸出電路,如果滿足以下條件,則輸出為高電平:
- 兩個(gè)輸入都是低電平
- 第一個(gè)輸入為高電平,第二個(gè)輸入是低電平
- 兩個(gè)輸入都是高電平
這等價(jià)于檢查是否第一個(gè)輸入小于等于第二個(gè)輸入。
這些問題有許多不同的可能解決方案。數(shù)字電路的進(jìn)階課程將教授優(yōu)化電路設(shè)計(jì)的方法,以便最大限度地減少所使用的門的數(shù)量。
匯編語言的開始
這里我們將使用匯編語言去編寫一個(gè)經(jīng)典的程序: Hello World程序。我們可以使用兩種廣泛的風(fēng)格來編寫匯編(.asm 程序)。
-
我們可以通過調(diào)用操作系統(tǒng)的系統(tǒng)調(diào)用來與操作系統(tǒng)交互。由于缺少更好的名稱,我們稱之為系統(tǒng)調(diào)用風(fēng)格(syscall-style)。這是最直接的方法,但是操作起來不太方便。如果我們使用這種方式,那么我們的匯編程序的入口程序就是
_start
,我們首先使用系統(tǒng)調(diào)用向標(biāo)準(zhǔn)輸出打印一個(gè)字符串,使用另外一個(gè)系統(tǒng)調(diào)用退出。如果我們使用系統(tǒng)調(diào)用風(fēng)格,我們的程序?qū)⑹峭耆?dú)立的:除了我們編寫的內(nèi)容之外,生成的可執(zhí)行文件中不會(huì)有任何內(nèi)容。
-
我們可以使用標(biāo)準(zhǔn)c庫中的方法例如
printf
和exit
。這稱之為"C庫風(fēng)格"。這就需要我們自己去鏈接c語言庫。這個(gè)方法顯然要強(qiáng)大得多,因?yàn)樗鼘標(biāo)準(zhǔn)庫中的所有資源都給了我們的程序。如果我們使用C庫的風(fēng)格,那么最終生成的可執(zhí)行文件將不僅包括我們編寫的代碼,還包括標(biāo)準(zhǔn)庫添加的很多的代碼。
下面我們先使用第一種風(fēng)格(系統(tǒng)調(diào)用風(fēng)格)編寫程序,這個(gè)方式上手更快一些。
;;;
;;; hello.s
;;; Prints "Hello, world!"
;;;
section .data
msg: db "Hello, world!", 10
MSGLEN: equ $-msg
section .text
;; Program code goes here
global _start
_start:
mov rax, 1 ; Syscall code in rax
mov rdi, 1 ; 1st arg, file desc. to write to
mov rsi, msg ; 2nd arg, addr. of message
mov rdx, MSGLEN ; 3rd arg, num. of chars to print
syscall
;; Terminate process
mov rax, 60 ; Syscall code in rax
mov rdi, 0 ; First parameter in rdi
syscall ; End process
可以使用下面的命令進(jìn)行匯編和鏈接:
asm hello.s
也可以手動(dòng)進(jìn)行:
yasm -g dwarf2 -f elf64 hello.s -l hello.lst
ld -g -o hello hello.o
然后執(zhí)行像下面這樣執(zhí)行:
./hello
將打印出下面這樣的內(nèi)容:
Hello, world!
打印后會(huì)退出。
一步一步分解該程序,每行均包含以下形式:
label: instruction ; comment
所有這些內(nèi)容都是可選的,因此只有幾行是以label開頭, 并且很多行沒有注釋。 Label后面的冒號(hào)(:)也是可選的,但是為了程序的清晰,最好寫上。
行 | 解釋 |
---|---|
section .data |
data 節(jié),包含初始化的常量和變量 |
msg: db “Hello, world!”, 10 |
msg 定義了一個(gè)指向"Hello,world!“字符串的標(biāo)簽,它將被逐字復(fù)制到我們的匯編程序中。db 代表"define byte”,即定義字節(jié),最后的10 ,在ascii表中代表LF(\n)。注意用匯編的 db 偽指令定義字符串,不會(huì)自動(dòng)添加"\0",這個(gè)要和C/C++相區(qū)別 |
MSGLEN: equ $-msg |
equ 定義了一個(gè)常量叫做MSGLEN , 這個(gè)常量代表的是msg的長度, $ 代表當(dāng)前的位置 |
section .text |
text 節(jié)定義了程序的實(shí)際執(zhí)行的代碼 |
global _start | 我們將_start 標(biāo)簽聲明為全局,以便在程序之外可見(以便操作系統(tǒng)可以找到它可以啟動(dòng)我們的程序) |
_start | 這將 _start 聲明為指向程序中當(dāng)前位置的標(biāo)簽 |
mov rax, 1 | 這會(huì)將值 1 加載到寄存器 rax 中,該寄存器存儲(chǔ)系統(tǒng)調(diào)用代碼。 1 是"寫入文件"的系統(tǒng)調(diào)用代碼 |
mov rdi, 1 | 將 1 存儲(chǔ)到寄存器 rdi 中。這是系統(tǒng)調(diào)用 write 的第一個(gè)參數(shù),它是文件描述符(1 是標(biāo)準(zhǔn)輸出) |
mov rsi, msg | 將 msg 、地址存儲(chǔ)到 rsi 中, 這是第二個(gè)參數(shù),表示要寫的消息 |
mov rdx, MSGLEN | 將 MSGLEN 存儲(chǔ)到 rdx 中。這是第三個(gè)參數(shù),即要寫入的長度(以字節(jié)為單位) |
syscall | 調(diào)用rax 中存儲(chǔ)的值所代表的系統(tǒng)調(diào)用,打印字符 |
mov rax, 60 | 60 是"退出進(jìn)程"的系統(tǒng)調(diào)用代碼 |
mov rdi, 0 | 第一個(gè)參數(shù),0,退出代碼(成功) |
syscall | 執(zhí)行系統(tǒng)調(diào)用 |
注意: 對(duì)于匯編語言而言,默認(rèn)的后綴是.s
。
對(duì)于Intel 語法而言, mov指令的結(jié)構(gòu)如下所示:
mov dest, src
即mov后先跟著目的對(duì)象,再接著是源對(duì)象。將其理解為dest = src
是可以的。
程序的節(jié)(sections)
內(nèi)存中正在運(yùn)行的程序,其內(nèi)存空間分為許多不同的"部分"。盡管所有部分都是同一地址空間的一部分,但它們?cè)诟拍钌嫌糜诓煌挠猛荆⑶也僮飨到y(tǒng)可能對(duì)其應(yīng)用不同的權(quán)限。例如,操作系統(tǒng)通常將.text
部分(可執(zhí)行機(jī)器代碼所在的位置)設(shè)置為只讀,因?yàn)樽孕薷拇a(通常)要么是錯(cuò)誤,要么是漏洞利用。
一個(gè)進(jìn)程的內(nèi)存布局通常如下所示:
--------------------
Stack (grows down)
…
Heap (grows up)
---------------------
.data section (global variables)
---------------------
.text section
---------------------
棧向下增長這一點(diǎn)很重要, 這代表壓棧操作會(huì)使得棧頂指針減少。
除了用于存放全局變量的.data
節(jié)之外,還有一塊是.bss
節(jié),其用于存放未經(jīng)過初始化的全局?jǐn)?shù)據(jù)。.data
和.bss
的區(qū)別在于當(dāng)程序運(yùn)行時(shí),操作系統(tǒng)會(huì)將.data
中的數(shù)據(jù)從磁盤中拷貝到內(nèi)存中。而.bss
節(jié)中由于存放的是未經(jīng)過初始化的數(shù)據(jù),因此操作系統(tǒng)不需要復(fù)制任何內(nèi)容,只需要預(yù)留好對(duì)應(yīng)的空間即可。(還有一些其他類型的節(jié),不過我們目前不會(huì)用到,例如.readonly
)
通過定義更多常量,可以使上面的程序更容易閱讀,例如:
section .data
SYS_write equ 1
SYS_stdout equ 1
SYS_exit equ 60
EXIT_SUCCESS equ 0
equ
定義了一個(gè)匯編時(shí)的常量,當(dāng)程序運(yùn)行的時(shí)候,這些常量不會(huì)在內(nèi)存中占據(jù)任何的空間。 這有點(diǎn)類似與C/C++
語言中的#define
。
使用它們時(shí),我們只需通過名稱來引用它們:
mov rax, SYS_exit
mov rdi, EXIT_SUCCESS
syscall
db將字節(jié)序列直接存儲(chǔ)到可執(zhí)行文件中, 例如下面的代碼:
msg db "Hello, world!", 10
其實(shí)際上做了兩件事情:
- "Hello,world!"連同后面的10一同被寫入了可執(zhí)行文件中。
-
msg
定義了一個(gè)標(biāo)簽,該標(biāo)簽指向了字節(jié)序的開始的地址。注意我們并不是將字符串的存入了msg
中,而是將字符串的地址存入了msg中。
因?yàn)槲覀兪褂孟到y(tǒng)調(diào)用風(fēng)格,所以我們的字符串不以終止符 NULL (\0) 字符結(jié)尾。(上面的字符串以 10 結(jié)尾,即換行的 ASCII 字符;這就是在 C/C++ 中使用 \n 字符轉(zhuǎn)義時(shí)得到的結(jié)果。) 我們必須知道要傳遞給 SYS_write 系統(tǒng)調(diào)用的字符串的長度。我們可以簡單地手動(dòng)計(jì)算字節(jié)數(shù),但如果我們更改字符串的內(nèi)容,就需要重新計(jì)算字符串的長度。
如前所述,匯編器將字符串 msg
放入生成的可執(zhí)行文件中的某個(gè)地址。事實(shí)上,我們的匯編源文件中的所有內(nèi)容都有一些地址,它將最終出現(xiàn)在生成的可執(zhí)行文件中。即使像 MSGLEN
這樣理論上占用 0 空間的東西也有輸出文件中"當(dāng)前位置"的一些概念。$
獲取當(dāng)前位置的地址。$-msg
從當(dāng)前地址減去地址msg
,得到 msg 指向的字符串的長度。需要注意的是,這只在定義 msg
之后立即定義了 MSGLEN
才會(huì)有效;如果中間有任何其他定義占用了文件中的空間,則計(jì)算出的長度將是錯(cuò)誤的。
(這也表明 equ
定義可以在其值中使用有限的算術(shù);計(jì)算是在匯編時(shí)完成的,而不是在運(yùn)行時(shí)完成的。)
在我們所有的程序中,我們首先是 .data
部分,然后是 .text
部分,但這只是一個(gè)約定。您可以更改各部分的順序,甚至可以將它們交錯(cuò)排列,您的程序仍然可以運(yùn)行。
調(diào)用操作系統(tǒng)的系統(tǒng)調(diào)用
調(diào)用系統(tǒng)調(diào)用的過程如下:
- 將
rax
設(shè)置為要執(zhí)行的系統(tǒng)調(diào)用的編號(hào)。例如SYS_exit的系統(tǒng)調(diào)用編號(hào)為60, 而SYS_write的系統(tǒng)調(diào)用編號(hào)為1。你可以在這里找到所有系統(tǒng)調(diào)用的編號(hào)。 - 將
rdi
,rsi
,rdx
,r10
,r8
,r9
設(shè)置為系統(tǒng)調(diào)用函數(shù)的第一個(gè)、第二個(gè)、第三個(gè)參數(shù)。往后以此類推。 - 執(zhí)行系統(tǒng)調(diào)用```syscall``指令。
請(qǐng)注意,步驟 (1) 和 (2) 可以按任何順序發(fā)生,但在執(zhí)行系統(tǒng)調(diào)用之前必須正確設(shè)置所有寄存器值。如果系統(tǒng)調(diào)用返回一個(gè)值(SYS_write
和 SYS_exit
都沒有),則系統(tǒng)調(diào)用返回后該值將位于 rax
中。
列出文件(Listing files)
yasm
命令中的 -l noop.lst
參數(shù)是可選的;它指示 YASM 生成一個(gè)列表文件,這是我們逐行編寫的匯編指令及其十六進(jìn)制操作碼的列表。以下是上述程序的列表文件:
1 %line 1+1 hello_bare.s
2
3
4
5
6
7 [section .data]
8
9 00000000 48656C6C6F2C20776F- msg db "Hello, world!", 10
10 00000000 726C64210A
11 MSGLEN equ $-msg
12
13 [section .text]
14
15
16
17 [global _start]
18 _start:
19
20 00000000 48C7C001000000 mov rax, 1
21 00000007 48C7C701000000 mov rdi, 1
22 0000000E 48C7C6[00000000] mov rsi, msg
23 00000015 48BA0E000000000000- mov rdx, MSGLEN
24 00000015 00
25 0000001F 0F05 syscall
26
27
28 00000021 48C7C03C000000 mov rax, 60
29 00000028 48C7C700000000 mov rdi, 0
30 0000002F 0F05 syscall
第一列是原始行號(hào),第二列是匯編程序中相對(duì)于當(dāng)前節(jié)的地址(從 00000000 開始),第三列是操作碼,第四列是我們的原始程序。
從這里看, mov rax, 60
的操作碼是48C7C03C000000
, mov rdi, 0
操作碼是48C7C700000000
, syscall的操作碼是0F05
。(x86-64 使用不同的指令寬度:并非所有操作碼的字節(jié)數(shù)都相同;有些較短,有些較長)
匯編和鏈接
asm
腳本負(fù)責(zé)在所有輸入文件上運(yùn)行匯編程序,然后將它們鏈接在一起。 它還能正確檢測(cè)您是否將 _start
或 main
定義為程序的入口點(diǎn),并在后一種情況下與C 標(biāo)準(zhǔn)庫鏈接。
如果你想進(jìn)行手動(dòng)匯編,則需要執(zhí)行的命令如下所示:
yasm -g dwarf2 -f elf64 filename.s -l filename.lst
- -g 參數(shù)給出了調(diào)試信息使用的格式,以便 GDB(參見下一節(jié))可以讀取它。
- -f 參數(shù)表示輸出 x86-64 格式的目標(biāo)文件。
- -l 參數(shù)表示輸出列表文件。
要將一個(gè)(或多個(gè))匯編的目標(biāo)文件鏈接到一起成為可執(zhí)行文件,有兩種選擇:
-
如果您沒有使用任何 C 標(biāo)準(zhǔn)庫函數(shù),并且程序的入口點(diǎn)名為 _start,則使用 ld:
ld -g -o exe_name object.o files.o ...
-
如果您使用的是 C 標(biāo)準(zhǔn)庫中的函數(shù),并且入口點(diǎn)名為 main,則使用 gcc:
gcc -o exe_name object.o files.o ...
這與用于鏈接 C 程序的目標(biāo)文件的命令行相同。 (事實(shí)上??,它可以用來鏈接由 C 和匯編語言混合組成的程序?。?/p>
(asm 腳本檢查是否有任何文件定義了 main;如果定義了 main,則假定您要使用 C 標(biāo)準(zhǔn)庫函數(shù)。)
調(diào)試匯編程序
GDB可以識(shí)別匯編語言,我們可以通過以下方式在 GDB 中運(yùn)行我們的程序
gdb ./hello
我們可以通過以下方式在程序的_start
處打上斷點(diǎn):
break _start
run
然后使用 n
(next) 命令逐行執(zhí)行程序。寄存器的值可以按名稱打印,前綴為 $
,例如
print $rax
或更改它們:
set $rax = 0
您還可以使用info registers
一次打印所有寄存器。
請(qǐng)注意,當(dāng)我使用 GDB 時(shí),我使用一個(gè)名為GDB dashboard
的插件,它顯示每一步的寄存器內(nèi)容。
GDB 將默認(rèn)使用 AT&T
語法進(jìn)行匯編。您可以通過輸入命令將其切換為 Intel語法
。
set disassembly-flavor intel
您可以將此命令放入 ~/.gdbinit
中,以便所有GDB會(huì)話都會(huì)有這樣的設(shè)置。
反匯編現(xiàn)有的程序
您可以使用 objdump
反匯編已編譯的可執(zhí)行文件。這對(duì)你弄清楚C/C++程序在匯編層面如何執(zhí)行非常有用。當(dāng)然,結(jié)果通常并不像您想象的那么有用, 因?yàn)榫幾g器可能對(duì)您的代碼做了一些"有趣"的事情。編譯器的目標(biāo)是使生成的程序集更快,但并不容易理解。
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
使用如下的命令進(jìn)行編譯:
gcc -c hello.c
這里會(huì)產(chǎn)生hello.o
的目標(biāo)文件。我們可以對(duì)其反匯編:
objdump -d -M intel hello.o
輸出如下:
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: bf 00 00 00 00 mov edi,0x0
9: e8 00 00 00 00 call e <main+0xe>
e: b8 00 00 00 00 mov eax,0x0
13: 5d pop rbp
14: c3 ret
這里看不到太多有用的東西。因?yàn)樗蕾嚵撕芏鄻?biāo)準(zhǔn)庫提供的很多方法的實(shí)現(xiàn), 而目前我們還沒有對(duì)它們進(jìn)行鏈接。目前對(duì)于調(diào)用printf
的地方,只是用了占位符進(jìn)行替代。
上面的步驟,我們只是對(duì)目標(biāo)文件進(jìn)行了反匯編,而不是鏈接后的可執(zhí)行文件。
我們可以對(duì)hello.o
鏈接成一個(gè)可執(zhí)行文件,執(zhí)行如下的命令:
gcc -o hello hello.o
然后使用objdump -d -M intel hello
,我們將會(huì)得到更多的匯編內(nèi)容。標(biāo)準(zhǔn)庫在運(yùn)行 main()
之前做了很多設(shè)置,并且可執(zhí)行文件包含所有這些代碼。另一方面,它讓我們看到了最終的 main
是什么樣子的:
0000000000400507 <main>:
400507: 55 push rbp
400508: 48 89 e5 mov rbp,rsp
40050b: bf a4 05 40 00 mov edi,0x4005a4
400510: e8 eb fe ff ff call 400400 <puts@plt>
400515: b8 00 00 00 00 mov eax,0x0
40051a: 5d pop rbp
40051b: c3 ret
40051c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
從連接后的反匯編的代碼中,我們知道:
在這里,空的調(diào)用已經(jīng)被替換為過程調(diào)用的puts(puts@plt
), 這個(gè)過程調(diào)用的內(nèi)部實(shí)現(xiàn)了printf
的功能。
main
是一個(gè)過程;它是從_start
標(biāo)簽(標(biāo)準(zhǔn)庫提供) 調(diào)用的,并且必須在完成后返回,因此它以ret
指令結(jié)束。
前兩條指令push rbp; mov rbp,rsp
也是每個(gè)過程開始的標(biāo)準(zhǔn)寫法,這個(gè)在后續(xù)的章節(jié)中會(huì)學(xué)習(xí)到。
閱讀反匯編的其余部分,你可以研究 _start
過程以及 puts
的定義。
附錄
課程資源
課程原文: https://staffwww.fullcoll.edu/aclifton/cs241/lecture-introduction.html文章來源:http://www.zghlxwxcb.cn/news/detail-845084.html
相關(guān)課程: https://www.cs.uaf.edu/2017/fall/cs301/lecture/09_13_memory.html文章來源地址http://www.zghlxwxcb.cn/news/detail-845084.html
到了這里,關(guān)于匯編語言第一講:計(jì)算機(jī)的組織架構(gòu)和匯編語言介紹的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!