1. Linux/x86 啟動協議

在 x86 平臺上,Linux 核心使用相當複雜的啟動約定。這部分是由於歷史原因演變而來的,以及早期希望核心本身就是一個可啟動映象的願望、複雜的 PC 記憶體模型以及由於即時模式 DOS 作為主流作業系統有效消亡而導致的 PC 行業中期望的變化。

目前,存在以下版本的 Linux/x86 啟動協議。

舊核心

僅支援 zImage/Image。一些非常早期的核心甚至可能不支援命令列。

協議 2.00

(核心 1.3.73) 添加了 bzImage 和 initrd 支援,以及啟動載入器和核心之間進行通訊的正式方式。setup.S 是可重定位的,儘管傳統的設定區域仍然假定為可寫。

協議 2.01

(核心 1.3.76) 添加了堆溢位警告。

協議 2.02

(核心 2.4.0-test3-pre3) 新的命令列協議。降低傳統記憶體上限。不覆蓋傳統的設定區域,從而使啟動對於使用來自 SMM 或 32 位 BIOS 入口點的 EBDA 的系統來說是安全的。zImage 已棄用但仍受支援。

協議 2.03

(核心 2.4.18-pre1) 明確使啟動載入器可以使用最高的 initrd 地址。

協議 2.04

(核心 2.6.14) 將 syssize 欄位擴充套件到四個位元組。

協議 2.05

(核心 2.6.20) 使保護模式核心可重定位。引入了 relocatable_kernel 和 kernel_alignment 欄位。

協議 2.06

(核心 2.6.22) 添加了一個包含啟動命令列大小的欄位。

協議 2.07

(核心 2.6.24) 添加了半虛擬化啟動協議。引入了 hardware_subarch 和 hardware_subarch_data 以及 load_flags 中的 KEEP_SEGMENTS 標誌。

協議 2.08

(核心 2.6.26) 添加了 crc32 校驗和和 ELF 格式有效負載。引入了 payload_offset 和 payload_length 欄位以幫助定位有效負載。

協議 2.09

(核心 2.6.26) 添加了一個 64 位物理指標欄位,指向 struct setup_data 的單鏈表。

協議 2.10

(核心 2.6.31) 添加了一個協議,用於放寬超出新增的 kernel_alignment 的對齊方式,新的 init_size 和 pref_address 欄位。添加了擴充套件啟動載入器 ID。

協議 2.11

(核心 3.6) 添加了一個 EFI 交接協議入口點偏移量的欄位。

協議 2.12

(核心 3.8) 添加了 xloadflags 欄位和 struct boot_params 的擴充套件欄位,用於在 64 位中載入 4G 以上的 bzImage 和 ramdisk。

協議 2.13

(核心 3.14) 支援在 xloadflags 中設定 32 位和 64 位標誌,以支援從 32 位 EFI 啟動 64 位核心

協議 2.14

被不正確的 COMMIT ae7e1238e68f2a472a125673ab506d49158c1889 (“x86/boot: Add ACPI RSDP address to setup_header”) 損壞,請勿使用!!! 假設與 2.13 相同。

協議 2.15

(核心 5.5) 添加了 kernel_info 和 kernel_info.setup_type_max。

注意

只有在更改了設定頭時才應更改協議版本號。如果更改了 boot_params 或 kernel_info,則無需更新版本號。 此外,建議使用 xloadflags(在這種情況下,也不應更新協議版本號)或 kernel_info 將受支援的 Linux 核心特性傳達給啟動載入器。 由於原始設定頭中可用的空間非常有限,因此應謹慎考慮對其進行的每次更新。 從協議 2.15 開始,與啟動載入器通訊的主要方式是 kernel_info。

1.1. 記憶體佈局

核心載入器的傳統記憶體對映,用於 Image 或 zImage 核心,通常如下所示

              |                        |
0A0000        +------------------------+
              |  Reserved for BIOS     |      Do not use.  Reserved for BIOS EBDA.
09A000        +------------------------+
              |  Command line          |
              |  Stack/heap            |      For use by the kernel real-mode code.
098000        +------------------------+
              |  Kernel setup          |      The kernel real-mode code.
090200        +------------------------+
              |  Kernel boot sector    |      The kernel legacy boot sector.
090000        +------------------------+
              |  Protected-mode kernel |      The bulk of the kernel image.
010000        +------------------------+
              |  Boot loader           |      <- Boot sector entry point 0000:7C00
001000        +------------------------+
              |  Reserved for MBR/BIOS |
000800        +------------------------+
              |  Typically used by MBR |
000600        +------------------------+
              |  BIOS use only         |
000000        +------------------------+

使用 bzImage 時,保護模式核心被重定位到 0x100000(“高位記憶體”),核心真實模式塊(引導扇區、設定和堆疊/堆)可重定位到 0x10000 和低位記憶體末尾之間的任何地址。 不幸的是,在協議 2.00 和 2.01 中,核心內部仍然使用 0x90000+ 記憶體範圍; 2.02 協議解決了該問題。

最好使“記憶體上限”(引導載入器觸及的低位記憶體中的最高點)儘可能低,因為一些較新的 BIOS 已開始在低位記憶體的頂部附近分配一些相當大的記憶體量,稱為擴充套件 BIOS 資料區。 啟動載入器應使用“INT 12h”BIOS 呼叫來驗證有多少低位記憶體可用。

不幸的是,如果 INT 12h 報告記憶體量太低,則啟動載入器通常只能向用戶報告錯誤。 因此,應將啟動載入器設計為在低位記憶體中佔用儘可能少的空間。 對於需要將資料寫入 0x90000 段的 zImage 或舊的 bzImage 核心,啟動載入器應確保不使用 0x9A000 點以上的記憶體; 太多 BIOS 將在該點之上崩潰。

對於具有啟動協議版本 >= 2.02 的現代 bzImage 核心,建議使用以下記憶體佈局

              ~                        ~
              |  Protected-mode kernel |
100000        +------------------------+
              |  I/O memory hole       |
0A0000        +------------------------+
              |  Reserved for BIOS     |      Leave as much as possible unused
              ~                        ~
              |  Command line          |      (Can also be below the X+10000 mark)
