vinix (2)

Table of Contents

make ISO

前回vinixのISO作りを途中で諦めてしまっていた。もったいないので、空き時間を 利用して、作成にいそしんでみた。最近は何でもChatGPTに聞けば結論がサッと 得られるようになってるけど、これじゃ途中の苦労がさっぱり伝わってこないので 馬鹿を承知でやってみた次第。

sakae@lu:srcs$ du -sh vinix/
24G     vinix/
sakae@lu:vinix$ ls -ltr
合計 1135352
 :
drwxrwxr-x   6 sakae sakae      4096  3月 16 06:08 util-vinix/
-rwxrwxr-x   1 sakae sakae     57638  3月 16 06:46 jinx*
drwxrwxr-x   2 sakae sakae      4096  3月 17 06:36 recipes/
drwxrwxr-x   8 sakae sakae      4096  3月 22 14:17 kernel/
drwxr-xr-x  46 sakae sakae      4096  3月 22 16:37 builds/
drwxr-xr-x  46 sakae sakae      4096  3月 22 16:37 pkgs/
drwxr-xr-x   8 sakae sakae      4096  3月 22 16:37 sysroot/
drwxr-xr-x 139 sakae sakae     12288  3月 22 16:37 sources/
drwxr-xr-x  12 sakae sakae      4096  3月 22 16:38 host-builds/
drwxr-xr-x  12 sakae sakae      4096  3月 22 16:39 host-pkgs/
-rw-rw-r--   1 sakae sakae 384573440  3月 22 16:39 initramfs.tar
drwxrwxr-x   4 sakae sakae      4096  3月 22 16:39 iso_root/
-rw-rw-r--   1 sakae sakae 389208064  3月 22 16:39 vinix.iso

全体がこんなに膨れるんですねぇ。これだけやるにも都合4時間ぐらいは優に必要かな。 だって、ユーザーランドをコンパイルするのに必要な道具類も作成してますから。 本当の職人さんは手に馴染む道具から自作するってのは、本当だな。

どこかのHPで自慢してた。最近のラズパイはARMの石だけじゃなくてrisc-Vの石も 内蔵してるとか。んでもって、risc-VでLチカしましょですって。アセンブラでゴリゴリ 記述するんかと思って期待したら、C言語ですよ。そんな高級言語を使ってrisc-Vだって。 臍でお茶を沸かすだな。

折角なんで、ISOが作成される過程を見てみる。

sakae@lu:vinix$ make all
make vinix.iso
make[1]: ディレクトリ '/var/my/srcs/vinix' に入ります
./build-support/makeiso.sh
+ rm -rf sysroot
+ set -f
+ ./jinx install sysroot base
  resolving dependencies...
  installing base...
  installing base-files...
  installing kernel...
  installing init...
  installing bash...
  installing binutils...
  :
  installing lz4...
  checking for conflicts...
  synchronising package files to sysroot 'sysroot'...
+ set +f
+ [ -d host-pkgs/limine ]
+ cd sysroot
+ tar cf ../initramfs.tar bin etc lib lib64 root run sbin tmp usr var
+ rm -rf iso_root
+ mkdir -pv iso_root/boot
mkdir: ディレクトリ 'iso_root' を作成しました
mkdir: ディレクトリ 'iso_root/boot' を作成しました
+ cp sysroot/usr/share/vinix/vinix iso_root/boot/
+ cp initramfs.tar iso_root/boot/
+ cp build-support/limine.conf iso_root/boot/
+ cp host-pkgs/limine/usr/local/share/limine/limine-bios.sys iso_root/boot/
+ cp host-pkgs/limine/usr/local/share/limine/limine-bios-cd.bin iso_root/boot/
+ cp host-pkgs/limine/usr/local/share/limine/limine-uefi-cd.bin iso_root/boot/
+ mkdir -pv iso_root/EFI/BOOT
:
mkdir: ディレクトリ 'iso_root/EFI' を作成しました
mkdir: ディレクトリ 'iso_root/EFI/BOOT' を作成しました
+ cp host-pkgs/limine/usr/local/share/limine/BOOTIA32.EFI host-pkgs/limine/usr/local/share/limine/BOOTX64.EFI iso_root/EFI/BOOT
+ xorriso -as mkisofs -R -r -J -b boot/limine-bios-cd.bin -no-emul-boot -boot-load-size 4 -boot-info-table -hfsplus -apm-block-
size 2048 --efi-boot boot/limine-uefi-cd.bin -efi-boot-part --efi-boot-image --protective-msdos-label iso_root -o vinix.iso
xorriso 1.5.6 : RockRidge filesystem manipulator, libburnia project.
:

