4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

アセンブリとCの橋渡し、リンカスクリプト編 Part2

Last updated at Posted at 2025-12-12

シリーズ一覧

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 ─┘

リンカがやること:

  1. シンボル解決: extern kernel_main → 実際のアドレス
  2. セクション配置: .text, .data, .bss をどこに置くか
  3. 再配置: 相対アドレスを絶対アドレスに変換

オブジェクトファイルの中身を見る

$ 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。リンカが最終的なアドレスを決める。

リンカスクリプト詳解

linker.ld
/* エントリポイントを指定 */
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ポートへのアクセスはアセンブリが必要:

boot/io.asm
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側で宣言:

src/io.h
#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では、メモリ管理を実装する。mallocfree を自作するよ。


次回: Cでメモリ管理、free忘れてリーク祭り Part3

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?