X+10000       +------------------------+
              |  Stack/heap            |      For use by the kernel real-mode code.
X+08000       +------------------------+
              |  Kernel setup          |      The kernel real-mode code.
              |  Kernel boot sector    |      The kernel legacy boot sector.
X             +------------------------+
              |  Boot loader           |      <- Boot sector entry point 0000:7C00
001000        +------------------------+
              |  Reserved for MBR/BIOS |
000800        +------------------------+
              |  Typically used by MBR |
000600        +------------------------+
              |  BIOS use only         |
000000        +------------------------+

... where the address X is as low as the design of the boot loader permits.

1.2. 真實模式核心頭

在以下文字中,以及核心啟動序列中的任何位置,“扇區”是指 512 個位元組。它與底層介質的實際扇區大小無關。

載入 Linux 核心的第一步應該是載入真實模式程式碼(引導扇區和設定程式碼),然後檢查偏移量 0x01f1 處的以下頭。真實模式程式碼總共可以達到 32K,儘管引導載入器可以選擇僅載入前兩個扇區 (1K),然後檢查啟動扇區大小。

標頭如下所示

偏移量/大小

協議

名稱

含義

01F1/1

全部 (1)

setup_sects

設定在扇區中的大小

01F2/2

全部

root_flags

如果設定,則以只讀方式掛載根

01F4/4

2.04+(2)

syssize

32 位程式碼的大小,單位為 16 位元組段落

01F8/2

全部

ram_size

請勿使用 - 僅用於 bootsect.S

01FA/2

全部

vid_mode

影片模式控制

01FC/2

全部

root_dev

預設根裝置號

01FE/2

全部

boot_flag

0xAA55 魔數

0200/2

2.00+

跳躍

跳轉指令

0202/4

2.00+

標頭

魔術簽名“HdrS”

0206/2

2.00+

版本

支援的啟動協議版本

0208/4

2.00+

realmode_swtch

啟動載入器鉤子(見下文)

020C/2

2.00+

start_sys_seg

低位載入段 (0x1000)(已過時)

020E/2

2.00+

kernel_version

指向核心版本字串的指標

0210/1

2.00+

type_of_loader

啟動載入器識別符號

0211/1

2.00+

loadflags

啟動協議選項標誌

0212/2

2.00+

setup_move_size

移動到高位記憶體大小(與鉤子一起使用)

0214/4

2.00+

code32_start

啟動載入器鉤子(見下文)

0218/4

2.00+

ramdisk_image

initrd 載入地址(由啟動載入器設定)

021C/4

2.00+

ramdisk_size

initrd 大小(由啟動載入器設定)

0220/4

2.00+

bootsect_kludge

請勿使用 - 僅用於 bootsect.S

0224/2

2.01+

heap_end_ptr

設定結束後的可用記憶體

0226/1

2.02+(3)

ext_loader_ver

擴充套件的啟動載入器版本

0227/1

2.02+(3)

ext_loader_type

擴充套件的啟動載入器 ID

0228/4

2.02+

cmd_line_ptr

指向核心命令列的 32 位指標

022C/4

2.03+

initrd_addr_max

最高的合法 initrd 地址

0230/4

2.05+

kernel_alignment

核心所需的物理地址對齊方式

0234/1

2.05+

relocatable_kernel

核心是否可重定位

0235/1

2.10+

min_alignment

最小對齊方式,以 2 的冪表示

0236/2

2.12+

xloadflags

啟動協議選項標誌

0238/4

2.06+

cmdline_size

核心命令列的最大大小

023C/4

2.07+

hardware_subarch

硬體子架構

0240/8

2.07+

hardware_subarch_data

特定於子架構的資料

0248/4

2.08+

payload_offset

核心有效負載的偏移量

024C/4

2.08+

payload_length

核心有效負載的長度

0250/8

2.09+

setup_data

指向 struct setup_data 連結串列的 64 位物理指標

0258/8

2.10+

pref_address

首選載入地址

0260/4

2.10+

init_size

初始化期間所需的線性記憶體

0264/4

2.11+

handover_offset

交接入口點的偏移量

0268/4

2.15+

kernel_info_offset

kernel_info 的偏移量

注意

  1. 為了向後相容,如果 setup_sects 欄位包含 0,則實際值為 4。

  2. 對於早於 2.04 的啟動協議,syssize 欄位的高兩位位元組不可用,這意味著如果設定了 LOAD_HIGH 標誌,則無法確定 bzImage 核心的大小。

  3. 忽略,但可以安全設定,用於啟動協議 2.02-2.09。

如果在偏移量 0x202 處找不到“HdrS” (0x53726448) 魔數,則啟動協議版本為“舊”。 載入舊核心時,應假定以下引數

Image type = zImage
initrd not supported
Real-mode kernel must be located at 0x90000.

否則,“version”欄位包含協議版本,例如,協議版本 2.01 將在此欄位中包含 0x0201。 在標頭中設定欄位時,必須確保僅設定所用協議版本支援的欄位。

1.3. 標頭欄位的詳細資訊

對於每個欄位,有些是來自核心的資訊,提供給啟動載入器(“讀取”),有些預計由啟動載入器填寫(“寫入”),有些預計由啟動載入器讀取和修改(“修改”)。

所有通用啟動載入器都應寫入標記為“(強制性)”的欄位。 想要以非標準地址載入核心的啟動載入器應填寫標記為“(重定位)”的欄位;其他啟動載入器可以忽略這些欄位。

所有欄位的位元組順序都是小端(畢竟這是 x86。)

欄位名稱

setup_sects

型別

讀取

偏移量/大小

0x1f1/1

協議

全部

設定程式碼的大小,單位為 512 位元組扇區。如果此欄位為 0,則實際值為 4。真實模式程式碼由引導扇區(始終為一個 512 位元組扇區)加上設定程式碼組成。

欄位名稱

root_flags

型別

修改(可選)

偏移量/大小

0x1f2/2

協議

全部

如果此欄位為非零值,則根預設設定為只讀。 不建議使用此欄位; 請改用命令列上的“ro”或“rw”選項。