sysrootってdirがゲストOSの/になるんだな。そこに向けてユーザーランドをインストール。 それから、/ をinitramfs.tarにまとめる。次は、ISOのdirを作成して、そこに必要な 物をぶち込む。ISOファイルシステムなんで、ローダーであるlimeineがハンドリング 出来るとな。最後の工程は、xorrisoとか言うアプリでISOに焼くとな。

How to debug kernel

見様見真似で、カーネルのデバッグってか閲覧はできるかな。

sakae@lu:vinix$ rm vinix.iso
sakae@lu:vinix$ rm -rf pkgs/kernel/
sakae@lu:vinix$ rm -rf builds/kernel/
sakae@lu:vinix$ cp builds/kernel/bin/vinix pkgs/kernel/usr/share/vinix/
sakae@lu:vinix$ make debug

肝は、stripされる前のvinixを使う事。

無理してgdbしてみると …

sakae@lu:kernel$ gdb bin/vinix
 :       ;; waiting long time. why???
(gdb) bt
#0  0xffffffff8001fc0b in sched__await () at obj/blob.c:14058
#1  sched__scheduler_isr (_d1=<optimized out>, gpr_state=0xffff8000012c2f48)
    at obj/blob.c:13595
#2  0xffffffff8005504f in interrupt_thunk_34 () at asm/int_thunks_asm.S:98
#3  0x0000000000000030 in ?? ()
#4  0x0000000000000030 in ?? ()
#5  0x00000000013090c0 in ?? ()
#6  0x0000000000000000 in ?? ()

userland__syscall_fork にBPを置いて、適当にdateをしてみた。

