シリーズ一覧
| Part | タイトル | 内容 |
|---|---|---|
| Part1 | 令和にCでOS書く狂人の記録 | VGA出力 |
| Part2 | アセンブリとCの橋渡し | 本記事 |
| Part3 | メモリ管理とmalloc | 動的メモリ |
| Part4 | printfを自作する | フォーマット出力 |
| Part5 | Rustで書き直したくなった | 移行検討 |
はじめに
前回、カーネルが動くようになった。でも「なんで動くの?」って部分、ちゃんと理解してる?
今回はリンカスクリプトを深掘りする。アセンブリとCがどう繋がってるのか、メモリ上でどう配置されてるのか、全部見ていく。
リンカって何してるの?
コンパイルの流れを整理しよう:
ソースコード → コンパイラ → オブジェクトファイル → リンカ → 実行ファイル
boot.asm → NASM → boot.o ─┐
├──→ LD ──→ c-os.bin
kernel.c → GCC → kernel.o ─┘
リンカがやること:
-
シンボル解決:
extern kernel_main→ 実際のアドレス -
セクション配置:
.text,.data,.bssをどこに置くか - 再配置: 相対アドレスを絶対アドレスに変換
オブジェクトファイルの中身を見る
$ objdump -h boot.o
boot.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000018 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .multiboot 0000000c 00000000 00000000 0000004c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .bss 00004000 00000000 00000000 00000058 2**4
ALLOC
この時点では VMA(仮想アドレス)が全部0。リンカが最終的なアドレスを決める。
リンカスクリプト詳解
/* エントリポイントを指定 */
ENTRY(_start)
SECTIONS
{
/* カーネルの開始アドレス */
. = 1M;
なぜ1MB?
x86のメモリマップ(リアルモードからの名残):
0x00000000 - 0x000003FF : 割り込みベクタテーブル
0x00000400 - 0x000004FF : BIOSデータ領域
0x00000500 - 0x00007BFF : 使用可能(でも小さい)
0x00007C00 - 0x00007DFF : ブートセクタ
0x00007E00 - 0x0009FFFF : 使用可能
0x000A0000 - 0x000BFFFF : VGAメモリ
0x000C0000 - 0x000FFFFF : BIOS ROM等
0x00100000 - ... : 拡張メモリ(1MB以降)← ここにカーネル!
1MB未満はBIOSやVGAが使ってるから、安全に使える1MB以降にカーネルを置く。
セクションの配置
.text BLOCK(4K) : ALIGN(4K)
{
*(.multiboot) /* 最初にMultibootヘッダ */
*(.text) /* 次にコード */
}
*(.multiboot) を先頭に置く理由:
GRUBはカーネルの先頭8KB以内にMultibootヘッダがあることを期待してる。だから .text の一番最初に配置。
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata) /* 読み取り専用データ(文字列リテラルなど) */
}
.data BLOCK(4K) : ALIGN(4K)
{
*(.data) /* 初期化済みグローバル変数 */
}
.bss BLOCK(4K) : ALIGN(4K)
{
*(COMMON) /* 共通シンボル */
*(.bss) /* 未初期化グローバル変数 */
}
}
BLOCK(4K)とALIGN(4K)の違い
-
ALIGN(4K): セクションの開始アドレスを4KB境界に -
BLOCK(4K): セクション内のアライメント
ページング(仮想メモリ)を有効にする予定なら、4KB境界で揃えておくと後が楽。
シンボルの参照
アセンブリからCの関数を呼ぶ:
extern kernel_main ; 外部シンボルを宣言
call kernel_main ; 呼び出し
Cからアセンブリのシンボルにアクセスもできる:
// リンカスクリプトで定義したシンボル
extern char _kernel_start[];
extern char _kernel_end[];
void print_kernel_size(void) {
size_t size = (size_t)_kernel_end - (size_t)_kernel_start;
// ...
}
リンカスクリプトでシンボルを定義:
SECTIONS
{
. = 1M;
_kernel_start = .; /* 現在アドレスをシンボルに */
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
_kernel_end = .; /* カーネル終端 */
}
呼び出し規約
32ビットx86のCdecl呼び出し規約:
引数: スタックに右から左へpush
戻り値: EAXレジスタ
呼び出し元がスタックをクリーンアップ
例:int add(int a, int b) を呼ぶとき:
push 20 ; 第2引数
push 10 ; 第1引数
call add ; 関数呼び出し
add esp, 8 ; スタックをクリーンアップ(引数2つ × 4バイト)
; 結果は EAX に入ってる
実践:Cからアセンブリ関数を呼ぶ
I/Oポートへのアクセスはアセンブリが必要:
global outb
global inb
; void outb(unsigned short port, unsigned char data)
outb:
mov dx, [esp + 4] ; port
mov al, [esp + 8] ; data
out dx, al
ret
; unsigned char inb(unsigned short port)
inb:
mov dx, [esp + 4] ; port
in al, dx
ret
C側で宣言:
#ifndef IO_H
#define IO_H
void outb(unsigned short port, unsigned char data);
unsigned char inb(unsigned short port);
#endif
使用例(キーボードから読み取り):
#include "io.h"
#define KEYBOARD_DATA_PORT 0x60
char read_key(void) {
return inb(KEYBOARD_DATA_PORT);
}
リンク後の確認
$ objdump -h c-os.bin
c-os.bin: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000xxx 00100000 00100000 00001000 2**12
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000xxx 00101000 00101000 00002000 2**12
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000xxx 00102000 00102000 00003000 2**12
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00004xxx 00103000 00103000 00004000 2**12
ALLOC
見て!VMAが 0x00100000(1MB)から始まってる。リンカスクリプトどおり。
nm でシンボルを確認
$ nm c-os.bin
00100000 T _start
00100018 T kernel_main
00100xxx T print
00100xxx T putchar
00100xxx T clear_screen
00103xxx B cursor_x
00103xxx B cursor_y
00103xxx B vga_buffer
-
T: テキストセクション(コード) -
B: BSSセクション(未初期化データ)
よくあるミス
1. Multibootヘッダが見つからない
error: no multiboot header found
原因:.multiboot セクションが先頭8KB以内にない
対策:リンカスクリプトで .text の最初に配置
2. undefined reference
undefined reference to `kernel_main'
原因:
- Cの関数名に
_が付いてない(古いコンパイラ) - 単純にスペルミス
対策:nm でシンボル名を確認
3. セクションが変なアドレスに
原因:リンカスクリプトの . = 1M; を忘れた
対策:objdump -h でVMAを確認
まとめ
リンカスクリプトは「どこに何を置くか」を決める設計図:
-
エントリポイント:
ENTRY(_start) -
開始アドレス:
. = 1M; -
セクション順序:
.multiboot→.text→.rodata→.data→.bss -
シンボル定義:
_kernel_start = .;
次回Part3では、メモリ管理を実装する。malloc と free を自作するよ。