欄位名稱

syssize

型別

讀取

偏移量/大小

0x1f4/4 (協議 2.04+) 0x1f4/2 (所有協議)

協議

2.04+

保護模式程式碼的大小,單位為 16 位元組段落。 對於早於 2.04 的協議版本,此欄位僅為兩個位元組寬,因此,如果設定了 LOAD_HIGH 標誌,則不能信任核心的大小。

欄位名稱

ram_size

型別

核心內部

偏移量/大小

0x1f8/2

協議

全部

此欄位已過時。

欄位名稱

vid_mode

型別

修改(強制性)

偏移量/大小

0x1fa/2

請參閱有關特殊命令列選項的部分。

欄位名稱

root_dev

型別

修改(可選)

偏移量/大小

0x1fc/2

協議

全部

預設根裝置號。 不建議使用此欄位,請改用命令列上的“root=”選項。

欄位名稱

boot_flag

型別

讀取

偏移量/大小

0x1fe/2

協議

全部

包含 0xAA55。 這是舊 Linux 核心所擁有的最接近魔數的東西。

欄位名稱

跳躍

型別

讀取

偏移量/大小

0x200/2

協議

2.00+

包含一個 x86 跳轉指令,0xEB 後跟一個相對於位元組 0x202 的帶符號偏移量。 這可用於確定標頭的大小。

欄位名稱

標頭

型別

讀取

偏移量/大小

0x202/4

協議

2.00+

包含魔數“HdrS” (0x53726448)。

欄位名稱

版本

型別

讀取

偏移量/大小

0x206/2

協議

2.00+

包含啟動協議版本,格式為(主版本 << 8) + 次版本,例如,版本 2.04 為 0x0204,假設版本 10.17 則為 0x0a11。

欄位名稱

realmode_swtch

型別

修改(可選)

偏移量/大小

0x208/4

協議

2.00+

啟動載入器鉤子(請參見下面的高階啟動載入器鉤子。)

欄位名稱

start_sys_seg

型別

讀取

偏移量/大小

0x20c/2

協議

2.00+

低位載入段 (0x1000)。 已過時。

欄位名稱

kernel_version

型別

讀取

偏移量/大小

0x20e/2

協議

2.00+

如果設定為非零值,則包含指向以 NUL 結尾的人工可讀核心版本號字串的指標,小於 0x200。 這可用於向用戶顯示核心版本。 此值應小於 (0x200 * setup_sects)。

例如,如果此值設定為 0x1c00,則可以在核心檔案中偏移量 0x1e00 處找到核心版本號字串。 當且僅當“setup_sects”欄位包含值 15 或更高時,此值才有效,因為

0x1c00  < 15 * 0x200 (= 0x1e00) but
0x1c00 >= 14 * 0x200 (= 0x1c00)

0x1c00 >> 9 = 14, So the minimum value for setup_secs is 15.

欄位名稱

type_of_loader

型別

寫入(強制性)

偏移量/大小

0x210/1

協議

2.00+

如果你的啟動載入器具有分配的 ID(請參見下表),請在此處輸入 0xTV,其中 T 是啟動載入器的識別符號,V 是版本號。 否則,請在此處輸入 0xFF。

對於高於 T = 0xD 的啟動載入器 ID,請將 T = 0xE 寫入此欄位,並將擴充套件 ID 減去 0x10 寫入 ext_loader_type 欄位。 同樣,ext_loader_ver 欄位可用於為啟動載入器版本提供超過四位的版本號。

例如,對於 T = 0x15,V = 0x234,寫入

type_of_loader  <- 0xE4
ext_loader_type <- 0x05
ext_loader_ver  <- 0x23

分配的啟動載入器 ID(十六進位制)

0

LILO (0x00 保留給 pre-2.00 啟動載入器)

1

Loadlin

2

bootsect-loader(0x20,保留所有其他值)

3

Syslinux

4

Etherboot/gPXE/iPXE

5

ELILO

7

GRUB

8

U-Boot

9

Xen

A

Gujin

B

Qemu

C

Arcturus Networks uCbootloader

D

kexec-tools

E

擴充套件(請參見 ext_loader_type)

F

特殊 (0xFF = 未定義)

10

已保留

11

Minimal Linux Bootloader <http://sebastian-plotz.blogspot.de>

12

OVMF UEFI 虛擬化堆疊

13

barebox

如果您需要分配啟動載入器 ID 值,請聯絡 <hpa@zytor.com>。

欄位名稱

loadflags

型別

修改(強制性)

偏移量/大小

0x211/1

協議

2.00+

此欄位是一個位掩碼。

位 0(讀取):LOADED_HIGH

  • 如果為 0,則保護模式程式碼載入到 0x10000。

  • 如果為 1,則保護模式程式碼載入到 0x100000。

位 1(核心內部):KASLR_FLAG

  • 壓縮核心內部使用該標誌將 KASLR 狀態傳達給正確的核心。

    • 如果為 1,則啟用 KASLR。

    • 如果為 0,則停用 KASLR。

位 5(寫入):QUIET_FLAG

  • 如果為 0,則列印早期訊息。

  • 如果為 1,則禁止顯示早期訊息。

    這要求核心(解壓縮器和早期核心)不要寫入需要直接訪問顯示硬體的早期訊息。

位 6(已過時):KEEP_SEGMENTS

協議:2.07+

  • 此標誌已過時。

位 7(寫入):CAN_USE_HEAP

將此位設定為 1,以指示在 heap_end_ptr 中輸入的值有效。 如果清除此欄位,則將停用某些設定程式碼功能。

欄位名稱

setup_move_size

型別

修改(強制性)

偏移量/大小

0x212/2

協議

2.00-2.01

使用協議 2.00 或 2.01 時,如果真實模式核心未載入到 0x90000,則稍後將在載入序列中將其移動到此處。 如果除了真實模式核心本身之外,你還想移動其他資料(例如核心命令列),請填寫此欄位。

該單位是從引導扇區開始的位元組。

當協議為 2.02 或更高版本,或者真實模式程式碼載入到 0x90000 時,可以忽略此欄位。

欄位名稱

code32_start

型別

修改(可選,重定位)

偏移量/大小

0x214/4

協議

2.00+

要跳轉到的保護模式地址。 這預設為核心的載入地址,引導載入器可以使用它來確定正確的載入地址。

可以修改此欄位以實現兩個目的

  1. 作為啟動載入器鉤子(請參見下面的高階啟動載入器鉤子。)

  2. 如果未安裝鉤子的啟動載入器以非標準地址載入可重定位核心,則它將必須修改此欄位以指向載入地址。

欄位名稱

ramdisk_image

型別

寫入(強制性)

偏移量/大小

0x218/4

協議

2.00+

初始 ramdisk 或 ramfs 的 32 位線性地址。 如果沒有初始 ramdisk/ramfs,則保留為零。

欄位名稱

ramdisk_size

型別

寫入(強制性)

偏移量/大小

0x21c/4

協議

2.00+

初始 ramdisk 或 ramfs 的大小。如果沒有初始 ramdisk/ramfs,則保留為零。

欄位名稱

bootsect_kludge

型別

核心內部

偏移量/大小

0x220/4

協議

2.00+

此欄位已過時。

欄位名稱

heap_end_ptr

型別

寫入(強制性)

偏移量/大小

0x224/2

協議

2.01+

將此欄位設定為 setup 堆疊/堆結束的偏移量(從真實模式程式碼的開頭算起),減去 0x0200。

欄位名稱

ext_loader_ver

型別

寫入(可選)

偏移量/大小

0x226/1

協議

2.02+

此欄位用作 type_of_loader 欄位中版本號的擴充套件。總版本號被認為是 (type_of_loader & 0x0f) + (ext_loader_ver << 4)。

此欄位的使用特定於引導載入程式。如果未寫入,則為零。

2.6.31 之前的核心無法識別此欄位,但對於協議版本 2.02 或更高版本,可以安全寫入。

欄位名稱

ext_loader_type

型別

寫入(如果 (type_of_loader & 0xf0) == 0xe0,則為必須)

偏移量/大小

0x227/1

協議

2.02+

此欄位用作 type_of_loader 欄位中型別編號的擴充套件。如果 type_of_loader 中的型別為 0xE,則實際型別為 (ext_loader_type + 0x10)。

如果 type_of_loader 中的型別不是 0xE,則忽略此欄位。

2.6.31 之前的核心無法識別此欄位,但對於協議版本 2.02 或更高版本,可以安全寫入。

欄位名稱

cmd_line_ptr

型別

寫入(強制性)

偏移量/大小

0x228/4

協議

2.02+

將此欄位設定為核心命令列的線性地址。核心命令列可以位於 setup 堆的末尾和 0xA0000 之間的任何位置;它不必與真實模式程式碼本身位於同一個 64K 段中。

即使您的引導載入程式不支援命令列,也請填寫此欄位,在這種情況下,您可以將其指向一個空字串(或者更好的是,指向字串“auto”。)如果此欄位保留為零,則核心將假定您的引導載入程式不支援 2.02+ 協議。

欄位名稱

initrd_addr_max

型別

讀取

偏移量/大小

0x22c/4

協議

2.03+

初始 ramdisk/ramfs 內容可能佔用的最大地址。對於 2.02 或更早的引導協議,不存在此欄位,最大地址為 0x37FFFFFF。(此地址定義為最高安全位元組的地址,因此如果您的 ramdisk 恰好為 131072 位元組長,並且此欄位為 0x37FFFFFF,則可以從 0x37FE0000 開始您的 ramdisk。)

欄位名稱

kernel_alignment

型別

讀取/修改(重定位)

偏移量/大小

0x230/4

協議

2.05+(讀取),2.10+(修改)

核心所需的對齊單元(如果 relocatable_kernel 為真)。如果以與此欄位中的值不相容的對齊方式載入可重定位核心,則將在核心初始化期間重新對齊。

從協議版本 2.10 開始,這反映了核心為獲得最佳效能而首選的對齊方式;載入程式可以修改此欄位以允許較小的對齊方式。請參閱下面的 min_alignment 和 pref_address 欄位。

欄位名稱

relocatable_kernel

型別

讀取(重定位)

偏移量/大小

0x234/1

協議

2.05+

如果此欄位非零,則可以在滿足 kernel_alignment 欄位的任何地址載入核心的保護模式部分。載入後,引導載入程式必須設定 code32_start 欄位以指向載入的程式碼,或指向引導載入程式鉤子。

欄位名稱

min_alignment

型別

讀取(重定位)

偏移量/大小

0x235/1

協議

2.10+

如果此欄位非零,則以 2 的冪表示核心啟動所需的最小對齊方式,而不是首選的對齊方式。如果引導載入程式使用此欄位,則應使用所需的對齊單元更新 kernel_alignment 欄位;通常

kernel_alignment = 1 << min_alignment;

核心過度未對齊可能會導致相當大的效能成本。因此,載入程式通常應嘗試從 kernel_alignment 到此對齊方式的每個 2 的冪的對齊方式。

欄位名稱

xloadflags

型別

讀取

偏移量/大小

0x236/2

協議

2.12+

此欄位是一個位掩碼。

位 0(讀取):XLF_KERNEL_64

  • 如果為 1,則此核心在 0x200 處具有傳統的 64 位入口點。

位 1(讀取):XLF_CAN_BE_LOADED_ABOVE_4G

  • 如果為 1,則 kernel/boot_params/cmdline/ramdisk 可以在 4G 以上。

位 2(讀取):XLF_EFI_HANDOVER_32

  • 如果為 1,則核心支援在 handover_offset 處給出的 32 位 EFI 切換入口點。

位 3(讀取):XLF_EFI_HANDOVER_64

  • 如果為 1,則核心支援在 handover_offset + 0x200 處給出的 64 位 EFI 切換入口點。

位 4(讀取):XLF_EFI_KEXEC

  • 如果為 1,則核心支援使用 EFI 執行時支援的 kexec EFI 啟動。

欄位名稱

cmdline_size

型別

讀取

偏移量/大小

0x238/4

協議

2.06+

不帶終止零的命令列的最大大小。這意味著命令列最多可以包含 cmdline_size 個字元。在協議版本 2.05 及更早版本中,最大大小為 255。

欄位名稱

hardware_subarch

型別

寫入(可選,預設為 x86/PC)

偏移量/大小

0x23c/4

協議

2.07+

在半虛擬化環境中,硬體低階架構元件(例如中斷處理、頁表處理和訪問程序控制暫存器)需要以不同的方式完成。

此欄位允許引導載入程式通知核心我們處於其中一個環境中。

0x00000000

預設的 x86/PC 環境

0x00000001

lguest

0x00000002

Xen

0x00000003

Intel MID(Moorestown、CloverTrail、Merrifield、Moorefield)

0x00000004

CE4100 TV 平臺

欄位名稱

hardware_subarch_data

型別

寫入(子架構相關)

偏移量/大小

0x240/8

協議

2.07+

指向硬體子架構特定資料的指標。此欄位當前未用於預設的 x86/PC 環境,請勿修改。

欄位名稱

payload_offset

型別

讀取

偏移量/大小

0x248/4

協議

2.08+

如果非零,則此欄位包含從保護模式程式碼的開頭到有效負載的偏移量。

有效負載可以被壓縮。壓縮和未壓縮資料的格式都應使用標準魔數確定。當前支援的壓縮格式為 gzip(魔數 1F 8B 或 1F 9E)、bzip2(魔數 42 5A)、LZMA(魔數 5D 00)、XZ(魔數 FD 37)、LZ4(魔數 02 21)和 ZSTD(魔數 28 B5)。未壓縮的有效負載當前始終為 ELF(魔數 7F 45 4C 46)。

欄位名稱

payload_length

型別

讀取

偏移量/大小

0x24c/4

協議

2.08+

有效負載的長度。

欄位名稱

setup_data

型別

寫入(特殊)

偏移量/大小

0x250/8

協議

2.09+

指向 NULL 終止的 struct setup_data 單鏈表的 64 位物理指標。這用於定義更可擴充套件的引導引數傳遞機制。struct setup_data 的定義如下

struct setup_data {
     __u64 next;
     __u32 type;
     __u32 len;
     __u8 data[];
}

其中,next 是指向連結串列下一個節點的 64 位物理指標,最後一個節點的 next 欄位為 0;type 用於標識資料的 內容;len 是資料欄位的長度;資料儲存實際有效負載。

此列表可能會在啟動過程中的多個點被修改。因此,在修改此列表時,應始終確保考慮連結串列已包含條目的情況。

setup_data 對於非常大的資料物件來說使用起來有點笨拙,既因為 setup_data 標頭必須與資料物件相鄰,又因為它具有 32 位長度欄位。然而,重要的是引導過程的中間階段有一種方法來識別哪些記憶體塊被核心資料佔用。

因此,協議 2.15 中引入了 setup_indirect struct 和 SETUP_INDIRECT 型別

struct setup_indirect {
     __u32 type;
     __u32 reserved;         /* Reserved, must be set to zero. */
     __u64 len;
     __u64 addr;
};

type 成員是 SETUP_INDIRECT | SETUP_* 型別。但是,它不能是 SETUP_INDIRECT 本身,因為使 setup_indirect 成為樹結構可能需要在需要解析它的東西中佔用大量堆疊空間,並且引導上下文中的堆疊空間可能有限。

讓我們舉一個例子,說明如何使用 setup_indirect 指向 SETUP_E820_EXT 資料。在這種情況下,setup_data 和 setup_indirect 將如下所示

struct setup_data {
     .next = 0,      /* or <addr_of_next_setup_data_struct> */
     .type = SETUP_INDIRECT,
     .len = sizeof(setup_indirect),
     .data[sizeof(setup_indirect)] = (struct setup_indirect) {
             .type = SETUP_INDIRECT | SETUP_E820_EXT,
             .reserved = 0,
             .len = <len_of_SETUP_E820_EXT_data>,
             .addr = <addr_of_SETUP_E820_EXT_data>,
     },
}

注意

SETUP_INDIRECT | SETUP_NONE 物件無法與 SETUP_INDIRECT 本身正確區分。因此,引導載入程式無法提供這種物件。

欄位名稱

pref_address

型別

讀取(重定位)

偏移量/大小

0x258/8

協議

2.10+

如果此欄位非零,則表示核心的首選載入地址。如果可能,重定位引導載入程式應嘗試在此地址載入。

不可重定位核心將無條件地移動自身並在該地址執行。可重定位核心會將自身移動到此地址,如果它載入到低於此地址的位置。

欄位名稱

init_size

型別

讀取

偏移量/大小

0x260/4

此欄位指示從核心執行時起始地址開始的線性連續記憶體量,核心需要這些記憶體才能檢查其記憶體對映。這與核心啟動所需的總記憶體量不同,但可以被重定位引導載入程式用來幫助選擇核心的安全載入地址。

核心執行時起始地址由以下演算法確定

if (relocatable_kernel) {
     if (load_address < pref_address)
             load_address = pref_address;
     runtime_start = align_up(load_address, kernel_alignment);
} else {
     runtime_start = pref_address;
}

因此,引導載入程式可以將必要的記憶體視窗位置和大小估計為

memory_window_start = runtime_start;
memory_window_size = init_size;

欄位名稱

handover_offset

型別

讀取

偏移量/大小

0x264/4

此欄位是從核心映象的開頭到 EFI 切換協議入口點的偏移量。使用 EFI 切換協議啟動核心的引導載入程式應跳轉到此偏移量。

有關更多詳細資訊,請參閱下面的 EFI 切換協議。

欄位名稱

kernel_info_offset

型別

讀取

偏移量/大小

0x268/4

協議

2.15+

此欄位是從核心映象的開頭到 kernel_info 的偏移量。kernel_info 結構嵌入在未壓縮的保護模式區域中的 Linux 映象中。

1.4. kernel_info

標頭之間的關係類似於各種資料節

setup_header = .data
boot_params/setup_data = .bss

上面的列表中缺少什麼?沒錯

kernel_info = .rodata