(gdb) bt
#0  userland__syscall_fork (gpr_state=0xffff80002d4e9f48) at obj/blob.c:12520
#1  0xffffffff80000203 in syscall__syscall_entry () at obj/blob.c:11346
#2  0x000000000000003b in ?? ()
(gdb) l
12515           flanterm_write(flanterm_ctx, s, len);
12516           klock__Lock_release(&terminal_print_lock);
12517   }
12518   proc__Thread* proc__current_thread(void) {
12519           proc__Thread* ret = ((proc__Thread*)(((void*)0)));
12520           __asm__ volatile (
12521                   "mov %%gs:8, %[ret];"
12522                   : [ret] "=r" (ret)
12523           );
12524           return ret;
(gdb) up
#1  0xffffffff80000203 in syscall__syscall_entry () at obj/blob.c:11346
11346           __asm__ volatile (

どうやらkernel/main.v のコンパイル結果が、obj/blob.cに展開されるみたい。 vlangとは似ても似つかない形になってて、追跡するには不向きだな。

make debug

これをやっておくと、下記の様に、システムコール(っぽい奴)がロギングされる様になる。 この機能を使って下さいって事だな。

sakae@lu:vinix$ cat jinx-config-debug
. ${base_dir}/jinx-config

PROD=false
TARGET_CFLAGS="$TARGET_CFLAGS -g"

Topに有るGNUMakefileから辿っていくと、これで有効になるんだけど、実際には何処でフックしてるんだろう?

kernel/modules/dev/serial/serial.v

__global (
        com1_lock klock.Lock // Lock for COM1 kernel debug reporting.
)

これっぽいけど、上のdebug設定と関連が取れないんだよなあ。いや、

static void _putchar(int character, void *extra_arg) {
    (void)character;
    (void)extra_arg;
#ifndef PROD
    dev__serial__out(character);
#endif

こちらはc/printf.cに有るんだけど、より直接的なコンパイル時の決定事項 だよな。こんな些細な事に拘っていないで、、、

sakae@lu:vinix$ make run-kvm
qemu-system-x86_64 -enable-kvm -cpu host -M q35,smm=off -m 4G -cdrom vinix.iso -serial stdio -smp 1 -s
pmm: Higher half direct map at: 0xffff800000000000
pmm: Memory map entry 0: 0x1000->0x57000  0x5
pmm: Memory map entry 1: 0x58000->0x47000  0x0
:
pmm: Bitmap size: 196608
pmm: Free pages: 952686
vmm: Kernel physical base: 0x7fb5f000
vmm: Kernel virtual base: 0xffffffff80000000
vmm: Kernel: Mapping 0x7fb5f000 to 0xffffffff80000000, length: 0x5d000
vmm: Kernel: Mapping 0x7fbbc000 to 0xffffffff8005d000, length: 0x11000
vmm: Kernel: Mapping 0x7fbcd000 to 0xffffffff8006e000, length: 0xb4000
acpi: Revision:  0
:
starting uACPI, version 2.0.0
RSDP 0x00000000000F52A0 00000000 v00 (BOCHS )
RSDT 0x000000007FFE22FC 00000038 v01 (BOCHS  BXPC    )
DSDT 0x000000007FFE0040 000020B4 v01 (BOCHS  BXPC    )
FACP 0x000000007FFE20F4 000000F4 v03 (BOCHS  BXPC    )
APIC 0x000000007FFE21E8 00000078 v03 (BOCHS  BXPC    )
HPET 0x000000007FFE2260 00000038 v01 (BOCHS  BXPC    )
MCFG 0x000000007FFE2298 0000003C v01 (BOCHS  BXPC    )
WAET 0x000000007FFE22D4 00000028 v01 (BOCHS  BXPC    )
FACS 0x000000007FFE0000 00000040
successfully loaded 1 AML blob, 1705 ops in 2ms (avg 799137/s)
namespace initialization done in 2ms: 36 devices, 0 thermal zones
:
vfs: Mounted tmpfs to `/`
vfs: Mounted devtmpfs to `/dev`
initramfs: Address: 0xffff800168ef0000
initramfs: Size:    386990080
initramfs: Unpacking...
initramfs: Done.
urandom: rdrand available
urandom: rdseed available
:
[]: set_fs_base(0x404254a0)
[]: returning
:
/sbin/init[1]: mmap(0x0, 0x80000, 0x700000020, -1, 0)
/sbin/init[1]: returning
:
/sbin/init[1]: chdir(/root)
/sbin/init[1]: returning
:
/sbin/init[1]: fork()
/sbin/init[1]: returning
:
/sbin/init[1]: waitpid(2, 0x6fffffffd38, 0)
:
/sbin/init[1][2]: execve(/bin/sh, [omit], [omit])
 :   ;; many syscall
/bin/sh[2]: execve(/usr/bin/bash, [omit], [omit])
:
/usr/bin/bash[2]: mmap(0x0, 0x80000, 0x700000020, -1, 0)
:
/usr/bin/bash[2][3]: execve(/usr/bin/date, [omit], [omit])
/usr/bin/date[3]: mmap(0x0, 0x80000, 0x700000020, -1, 0)
:
/usr/bin/date[3]: exit(0)
:
/usr/bin/bash[2]: write(2, 0x80000778008, 0x28)
/usr/bin/bash[2]: returning

/usr/bin/bash[2]: ppoll(0x8000067fca0, 1, 0x0, 0x41053c40)
Polling on 1 FDs
fdnum 0, events 1

[]内の数字はpidっぽいな。何だかstraceみたいで面白い。sh -> bash な2段構えって vinix特有なのかな? 最後はbashからの入力待ちで、ppollが発っせられたな。

kernel

いよいよソースを見ていく。

外観検査

matzさんが、まずは外観を眺めてokって吠えていたから。

sakae@lu:kernel$ ls
GNUmakefile  c/           freestnd-c-hdrs/  linker.ld  modules/           v.mod
asm/         cc-runtime/  get-deps*         main.v     uacpi-repository/

kernelは完全に自給自足の生活を強いられるんで、runtimeを同梱するとな。c/ の中は ほとんどがヘッダーファイルだった。uacpiは面倒なAML言語でのパワーマネジメントを サポートするコード一式だ。面倒をユーザーに強いる頑強。 後は、main.vとその仲間達のクラスターであるmodulesって構成。

SECTIONS
{
    /* We want to be placed in the topmost 2GiB of the address space, for optimisations */
    /* and because that is what the Limine spec mandates. */
    /* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
    /* that is the beginning of the region. */
    . = 0xffffffff80000000;

    text_start = .;

    .text : {
        *(.text .text.*)
    } :text
    :

kernel独自のリンカー・スクリプトが用意されてて注意書きがしてあった。ブートローダー の事も考えてあげましょうってね。ちなみに現役のカーネルを検査すると、こんなだった。

sakae@lu:kernel$ LANG=C readelf -h bin/vinix
ELF Header:
  Entry point address:               0xffffffff80038110

sakae@lu:kernel$ nm bin/vinix | grep ffffffff80038110
ffffffff80038110 T main__kmain

main.v

呼出の階層図

main()
   kmain()
        memory.pmm_init()  // Initialize the memory allocator.
        C._vinit(0, 0)     // Call Vinit to initialise the runtime
        gdt.initialise()   // Initialize the earliest arch structures.
        idt.initialise()
        isr.initialise()
        term.initialise()   // Init terminal
        serial.early_initialise()
        memory.vmm_init()
        acpi.initialise()   // ACPI init
        hpet.initialise()
        pci.initialise()
        smp.initialise()
        time.initialise()
        sched.initialise()
        spawn kmain_thread()
            term.framebuffer_init()
            table.init_syscall_table()
            socket.initialise()
            pipe.initialise()
            futex.initialise()
            fs.initialise()
            fs.mount(vfs_root, '', '/', 'tmpfs') or {}
            fs.create(vfs_root, '/dev', 0o644 | stat.ifdir) or {}
            fs.mount(vfs_root, '', '/dev', 'devtmpfs') or {}
            initramfs.initialise()
            streams.initialise()
            random.initialise()
            fbdev.initialise()
            fbdev.register_driver(simple.get_driver())
            console.initialise()
            serial.initialise()
            mouse.initialise()
            hda.initialize()
            userland.start_program(false, vfs_root, '/sbin/init', ['/sbin/init'], [], '/dev/console',
                '/dev/console', '/dev/console') or { panic('Could not start init process') }
            sched.dequeue_and_die()
        sched.await()

kmainの中でハード関係のイニシャライズが実行されてって舞台の準備 が実施される。そして、 kmain_thread で、ソフト的な準備をする。 最後の所で、/sbin/initを実行して、ユーザーランドのアプリbashが 起動するって流れだな。

Limine

Limine から辿れるFAQによると、サポートするファイルシステムは、FAT*, ISO9660 だけだよ。grub2みたいに、多彩なサポートはしないよとな。シンプルが一番です。

The Limine Boot Protocol 一番大事なのはこれ。確かに色々な石の対応が説明されてる。今回はx86-64の所を かいつまんで見ればいいのだな。

2025-01-20にやった risc-Vの1000行OS に通じるものが有るな。

linime: Loading executable 'boot():/boot/vinix'...
linime: Loading module 'boot():/boot/initramfs.ar'...

これが起動直後のメッセージだ。confに記述された情報を元にロードしてるな。

今度は起動メニューからEditorを開いてモジュール名を変更してみた。そしてF10 キーで起動させると、

PANIC: linime: Faild to open module with path 'boot:/boot/fs.tar' Is the path collect?
Stacktrace:
 [0x11c3c] <panic+0x76>
 [0x2b152] <limine_load+0x2412>
 [0x364fb] <boot+0x6b>
 [0x3749c] <_menu+0xe9c>
End of trace. Press a key to return to editor.

無い袖は振れませんとばかりにパニくった。これでlinimeへダイビングする時の 入口が手にはいりましたねぇ。

initramfs.tar

このtar玉をカーネルはどうやって展開してるんだろう? 普通に考えたらカーネルが ロードされた時にtar玉もローダーによって展開されてる。そりゃ浅はかだよ。 ファイルシステムを知ってるのはカーネルなんだから、展開するのはカーネルの仕事だ。 だったら、カーネルはtar玉の名前だけでも知る必要が有る。そんなの簡単。os.argv[1] とかで得られるじゃん。待て、まだ浅はかだぞ。環境変数を誰が用意してくれるねん?

ここは絶対に自主独立じゃなか、あかん。孤高のカーネルを強いられるんだ。ってな訳で モジュールinitramfsですよ。そこに上記の疑問への回答がつまっているはず。 もう一度debug-logを再掲する。

initramfs: Address: 0xffff800168ef0000
initramfs: Size:    386990080
initramfs: Unpacking...
initramfs: Done.

コードと対面する。

@[_linker_section: '.requests']
@[cinit]
__global (
        volatile module_req = limine.LimineModuleRequest{
                response: unsafe { nil }
        }
)

カーネルファイルの一部にローダーであるlimineとの通信エリアが 用意されてるんだな。それを利用して、

pub fn initialise() {
        if module_req.response == unsafe { nil } {
                panic('Modules bootloader response missing')
        }

        if module_req.response.module_count < 1 {
                panic('No initramfs')
        }

        mut modules := module_req.response.modules

        initramfs_begin := unsafe { modules[0].address }
        initramfs_size := unsafe { modules[0].size }

        println('initramfs: Address: 0x${voidptr(initramfs_begin):x}')
        println('initramfs: Size:    ${u32(initramfs_size):u}')

        print('initramfs: Unpacking...')

	;; シグネチャ(utar)を確認後に、tar玉を展開する

        print('\ninitramfs: Done.\n')	

linimeによってtar玉は(勿論カーネルも)最高位のメモリーにロードされる。その先頭アドレスとサイズを入手 する訳だな。メモリーに載ってしまえば(ISO)ファイルシステムの理解なんて不要だ。

tar x

tar xf の間違いじゃないからね! (fはファイル指定のオプション) メモリーの上位に 有るデータをカーネルの管理域(ramfs)にコピーする。

下記は、dirや普通のファイルを作成してる部分だ。

match unsafe { USTARFileType(current_header.filetype) } {
        .gnu_long_path {
                // limit for safety
                if size >= 65536 {
                        panic('initramfs: long file name exceeds 65536 characters.')
                }

                name_override = unsafe { tos(voidptr(u64(current_header) + 512), int(size)) }
        }
        .directory {
                fs.create(vfs_root, name, u32(mode | stat.ifdir)) or {
                        panic('initramfs: failed to create directory ${name}')
                }
        }
        .regular_file {
                new_node := fs.create(vfs_root, name, u32(mode | stat.ifreg)) or {
                        panic('initramfs: failed to create file ${name}')
                }
                mut new_resource := new_node.resource
                buf := voidptr(u64(current_header) + 512)
                new_resource.write(0, buf, 0, size) or {
                        panic('initramfs: failed to write file ${name}')
                }
        }

これで安心。つぎ何を見よう? 際限が無くなりそうなので、一旦終了する。

README

栃木県の宇都宮市は餃子だけじゃないよ。カクテルの街って、どこかで聞いたような。 オリオンは静かに詠う なんて小説を読んだんだけど、百人一首のまち でもあるらしい。知らなかったなあ。知らないと言えば、この小説の根幹である 競技カルタの事も良く知らなかった。文中で説明が出てくるんだけど、まとめって ことで。

はじめての競技かるた

競技かるたのルールについて説明

百人一首の解説

何となく、補完を思い出しちゃったぞ。


This year's Index

Home