簡體   English   中英

匯編代碼是如何編譯和運行的?

[英]How does assembly code get compiled and run?

我看到了應該在匯編中創建引導扇區的匯編代碼。 代碼是:

jmp $
times 510 - ($ - $$) db 0
db 0x55, 0xaa

一切都很好,但第一行應該創建一個無限循環。 我來自更高級別的抽象編程,所以在我的腦海中這意味着該行將永遠執行(因為它會不斷跳轉到內存中的當前地址),代碼的 rest 是如何執行的? 它是在編譯期間執行的嗎? 此外,通常 x86 匯編代碼包括“開始”部分。 為什么這段代碼有效?

匯編器只是一個工具,它在一個文件中獲取字節並在另一個文件中生成不同的字節。 就像 C 編譯器或文字處理器(文字處理器從用戶那里獲取輸入,將其轉換為文件中的字節和屏幕上的像素)。 等等等等。

匯編語言特定於工具,而不是目標,x86 尤其是不兼容的匯編語言列表的麻煩(這不是英特爾與 at&t 語法的事情,有無數不兼容的英特爾 x86 匯編程序)。

您不編譯匯編語言(好吧,有些工具鏈您使用編譯器進行匯編,可悲的是,有些人故意使用編譯器,以便他們從同一個工具鏈中獲得另一種匯編語言(使用 gcc 而不是 as 給您一個同一目標的不同的、不兼容的匯編語言))你組裝它。 指令只是語言的一部分,指令、如何創建注釋和標簽等也是它的一部分。

較舊的匯編器基本上可以最終得到帶有指令 like.org 的最終二進制文件,這樣您幾乎可以將其用作 linker。 盡管今天的工具鏈往往是編譯器、匯編器和 linker。 您從編譯器和匯編器創建對象(文件),然后將它們鏈接在一起。 Think if a high level language like C where you will have multiple source files (if you choose) and one file may call functions in another, each C file will become its own object, and the linker not only links all the objects together defining their memory 中的物理位置,但還將外部引用鏈接在一起,以便它們可以相互調用函數。 或者訪問全局變量。

(像 NASM這樣的一些現代匯編程序仍然支持制作平面二進制文件,自己填寫符號地址而不是需要 linker。這就是您問題中的 NASM 源通常如何構建到沒有元數據的 512 字節舊版 BIOS MBR 引導扇區中, 用nasm -f bin foo.asm 。但是 GNU 匯編器 GAS 不支持這樣做,而且這個答案只考慮了 GNU 工具鏈是如何完成的。)

所以我會嘗試

jmp $

即使我很確定它與我將要使用的匯編器不兼容(來自 binutils 的 gnu 匯編器)。

