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
バイナリエディタも使えると幅が広がるとのこと
Month: January 2024
コンピュータの構造
### レジスタ
汎用レジスタ、フラグレジスタ、プログラムカウンタ(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で実行していたが、アセンブリの作成が入ってくるのね。
x86-64アセンブラ
.intel_syntax noprefix
hello: .string "hello world\n"
.global _start
_start:
lea rdi,[hello]
call printf
mov rdi, 0
call exit
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
構造体定義だと複雑なことをするのね