長期以來,由於缺乏替代方案以及——尤其是在早期——慣性,我們一直在(濫用).data 用於可以放入 .rodata 或 .bss 中的內容。此外,BIOS stub 負責建立 boot_params,因此 BIOS 的載入程式無法使用它(但是可以使用 setup_data)。

由於 2 位元組跳轉欄位的範圍(該欄位兼作結構的長度欄位)與 struct boot_params 中“孔”的大小相結合,保護模式載入程式或 BIOS stub 必須將其複製到該孔中,因此 setup_header 永久限制為 144 位元組。目前它的長度是 119 位元組,這給我們留下了 25 個非常寶貴的位元組。如果不完全修改引導協議,破壞向後相容性,就無法解決這個問題。

boot_params 本身限制為 4096 位元組,但可以透過新增 setup_data 條目來任意擴充套件。它不能用於傳達核心映象的屬性,因為它是 .bss 並且沒有映象提供的內容。

kernel_info 透過為核心映象資訊提供可擴充套件的位置來解決這個問題。它是隻讀的,因為核心無法依賴引導載入程式將其內容複製到任何地方,但這沒關係;如果需要,它仍然可以包含已啟用的引導載入程式應複製到 setup_data 塊中的資料項。

所有 kernel_info 資料都應是此結構的一部分。固定大小的資料必須放在 kernel_info_var_len_data 標籤之前。可變大小的資料必須放在 kernel_info_var_len_data 標籤之後。每個可變大小的資料塊必須以標頭/魔術和它的大小作為字首,例如

kernel_info:
      .ascii  "LToP"          /* Header, Linux top (structure). */
      .long   kernel_info_var_len_data - kernel_info
      .long   kernel_info_end - kernel_info
      .long   0x01234567      /* Some fixed size data for the bootloaders. */
kernel_info_var_len_data:
example_struct:               /* Some variable size data for the bootloaders. */
      .ascii  "0123"          /* Header/Magic. */
      .long   example_struct_end - example_struct
      .ascii  "Struct"
      .long   0x89012345
example_struct_end:
example_strings:              /* Some variable size data for the bootloaders. */
      .ascii  "ABCD"          /* Header/Magic. */
      .long   example_strings_end - example_strings
      .asciz  "String_0"
      .asciz  "String_1"
example_strings_end:
kernel_info_end:

這樣,kernel_info 就是一個自包含的 blob。

注意

每個可變大小的資料標頭/魔術可以是任何 4 個字元的字串,字串末尾沒有 0,並且不會與現有的可變長度資料標頭/魔術衝突。

1.5. kernel_info 欄位的詳細資訊

欄位名稱

標頭

偏移量/大小

0x0000/4

包含魔數“LToP”(0x506f544c)。

欄位名稱

大小

偏移量/大小

0x0004/4

此欄位包含 kernel_info 的大小,包括 kernel_info.header。它不計算 kernel_info.kernel_info_var_len_data 的大小。引導載入程式應使用此欄位來檢測 kernel_info 中支援的固定大小欄位和 kernel_info.kernel_info_var_len_data 的開頭。

欄位名稱

size_total

偏移量/大小

0x0008/4

此欄位包含 kernel_info 的大小,包括 kernel_info.header 和 kernel_info.kernel_info_var_len_data。

欄位名稱

setup_type_max

偏移量/大小

0x000c/4

此欄位包含 setup_data 和 setup_indirect 結構允許的最大型別。

1.6. 核心命令列

核心命令列已成為引導載入程式與核心通訊的重要方式。它的一些選項也與引導載入程式本身相關,請參閱下面的“特殊命令列選項”。

核心命令列是一個以 null 結尾的字串。可以從 cmdline_size 欄位檢索最大長度。在協議版本 2.06 之前,最大值為 255 個字元。太長的字串將被核心自動截斷。

如果引導協議版本為 2.02 或更高版本,則核心命令列的地址由標頭欄位 cmd_line_ptr 給出(參見上文。)此地址可以位於 setup 堆的末尾和 0xA0000 之間的任何位置。

如果協議版本不是 2.02 或更高版本,則使用以下協議輸入核心命令列

  • 在偏移量 0x0020(字)“cmd_line_magic”處,輸入魔數 0xA33F。

  • 在偏移量 0x0022(字)“cmd_line_offset”處,輸入核心命令列的偏移量(相對於真實模式核心的起始位置)。

  • 核心命令列必須位於 setup_move_size 覆蓋的記憶體區域內,因此您可能需要調整此欄位。

1.7. 真實模式程式碼的記憶體佈局

真實模式程式碼需要設定堆疊/堆,以及為核心命令列分配的記憶體。這需要在底部兆位元組中的真實模式可訪問記憶體中完成。

應該注意的是,現代機器通常具有相當大的擴充套件 BIOS 資料區 (EBDA)。因此,建議儘可能少地使用低兆位元組。

不幸的是,在以下情況下,必須使用 0x90000 記憶體段

  • 載入 zImage 核心時 ((loadflags & 0x01) == 0)。

  • 載入 2.01 或更早的引導協議核心時。

注意

對於 2.00 和 2.01 引導協議,真實模式程式碼可以在另一個地址載入,但它會在內部重定位到 0x90000。對於“舊”協議,真實模式程式碼必須載入到 0x90000。

在 0x90000 載入時,避免使用 0x9a000 以上的記憶體。

對於引導協議 2.02 或更高版本,命令列不必與真實模式 setup 程式碼位於同一 64K 段中;因此允許為堆疊/堆提供完整的 64K 段,並將命令列定位在其上方。

核心命令列不應位於真實模式程式碼下方,也不應位於高位記憶體中。

1.8. 示例引導配置

作為一個示例配置,假設真實模式段的以下佈局。

在 0x90000 以下載入時,使用整個段

0x0000-0x7fff

真實模式核心

0x8000-0xdfff

堆疊和堆

0xe000-0xffff

核心命令列

在 0x90000 載入時 OR 協議版本為 2.01 或更早版本

0x0000-0x7fff

真實模式核心

0x8000-0x97ff

堆疊和堆

0x9800-0x9fff