as so.s
so.s: Assembler messages:
so.s:1: Error: missing or invalid immediate expression `'

所以與其

jmp .

as so.s -o so.o
objdump -d so.o

so.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
   0:   eb fe                   jmp    0x0

更好的是,我可以構建它並查看匯編程序生成的機器代碼。 匯編器的部分工作是將人類可讀的 jmp 轉換為處理器可以實際執行的字節/位(0xEB,0xFE)。

我對我的 x86 非常生疏,在這個級別上使用它沒有太多價值,但是 0xFE 是 -2 並且它是一個 2 字節指令,所以這看起來像一個偏移量,所以 0xEB 自然是 8 位 x86指令,0xFE 偏移量。 我們可以測試一下

here:
jmp here
jmp here
jmp here
jmp here
jmp here

我討厭 x86 工具,等一下,讓我們回到這個。

鏈接,什么是 start 或 _start 的事情。 至少對於 gnu 工具鏈來說,這不是必需的,也許其他的,您必須將代碼寫入工具鏈。 如果您堅持使用高級語言和已經構建的工具,甚至是 gnu,那么已經為您完成了血腥的細節。 高級語言需要引導....在 asm 或一些低級語言中,如果您嘗試以相同的語言引導,則雞和蛋問題,有些人嘗試並最終失敗。

請注意 gnu 匯編程序,並了解 gnu 支持許多“目標”(x86、arm、mips 等),不一定是設計使然,因為每個目標都可能由不同的人或作者團隊創建,但更多的是借用現有的目標代碼把它變成一些新的目標代碼,比如 jmp。 點表示這個地址是這里的快捷方式:jmp here,無需輸入那么多文本。 您可以使用其中一些 gnu 匯編語言執行 jmp.+2 ...

如果我這樣做

as so.s -o so.o
ld so.o -o so.elf
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
objdump -d so.elf

so.elf:     file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <__bss_start-0x1000>:
  401000:   eb fe                   jmp    401000 <__bss_start-0x1000>

所以我們將它鏈接為最終的二進制文件,加載/入口點是 0x401000。 但是我們沒有向 linker 指定任何 memory 地址,它是如何確定這是我們想要代碼的地方? 因為工具鏈是為我的操作系統構建的(哦,是的,假設沒有兩個操作系統支持相同的二進制文件格式,也沒有相同的操作系統加載所述二進制格式文件的規則,以及特定於每個操作的系統調用系統等等),它是用一個 C 庫,然后是該庫和目標的一些引導代碼,以及一個 linker 腳本構建的,該腳本與該目標的該庫的引導代碼相結合......有一個默認值. And that default linker script, using the linker script language for the gnu linker, ld (assume no two toolchains use the same linker script language), contains

ENTRY(_start)

它告訴 linker 在二進制文件中標記找到 label _start 的入口點。 這不一定是程序中的第一條指令。 它幾乎可以在任何地方,但要根據操作系統加載程序的規則。 由於我沒有指定它選擇了默認的 linker 腳本。

現在,即使我:

ld -Ttext=0x1000 so.o -o so.elf
ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000


objdump -d so.elf

so.elf:     file format elf64-x86-64


Disassembly of section .text:

0000000000001000 <__bss_start-0x1000>:
    1000:   eb fe                   jmp    1000 <__bss_start-0x1000>

這實際上是超級痛苦和丑陋的,因為它正在使用別人的 linker 腳本並破解其中的一部分,但給我們留下了其他部分:

jmp .
.data
.byte 0x55

ld -Ttext=0x1000 so.o -o so.elf

Disassembly of section .text:

0000000000001000 <.text>:
    1000:   eb fe                   jmp    1000 <__bss_start-0x1001>

Disassembly of section .data:

0000000000002000 <__bss_start-0x1>:
    2000:   55                      push   %rbp

因此,如果我制作自己的 linker 腳本

MEMORY
{
    one : ORIGIN = 0x00000000, LENGTH = 0x1000
    two : ORIGIN = 0x80000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > one
    .bss    : { *(.bss*)    } > two
}

並與之鏈接

as so.s -o so.o
ld  -Tso.ld so.o -o so.elf
objdump -d so.elf

so.elf:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
   0:   eb fe                   jmp    0x0

它不再抱怨_start。 因為我的沒有 ENTRY(_start)

但如果我:

ENTRY(banana)
MEMORY
{
    one : ORIGIN = 0x00000000, LENGTH = 0x1000
    two : ORIGIN = 0x80000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > one
    .bss    : { *(.bss*)    } > two
}

ld: warning: cannot find entry symbol banana; defaulting to 0000000000000000

所以很容易安裝 gnu binutils 工具,你可以看到使用它們是多么容易......

所以回到另一件事

here:
jmp here
jmp here
jmp here
jmp here
jmp here
jmp here

可變長度指令集的反匯編非常痛苦,即使是 gnu 也很困難,但有時您需要鏈接,尤其是在查看跳轉、分支、調用等時...

MEMORY
{
    one : ORIGIN = 0x00001000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > one
}

objdump -d so.o

so.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <here>:
   0:   eb fe                   jmp    0 <here>
   2:   eb fc                   jmp    0 <here>
   4:   eb fa                   jmp    0 <here>
   6:   eb f8                   jmp    0 <here>
   8:   eb f6                   jmp    0 <here>
   a:   eb f4                   jmp    0 <here>

objdump -d so.elf

so.elf:     file format elf64-x86-64


Disassembly of section .text:

0000000000001000 <here>:
    1000:   eb fe                   jmp    1000 <here>
    1002:   eb fc                   jmp    1000 <here>
    1004:   eb fa                   jmp    1000 <here>
    1006:   eb f8                   jmp    1000 <here>
    1008:   eb f6                   jmp    1000 <here>
    100a:   eb f4                   jmp    1000 <here>

它是相同的機器代碼。 像 x86 這樣具有重載助記符的匯編語言可能有近跳和遠跳。

所以.s

.globl one
one:
jmp two

xs

.globl two
two:
jmp one

MEMORY
{
    one : ORIGIN = 0x00001000, LENGTH = 0x1000
    two : ORIGIN = 0x00002000, LENGTH = 0x1000
}
SECTIONS
{
    .one   : { so.o(.text)   } > one
    .two   : { x.o(.text)   } > two
}

as so.s -o so.o
as x.s -o x.o
ld -Tso.ld -o so.elf

objdump -d  so.o

so.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <one>:
   0:   e9 00 00 00 00          jmpq   5 <one+0x5>

objdump -d  x.o

x.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <two>:
   0:   e9 00 00 00 00          jmpq   5 <two+0x5>

objdump -d so.elf

so.elf:     file format elf64-x86-64


Disassembly of section .one:

0000000000001000 <one>:
    1000:   e9 fb 0f 00 00          jmpq   2000 <two>

Disassembly of section .two:

0000000000002000 <two>:
    2000:   e9 fb ef ff ff          jmpq   1000 <one>

在 object 級別(so.o,xo),匯編器不知道這些標簽在哪里,so.s 沒有兩個 label 所以匯編器假設它是外部的,並且必須假設距離很遠並且編碼因此(例如,如果要制作一個 eb 00 並且一旦鏈接 label 就太遠了,因為代碼有問題並且可能無法使用,所以工具鏈已經設計了如何解決這些問題的規則,正如我們在上面看到的那樣jmp 這里的那些。顯然這是一個 pc 相對偏移量,它不是一個絕對地址。此外,每條指令都是兩個字節,你 go 向下的偏移量是兩個更遠的地方。如果混合在那個代碼中是一個長跳到外部,但匯編器將其編碼為短跳轉,然后 linker 嘗試解決它,如果它再插入三個字節並更改操作碼,那么下面的 jmp 在這里由於它們是完整的機器代碼,因此現在所有的結構都被沖洗掉了。

因此,無論如何,匯編程序都會為偏移量填充零,然后 linker 在完成其工作時將這些零替換為實際偏移量。 有時你會看到匯編器編碼了一個跳轉到 self(jmp., here:jmp here),然后 linker 修補了它。

b .
b two

00000000 <.text>:
   0:   eafffffe    b   0 <.text>
   4:   eafffffe    b   0 <two>

在 object 級別,第二個實際上是 b 4 的 b 0 是錯誤的……但因為這是關心的 object。 它是程序的一部分,而不是完整的程序。

現在,匯編語言,C語言等等。編譯語言和匯編指令最終都是機器碼。 處理器不能直接執行機器代碼和 C 代碼,它必須轉換成它可以執行的東西,你能把這些語言變成解釋語言嗎? 可以,但一般不會。

0000000000001000 <one>:
    1000:   e9 fb 0f 00 00          jmpq   2000 <two>

Disassembly of section .two:

0000000000002000 <two>:
    2000:   e9 fb ef ff ff          jmpq   1000 <one>

所以歸根結底,使用 x86(64 位等)和任何處理器,處理器非常非常非常愚蠢,他們真的只做他們被告知的事情。 程序員可以完全控制處理器是否崩潰,這與火車非常相似,如果您不將軌道線性連接並設置開關以采用不同的軌道集等,火車就會崩潰。 作為程序員,您必須提供處理器將遵循的順序指令路徑,這就是它知道如何做的全部,如果您搞砸了處理器可能會偶然發現看起來像代碼的字節,或者您可能很幸運並且火車跳過空氣,只是碰巧降落在某處的某些軌道上,沒有撞車,但是現在火車不在您想要的位置,因此更難調試故障。

e9 被讀取,即知道接下來的四個字節,little endian 是一個偏移量。 現在我們認為從我們作為程序員開始的地方已經消耗了 5 個字節

one: jmp two

並將“一”視為該指令的“地址”,但(假)程序計數器在它看起來進行 pc 相對跳轉之前是五個字節,因此指令中編碼的偏移量是從地址偏移五個字節我們程序員正在考慮的一個。 如果您允許,這些工具會為您執行此操作。

所以現在啞處理器接收指令並執行它,如果我實際上已經將它鏈接到在操作系統上工作,這將是一個無限循環。 jmp 到那個地址的這個地址 跳轉到那個地址的這個地址 跳轉到這個地址。

j。 更簡單

   0:   eb fe                   jmp    0x0

用 eb 指令開始執行代碼,這兩個字節是整個指令,就是說向后跳轉兩個字節。 (啞)處理器向后兩個字節找到一個 eb 和 fe,告訴它向后跳兩個字節,它找到一個 eb fe,並且......永遠或直到被中斷。

為 x86 選擇另一個具有不同語法的匯編程序,也許 $ 表示這個(類似標簽的)地址在這里 jmp here。

而且由於我沒有弄亂 x86,因此如果該 jmp 沒有實際執行並且您正在查看的是由 bios 定義的構造,作為標記與引導相關的內容的一種方式,我不會感到驚訝。 bios (x86) 是另一本很長的書或一套書,x86 現在和歷史上如何啟動,等等。如果我的猜測是正確的,那么有人可能會說,嘿,讓我們先跳到 self 前面,以防有人試圖執行這個...我可能錯了,您的問題與工具和執行有關,而不是與引導扇區本身有關。 雖然它的名稱為 boot,但它被設計為 bios 用來啟動該媒體的東西。 引導通常是一系列步驟,從處理器如何在它可以支持的通常非常有限的單個或一組媒體上找到它的第一條指令,然后該代碼讓更多的處理器或外圍設備找到其他媒體等等(bios在 flash 上引導硬盤驅動器上的扇區到文件系統以加載引導加載程序,然后可能加載具有自己的外圍設備驅動程序的 kernel 並找到文件系統等)。 而你看到的只是階梯中的一步。

哦,是的....以及“它是如何運行的”,除了上面梯子上的步驟。

普通(編譯/組裝/鏈接)程序被放入操作系統支持的二進制格式(.exe、.coff、.elf 等)。 該文件格式必須符合該操作系統的規則。 然后,當在命令行上或單擊您嘗試運行它時,操作系統具有這些天設置虛擬環境/地址空間的代碼,以保護您免受他人和其他人的侵害,然后加載該文件的部分實際上是將代碼和數據放入該虛擬地址空間,然后從處理器的超級用戶級別切換到用戶/應用程序模式並跳轉到二進制文件定義的入口點。

gnu 和 llvm 等工具鏈可用於生成不符合特定主機操作系統的程序。 這些工具有些通用。 您可以在沒有 main() 且不支持 C 庫的情況下制作 C 程序,這有點微不足道。 例如,您可以創建一些 x86 代碼,如果您知道如何以及將其放在處理器用來在主板上啟動的 flash 上的位置,它將運行該代碼而不是 bios。 但是您現在符合不同環境的規則,並且可能無法使用該工具鏈的默認 output 二進制文件格式的許多組件。 如果您正在創建處理器啟動的第一條指令,那么沒有文件系統沒有操作系統(有時沒有內存)沒有任何東西可以解析 exe 或 elf 文件,它是純數據和機器代碼。 例如,您需要一些工具來獲取 elf 文件,並使用一些硬件或探針或魔術盒將芯片放入其中,提取字節並將它們編程到該芯片中。 或者某些工具將elf文件,按照芯片硬件程序的規則,制作芯片硬件程序知道如何使用的另一種文件格式。

x86 幾乎是您想嘗試的最糟糕的第一個地方,“但我有一個”是最糟糕的借口,您的 arm 處理器數量是 Z8A9DA7865483C5FD359F3ACEF178 處理器數量的 4 到 100 倍。 但“我有一個”並不是一個很好的理由。 如果你想在這個級別上工作。 從更好和/或更簡單的指令集/處理器和模擬器/模擬器開始。 你不會把任何東西變磚,你不會讓任何東西冒煙,而且你成功的幾率要高得多,因為你對正在發生的事情有更好的可見性/調試。 您無法看到/調試嘗試在某些媒體上讀取和使用引導扇區的 bios。 (除非您有該特定主板的模擬器或該特定主板的特殊工具和知識)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM