初めてのOS

EB4E9048454C4C4F49504C0002010100
02E000400BF009001200020000000000
400B0000000029FFFFFFFF48454C4C4F
2D4F5320202046415431322020200000
00000000000000000000000000000000
B800008ED0BC007C8ED88EC0BE747C8A
0483C6013C007409B40EBB0F00CD10EB
EEF4EBFD0A0A68656C6C6F2C20776F72
6C640A00000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
·N·HELLOIPL·····
···@············
@·····)····HELLO
-OS···FAT12·····
················
·······|·····t|·
····<·t········· ······hello,·wor ld·············· 上記バイナリでPCを起動するとhello worldが表示される。 最も簡単なOS バイナリエディタも使えると幅が広がるとのこと

コンピュータの構造

### レジスタ
汎用レジスタ、フラグレジスタ、プログラムカウンタ(EIP)、スタックポインタ(ESP)、制御レジスタ(EFLAGS)がある
フラグレジスタは算術結果を保存
プログラムカウンタは次のどのアドレスから読み込めば良いかを示す
スタックポインタはスタックと呼ばれるメモリ上に確保された記憶領域のアドレスを表す

### 制御装置
フェッチ、デコード(機械語の解読)、指示、プログラムカウンタを更新

### リトルエンディアン
リトルエンディアンはビットにかかわらずアドレスから値を呼び出すことができる
ビッグエンディアンはビットごとに調整しなければならない

アセンブラ・コンパイラ基礎

逆アセンブルとは機械語をアセンブラに変換すること
objdump は一つ以上のオブジェクトファイルについて情報を表示

$ objdump -d -M intel /bin/ls

Disassembly of section .fini:

0000000000016200 <.fini>:
16200: d503201f nop
16204: a9bf7bfd stp x29, x30, [sp, #-16]!
16208: 910003fd mov x29, sp
1620c: a8c17bfd ldp x29, x30, [sp], #16
16210: d65f03c0 ret

一番左の 16200: は機械語が入っているメモリ
d503201f は実際の機械語

int main() {
    return 42;
}

$ cc -o test1 test1.c
$ ./test1
$ echo $?
42

$?をechoで終了コマンドを出力

test2.s

.intel_syntax noprefix
.globl main
main:
    mov rax, 42
    ret

### 関数呼び出し
関数で元々実行していたアドレスをリターンアドレスという
リターンアドレスはメモリのスタック上に保存される
スタックトップを保持している記憶領域をスタックポインタという

int plus(int x, int y) {
	return x + y;
}

int main() {
	return plus(3, 4);
}

第一引数はRDIレジスタ、第二引数はRSIレジスタに入れる
x86-64は通常2つのレジスタしか受け取らない
関数からの返り値はRAXに入れるとなっている
callとretは対になる命令

.intel_sytax noprefix
.global plus, main

plus:
	add rsi, rdi
	mov rax, rsi
	ret

main:
	mov rdi, 3
	mov rsi, 4
	call plus
	ret

arm64のスタックポインタ(SP)へのpushとpop

.text
.global _start
_start:
mov     x2,  #13
adr     x1,  msg
str     x2, [sp, #-16]!
str     x1, [sp, #-16]!
ldr     x1, [sp], #16
ldr     x2, [sp], #16
mov     x0,  #1 
mov     x8,  #64
svc     #0
mov     x0,  xzr
mov     x8,  #93
svc     #0 
msg:
.asciz "hello world"

$ as -o source.o source.s
$ ld -o source source.o
$ ./source
hello world

スタックポインタは16バイトで整列されていることを要求するためずらす
svc #0 はx0からx8のレジスタの値を

arm64のcompiler実装

source

print("hello world")

compiler.js

var {exec, write, show, error} = require("./utils.js");
var lexer = require("./lexer.js");
var parser = require("./parser-comp.js");
var genasm = require("./genasm.js");

var source = read("source.3");

var tokens = lexer(source);

show("処理前tokens =", tokens);

parser(tokens);

console.log("-------------");

exec("as source.s -o source.o");

exec("ld -lc --dynamic-linker /lib64/ld-linux-x86-64.so.2 -o exec source.o");

console.log(exec("./exec"));

parser-comp.js

module.exports = parser;
var {write,expect,accept,show,error} = require("./utils.js");
var tokens;

function parser(t) {
    tokens = t;
    return callprint();
}

function callprint(){
    if(tokens.length==0) return;

    expect(tokens,"print");

    expect(tokens,"(");

    var msg = tokens.shift();
    var codes = [];

    codes.push(".text");
    codes.push(".global _start");
    codes.push("_start:");

    codes.push("mov     x2,  #13");
    codes.push("adr     x1,  msg");

    codes.push("mov     x0,  #1 ");
    codes.push("mov     x8,  #64");

    codes.push("svc     #0");
    codes.push("mov     x0,  xzr");
    codes.push("mov     x8,  #93");
    codes.push("svc     #0 ");
    codes.push("msg:");
    codes.push(".asciz " + msg);
    codes.push("\n");

    var asm = codes.join("\n")+"\n";

    write("source.s",asm);

    expect(tokens,")");
}

utils.js

module.exports = {exec, write, read, show, error, accept, expect}

function exec(cmd) {
    return require('child_process').execSync(cmd, {encoding:"utf8"});
}

function write(filename, data){
    require('fs').writeFileSync(filename,data);
}

parser-comp.jsのcallfunctionでarm64用のassemblyを書いて保存する。
assemblerの実行は別に分ける。
interpreterの方はrun.jsで実行していたが、アセンブリの作成が入ってくるのね。

cポインタとassembler

#include <stdio.h>

int main(void) {

    int a = 75;
    long addrA = (long)&a;
    printf("%ld\n", addrA);
    return 0;
}
|main|  PROC
|$LN3|
        stp         fp,lr,[sp,#-0x20]!
        mov         fp,sp
        mov         w8,#0x4B
        str         w8,[sp,#0x10]
        add         x8,sp,#0x10
        mov         w8,w8
        str         w8,[sp,#0x14]
        ldr         w1,[sp,#0x14]
        adrp        x8,|$SG4983|
        add         x0,x8,|$SG4983|
        bl          printf
        mov         w0,#0
        ldp         fp,lr,[sp],#0x20
        ret

str w8,[sp,#0x14] としてるのは、先頭アドレスを保存しているっぽいね。

ポインタは

int main(void) {

    int a = 75;
    int* addrA = (void*)&a;
    printf("%p\n", addrA);
    return 0;
}
|main|  PROC
|$LN3|
        stp         fp,lr,[sp,#-0x20]!
        mov         fp,sp
        mov         w8,#0x4B
        str         w8,[sp,#0x10]
        add         x8,sp,#0x10
        str         x8,[sp,#0x18]
        ldr         x1,[sp,#0x18]
        adrp        x8,|$SG4983|
        add         x0,x8,|$SG4983|
        bl          printf
        mov         w0,#0
        ldp         fp,lr,[sp],#0x20
        ret

アドレス&aとポインタ*addrAはそれぞれメモリ上別の領域に格納されて、printfでは*addrAのアドレスが呼ばれていますね。

Cで困ったらコンパイルした後のアッセンブラを見れば良いのかな。

cの関数とassembler

#include <stdio.h>

void hello(void){
    printf("hello\n");
}

int main(void) {

    hello();
    return 0;
}
|hello| PROC
|$LN3|
        stp         fp,lr,[sp,#-0x10]!
        mov         fp,sp
        adrp        x8,|$SG4980|
        add         x0,x8,|$SG4980|
        bl          printf
        ldp         fp,lr,[sp],#0x10
        ret

        ENDP  ; |hello|

|main|  PROC
|$LN3|
        stp         fp,lr,[sp,#-0x10]!
        mov         fp,sp
        bl          hello
        mov         w0,#0
        ldp         fp,lr,[sp],#0x10
        ret

        ENDP  ; |main|

関数の場合は、hello()はassembler上ではmainと分離され、bl helloで呼び出すようになっている。すなわち、関数ごとにassembleすれば良いのね。なるほど

cの配列とassembler

#include <stdio.h>

int main(void) {

    int nums[10];
    nums[2] = 15;
    printf("%d\n", nums[2]);
    return 0;
}
|main|  PROC
|$LN3|
        stp         fp,lr,[sp,#-0x10]!
        mov         fp,sp
        bl          __security_push_cookie
        sub         sp,sp,#0x20
        mov         x8,#4
        mov         x9,#2
        mul         x9,x8,x9
        mov         x8,sp
        add         x9,x8,x9
        mov         w8,#0xF
        str         w8,[x9]
        mov         x8,#4
        mov         x9,#2
        mul         x9,x8,x9
        mov         x8,sp
        add         x8,x8,x9
        ldr         w1,[x8]
        adrp        x8,|$SG4981|
        add         x0,x8,|$SG4981|
        bl          printf
        mov         w0,#0
        add         sp,sp,#0x20
        bl          __security_pop_cookie
        ldp         fp,lr,[sp],#0x10
        ret

        ENDP  ; |main|

値を変えながらassembleしてみると、mov x8,#4のところはnum[2], [10], num[15]でも変わらないのね。mul x9, x8、x9の挙動がいまいちわからんが

cの構造体とassembler

#include <stdio.h>

typedef char String[1024];

int main(void) {

    typedef struct {
        String name;
        int hp;
        int attack;
    } Monster;

    Monster seiryu = {"青竜", 100, 15};
    printf(seiryu.name, seiryu.hp, seiryu.attack);
    return 0;
}
|$LN3|
        stp         fp,lr,[sp,#-0x10]!
        mov         fp,sp
        bl          __security_push_cookie
        sub         sp,sp,#0x410
        add         x8,sp,#8
        str         x8,[sp]
        ldr         x9,[sp]
        adrp        x8,|$SG4988|
        add         x8,x8,|$SG4988|
        ldr         w10,[x8]
        str         w10,[x9]
        ldrsh       w10,[x8,#4]
        strh        w10,[x9,#4]
        ldrsb       w8,[x8,#6]
        strb        w8,[x9,#6]
        add         x0,sp,#0xF
        mov         x2,#0x3F9
        mov         w1,#0
        bl          memset
        mov         w8,#0x64
        str         w8,[sp,#0x408]
        mov         w8,#0xF
        str         w8,[sp,#0x40C]
        mov         w0,#0
        add         sp,sp,#0x410
        bl          __security_pop_cookie
        ldp         fp,lr,[sp],#0x10
        ret

        ENDP  ; |main|

もう少しシンプルにする

int main(void) {

    typedef struct {
        String name;
    } Monster;

    Monster seiryu = {"青竜"};
    // printf(seiryu.name, seiryu.hp, seiryu.attack);
    return 0;
}
|main|  PROC
|$LN3|
        stp         fp,lr,[sp,#-0x10]!
        mov         fp,sp
        bl          __security_push_cookie
        sub         sp,sp,#0x400
        add         x8,sp,#8
        str         x8,[sp]
        ldr         x9,[sp]
        adrp        x8,|$SG4986|
        add         x8,x8,|$SG4986|
        ldr         w10,[x8]
        str         w10,[x9]
        ldrsh       w10,[x8,#4]
        strh        w10,[x9,#4]
        ldrsb       w8,[x8,#6]
        strb        w8,[x9,#6]
        add         x0,sp,#0xF
        mov         x2,#0x3F9
        mov         w1,#0
        bl          memset
        mov         w0,#0
        add         sp,sp,#0x400
        bl          __security_pop_cookie
        ldp         fp,lr,[sp],#0x10
        ret

x8, x9に入れたものをw10にstr, ldr
ldrshは16ビットにstr

構造体定義だと複雑なことをするのね