核心命令列

這樣的引導載入程式應在標頭中輸入以下欄位

unsigned long base_ptr;       /* base address for real-mode segment */

if (setup_sects == 0)
      setup_sects = 4;

if (protocol >= 0x0200) {
      type_of_loader = <type code>;
      if (loading_initrd) {
              ramdisk_image = <initrd_address>;
              ramdisk_size = <initrd_size>;
      }

      if (protocol >= 0x0202 && loadflags & 0x01)
              heap_end = 0xe000;
      else
              heap_end = 0x9800;

      if (protocol >= 0x0201) {
              heap_end_ptr = heap_end - 0x200;
              loadflags |= 0x80;              /* CAN_USE_HEAP */
      }

      if (protocol >= 0x0202) {
              cmd_line_ptr = base_ptr + heap_end;
              strcpy(cmd_line_ptr, cmdline);
      } else {
              cmd_line_magic  = 0xA33F;
              cmd_line_offset = heap_end;
              setup_move_size = heap_end + strlen(cmdline) + 1;
              strcpy(base_ptr + cmd_line_offset, cmdline);
      }
} else {
      /* Very old kernel */

      heap_end = 0x9800;

      cmd_line_magic  = 0xA33F;
      cmd_line_offset = heap_end;

      /* A very old kernel MUST have its real-mode code loaded at 0x90000 */
      if (base_ptr != 0x90000) {
              /* Copy the real-mode kernel */
              memcpy(0x90000, base_ptr, (setup_sects + 1) * 512);
              base_ptr = 0x90000;              /* Relocated */
      }

      strcpy(0x90000 + cmd_line_offset, cmdline);

      /* It is recommended to clear memory up to the 32K mark */
      memset(0x90000 + (setup_sects + 1) * 512, 0, (64 - (setup_sects + 1)) * 512);
}

1.9. 載入核心的其餘部分

32 位(非真實模式)核心從核心檔案中的偏移量 (setup_sects + 1) * 512 開始(同樣,如果 setup_sects == 0,則實際值為 4。)它應該載入到 Image/zImage 核心的地址 0x10000 和 bzImage 核心的地址 0x100000。

如果協議 >= 2.00 並且 loadflags 欄位中的 0x01 位 (LOAD_HIGH) 設定,則核心是 bzImage 核心

is_bzImage = (protocol >= 0x0200) && (loadflags & 0x01);
load_address = is_bzImage ? 0x100000 : 0x10000;

注意

Image/zImage 核心的大小可以高達 512K,因此使用整個 0x10000-0x90000 範圍的記憶體。這意味著這些核心幾乎必須在 0x90000 載入真實模式部分。bzImage 核心允許更大的靈活性。

1.10. 特殊命令列選項

如果引導載入程式提供的命令列由使用者輸入,則使用者可能希望以下命令列選項能夠工作。即使並非所有選項對核心都真正有意義,通常也不應從核心命令列中刪除它們。需要引導載入程式本身的其他命令列選項的引導載入程式作者應在核心的命令列引數中註冊它們,以確保它們現在或將來不會與實際核心選項衝突。

vga=<mode>

這裡的 <mode> 是一個整數(以 C 表示法,可以是十進位制、八進位制或十六進位制)或字串“normal”(表示 0xFFFF)、“ext”(表示 0xFFFE)或“ask”(表示 0xFFFD)之一。此值應輸入到 vid_mode 欄位中,因為它在解析命令列之前被核心使用。

mem=<size>

<size> 是一個以 C 表示法的整數,可以選擇後跟(不區分大小寫)K、M、G、T、P 或 E(表示 << 10、<< 20、<< 30、<< 40、<< 50 或 << 60)。這指定了核心的記憶體結束位置。這會影響 initrd 的可能位置,因為 initrd 應放置在靠近記憶體結束位置。請注意,這是核心和引導載入程式的選項!

initrd=<file>

應載入 initrd。<file> 的含義顯然取決於引導載入程式,並且一些引導載入程式(例如 LILO)沒有這樣的命令。

此外,一些引導載入程式將以下選項新增到使用者指定的命令列

BOOT_IMAGE=<file>

已載入的引導映象。同樣,<file> 的含義顯然取決於引導載入程式。

auto

核心在沒有明確的使用者干預的情況下啟動。

如果這些選項由引導載入程式新增,則強烈建議將它們放置在最前面,在使用者指定或配置指定的命令列之前。否則,“init=/bin/sh”會被“auto”選項混淆。

1.11. 執行核心

透過跳轉到核心入口點來啟動核心,核心入口點位於從真實模式核心開始的偏移量 0x20 處。這意味著,如果您將真實模式核心程式碼載入到 0x90000,則核心入口點為 9020:0000。

在入口處,ds = es = ss 應指向真實模式核心程式碼的起始位置(如果程式碼載入到 0x90000,則為 0x9000),sp 應正確設定,通常指向堆的頂部,並且應停用中斷。此外,為了防止核心中的錯誤,建議引導載入程式設定 fs = gs = ds = es = ss。

在我們上面的示例中,我們將執行

/*
 * Note: in the case of the "old" kernel protocol, base_ptr must
 * be == 0x90000 at this point; see the previous sample code.
 */
seg = base_ptr >> 4;

cli();                        /* Enter with interrupts disabled! */

/* Set up the real-mode kernel stack */
_SS = seg;
_SP = heap_end;

_DS = _ES = _FS = _GS = seg;
jmp_far(seg + 0x20, 0);       /* Run the kernel */

如果您的引導扇區訪問軟盤驅動器,建議在執行核心之前關閉軟盤電機,因為核心啟動會關閉中斷,因此電機不會關閉,特別是如果載入的核心具有軟盤驅動程式作為按需載入的模組!

1.12. 高階引導載入程式鉤子

如果引導載入程式在特別惡劣的環境中執行(例如 LOADLIN,它在 DOS 下執行),則可能無法遵循標準的記憶體位置要求。這樣的引導載入程式可以使用以下鉤子,如果設定了這些鉤子,則核心會在適當的時間呼叫它們。這些鉤子的使用可能應該被認為是絕對的最後手段!

重要提示:所有鉤子都需要在呼叫過程中保留 %esp、%ebp、%esi 和 %edi。

realmode_swtch

在進入保護模式之前立即呼叫的 16 位真實模式遠子例程。預設例程停用 NMI,因此您的例程可能也應該這樣做。

code32_start

在轉換為保護模式後,但在核心解壓縮之前立即跳轉到的 32 位扁平模式例程。除 CS 外,不保證設定任何段(當前核心會這樣做,但較舊的核心不會);您應該自己將它們設定為 BOOT_DS (0x18)。

完成鉤子後,您應該跳轉到您的引導載入程式覆蓋它之前此欄位中的地址(如果合適,則重新定位)。

1.13. 32 位引導協議

對於具有傳統 BIOS 以外的一些新 BIOS 的機器,例如 EFI、LinuxBIOS 等以及 kexec,無法使用基於傳統 BIOS 的核心中的 16 位真實模式 setup 程式碼,因此需要定義 32 位引導協議。

在 32 位引導協議中,載入 Linux 核心的第一步應該是設定引導引數(struct boot_params,傳統上稱為“零頁面”)。應分配 struct boot_params 的記憶體並將其初始化為全零。然後,應將核心映象偏移量 0x01f1 處的 setup 標頭載入到 struct boot_params 中並進行檢查。setup 標頭的結尾可以計算如下

0x0202 + byte value at offset 0x0201

除了像 16 位引導協議那樣讀取/修改/寫入 struct boot_params 的 setup 標頭之外,引導載入程式還應填寫 struct boot_params 的附加欄位,如零頁面章節中所述。

設定 struct boot_params 後,引導載入程式可以以與 16 位引導協議相同的方式載入 32/64 位核心。

在 32 位引導協議中,透過跳轉到 32 位核心入口點來啟動核心,該入口點是載入的 32/64 位核心的起始地址。

在入口處,CPU 必須處於停用分頁的 32 位保護模式;必須載入一個 GDT,其中包含選擇器 __BOOT_CS(0x10) 和 __BOOT_DS(0x18) 的描述符;這兩個描述符必須是 4G 扁平段;__BOOT_CS 必須具有執行/讀取許可權,並且 __BOOT_DS 必須具有讀取/寫入許可權;CS 必須是 __BOOT_CS,DS、ES、SS 必須是 __BOOT_DS;必須停用中斷;%esi 必須儲存 struct boot_params 的基地址;%ebp、%edi 和 %ebx 必須為零。

1.14. 64 位引導協議

對於具有 64 位 CPU 和 64 位核心的機器,我們可以使用 64 位引導載入程式,並且我們需要 64 位引導協議。

在 64 位引導協議中,載入 Linux 核心的第一步應該是設定引導引數(struct boot_params,傳統上稱為“零頁面”)。struct boot_params 的記憶體可以分配在任何位置(甚至在 4G 以上)並初始化為全零。然後,應將核心映象偏移量 0x01f1 處的 setup 標頭載入到 struct boot_params 中並進行檢查。setup 標頭的結尾可以計算如下

0x0202 + byte value at offset 0x0201

除了像 16 位引導協議那樣讀取/修改/寫入 struct boot_params 的 setup 標頭之外,引導載入程式還應填寫 struct boot_params 的附加欄位,如零頁面章節中所述。

設定 struct boot_params 後,引導載入程式可以以與 16 位引導協議相同的方式載入 64 位核心,但核心可以載入到 4G 以上。

在 64 位引導協議中,透過跳轉到 64 位核心入口點來啟動核心,該入口點是載入的 64 位核心的起始地址加上 0x200。

在入口處,CPU 必須處於啟用分頁的 64 位模式。使用 setup_header.init_size 的範圍從載入的核心和零頁面和命令列緩衝區的起始地址獲取標識對映;必須載入一個 GDT,其中包含選擇器 __BOOT_CS(0x10) 和 __BOOT_DS(0x18) 的描述符;這兩個描述符必須是 4G 扁平段;__BOOT_CS 必須具有執行/讀取許可權,並且 __BOOT_DS 必須具有讀取/寫入許可權;CS 必須是 __BOOT_CS,DS、ES、SS 必須是 __BOOT_DS;必須停用中斷;%rsi 必須儲存 struct boot_params 的基地址。

1.15. EFI 切換協議(已棄用)

此協議允許引導載入程式將初始化推遲到 EFI 引導 stub。引導載入程式需要從引導介質載入核心/initrd 並跳轉到 EFI 切換協議入口點,該入口點是從 startup_{32,64} 開頭偏移 hdr->handover_offset 個位元組的位置。

在涉及節對齊、可執行映象在檔案本身大小之外的記憶體佔用以及可能影響映象作為 EFI 韌體提供的執行上下文中的 PE/COFF 二進位制檔案正確操作的 PE/COFF 標頭的任何其他方面時,引導載入程式必須遵守核心的 PE/COFF 元資料。

切換入口點的函式原型如下所示

void efi_stub_entry(void *handle, efi_system_table_t *table, struct boot_params *bp);

“handle”是 EFI 映象控制代碼,由 EFI 韌體傳遞給引導載入程式,“table”是 EFI 系統表 - 這些是 UEFI 規範第 2.3 節中描述的“切換狀態”的前兩個引數。“bp”是引導載入程式分配的引導引數。

引導載入程式必須填寫 bp 中的以下欄位

- hdr.cmd_line_ptr
- hdr.ramdisk_image (if applicable)
- hdr.ramdisk_size  (if applicable)

所有其他欄位應為零。

注意

EFI 切換協議已棄用,取而代之的是普通的 PE/COFF 入口點,以及基於 LINUX_EFI_INITRD_MEDIA_GUID 的 initrd 載入協議(有關這方面的引導載入程式示例,請參閱 [0]),它消除了 EFI 引導載入程式瞭解 boot_params 的內部表示或對命令列和 ramdisk 在記憶體中的位置或核心映象本身的位置的任何要求/限制的需要。

[0] https://github.com/u-boot/u-boot/commit/ec80b4735a593961fe701cc3a5d717d4739b0fd0