.intel_syntax noprefix hello: .string "hello world\n" .global _start _start: lea rdi,[hello] call printf mov rdi, 0 call exit
Category: assembler
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
構造体定義だと複雑なことをするのね
cのfor文、while文とassembler
### for
int main(void) { for(int i = 0; i < 5; i++) { printf("hello\n"); } return 0; }
stp fp,lr,[sp,#-0x20]! mov fp,sp mov w8,#0 str w8,[sp,#0x10] b |$LN4@main| |$LN2@main| ldr w8,[sp,#0x10] add w8,w8,#1 str w8,[sp,#0x10] |$LN4@main| ldr w8,[sp,#0x10] cmp w8,#5 bge |$LN3@main| adrp x8,|$SG4984| add x0,x8,|$SG4984| bl printf b |$LN2@main| |$LN3@main| mov w0,#0 ldp fp,lr,[sp],#0x20 ret
for文は、その記述の通り、0でmov, strして、その後ad w8 w8 #1として、cmpで5より小さいか判定してprintfしていますね。c言語だとi++より先にi<10と書いているが、assemblerではi++が上に来るのが興味深い ### while [code] #include <stdio.h> int main(void) { int count = 0; while (count < 5) { printf("hello\n"); count++; } return 0; } [/code] [code] |main| PROC |$LN5| stp fp,lr,[sp,#-0x20]! mov fp,sp mov w8,#0 str w8,[sp,#0x10] |$LN2@main| ldr w8,[sp,#0x10] cmp w8,#5 bge |$LN3@main| adrp x8,|$SG4983| add x0,x8,|$SG4983| bl printf ldr w8,[sp,#0x10] add w8,w8,#1 str w8,[sp,#0x10] b |$LN2@main| |$LN3@main| mov w0,#0 ldp fp,lr,[sp],#0x20 ret [/code] whileの場合も内部処理的にはcmpして、そのまま処理するか、bgeでjumpするかを記述しており、forと似たような処理となっている。 compilerを作る際には、ASTで条件式に応じてラベル出力まで出力しないといけないのか。中間言語生成のところが結構考えないといけないのかもしれない。 アセンブラ自体は単純で、高級言語の予約語がそれぞれ機能的になっていることがわかる。
cのif文とassembler
#include <stdio.h> #include <stdbool.h> int main(void) { bool tenki = true; if(tenki == true) printf("晴です\n"); else printf("雨です\n"); return 0; }
|main| PROC |$LN5| stp fp,lr,[sp,#-0x20]! mov fp,sp mov w8,#1 strb w8,[sp,#0x10] ldrb w8,[sp,#0x10] mov w8,w8 cmp w8,#1 bne |$LN2@main| adrp x8,|$SG4984| add x0,x8,|$SG4984| bl printf b |$LN3@main| |$LN2@main| adrp x8,|$SG4985| add x0,x8,|$SG4985| bl printf |$LN3@main| mov w0,#0 ldp fp,lr,[sp],#0x20 ret
cmp : レジスタからレジスタ、またはレジスタから定数の減算を行って、演算結果に対応した条件フラグを設定
-> compareか?
bne: bはジャンプなので、neで異なる場合は$LN2@mainにjumpとなっていることがわかりますね。
条件分岐でelseの場合はニーモニックにlabelをつけて、そこへjumpするように出力していることがわかります。
cの文字列とassembler
#include <stdio.h> typedef char String[1024]; int main(void) { String name = "ドラゴン"; printf("私は%sです\n", name); 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,|$SG4982| add x8,x8,|$SG4982| ldr x10,[x8] str x10,[x9] ldr w10,[x8,#8] str w10,[x9,#8] ldrsb w8,[x8,#0xC] strb w8,[x9,#0xC] add x0,sp,#0x15 mov x2,#0x3F3 mov w1,#0 bl memset add x1,sp,#8 adrp x8,|$SG4983| add x0,x8,|$SG4983| bl printf mov w0,#0 add sp,sp,#0x400 bl __security_pop_cookie ldp fp,lr,[sp],#0x10 ret
なぜ64ビットのx10と32ビットのw8を使用しているか不明
blはサブルーチン呼び出し
代入とassembler
c
#include <stdio.h> // mainブロック int main(void) { int age; age = 20; printf("私は%d歳です\n", age); return 0; }
assembler
|main| PROC |$LN3| stp fp,lr,[sp,#-0x20]! mov fp,sp mov w8,#0x14 str w8,[sp,#0x10] ldr w1,[sp,#0x10] adrp x8,|$SG4981| add x0,x8,|$SG4981| bl printf mov w0,#0 ldp fp,lr,[sp],#0x20 ret
str
ストア命令 (STR) はレジスタに格納されている値をメモリに書き込み
ageをdoubleにするとこうなる
ラベルでldrしている
|$LN4| stp fp,lr,[sp,#-0x20]! mov fp,sp ldr d16,|$LN3@main| str d16,[sp,#0x10] ldr x1,[sp,#0x10] adrp x8,|$SG4981| add x0,x8,|$SG4981| bl printf mov w0,#0 ldp fp,lr,[sp],#0x20 ret nop |$LN3@main| DCFD 20.5
DCFD and DCFDU: The DCFD directive allocates memory for word-aligned double-precision floating-point numbers, and defines the initial runtime contents of the memory. DCFDU is the same, except that the memory alignment is arbitrary.
https://developer.arm.com/documentation/dui0801/l/Directives-Reference
型によってassemblerのオペランドが異なっていることがわかる
printfとassembler
c
#include <stdio.h> int main(void) { printf("テキスト1"); printf("テキスト2"); return 0; }
assembler
|main| PROC |$LN3| stp fp,lr,[sp,#-0x10]! mov fp,sp adrp x8,|$SG4980| add x0,x8,|$SG4980| bl printf adrp x8,|$SG4981| add x0,x8,|$SG4981| bl printf mov w0,#0 ldp fp,lr,[sp],#0x10 ret
STP
Store Pair of Registers calculates an address from a base register value and an immediate offset, and stores two 32-bit words or two 64-bit doublewords to the calculated address, from two registers. For information about memory accesses, see Load/Store addressing modes.
命令は 2つのレジスタ (Xt1、Xt2) の内容を、メモリアドレスを保持するレジスタ (Xn) にオフセットの値を加えたアドレスを先頭とするメモリに書き込み、レジスタをスタックに退避する
adrp
プログラムカウンタ (PC) に ±4GB の範囲のオフセットを加えたアドレスを 4KBを単位としたページアドレスに変換して、指定したレジスタ(Xd) に書き込みます。 ADRP命令のオフセットの値には immHi の19ビットと immL の2ビットを加えた21ビットの範囲の整数を 4096倍(12bit) した整数が指定できるため、PCの値±4GB の範囲の 4KB を単位としたページアドレスを指定できます。
bl
ンク付分岐命令の BL は、プログラムカウンタの値にオフセットを加えた メモリアドレスに分岐(ジャンプ)します。 分岐先の命令に付けたラベルを分岐先として指定しますが、 アセンブラが「BL」命令のアドレスとラベルのアドレスから、 プログラムカウンタ相対オフセットを計算します。 オフセットの値は内部的には26ビットの符号付整数ですが、 プログラムカウンタの値は常に4の倍数であることを利用するため、 分岐先は ±128MB の範囲が可能
lr リンクレジスタ
L x30レジスタ 関数コールした時の戻り番地を記憶
fp フレームポインタ
L x29レジスター
プログラムカウンター(PC)
L 次に実行される命令のアドレスを保持するレジスタです
スタックポインター(SP)
L スタックのトップを指すレジスタです
ステータスレジスター(NZCV)
L 直前の演算の結果に関するフラグを保持するレジスタです
x86アセンブリ
アセンブリを学ぶメリット
—
CPU を支配できる.
コンパイラより高速なコードを書ける
文法が簡単である.
バイナリ (実行できるプログラム) が小さい.
まったく新しい OS を作るには必須の知識である.
ウイルスに負けない体力を養える.
コンパイラやインタプリタの作者になれる.
Linux カーネルの機能を理解できる.
C のポインタが簡単に理解できる.
—
OS(ブートローダー、カーネル、デーモン、シェル、デスクトップマネージャ、アプリケーション)、カーネル、インタプリタ、コンパイラ周りを理解するには必須のスキルだということがわかる。
hello.asm
;------------------------------------ ; hello.asm ;------------------------------------ section .text global _start msg db 'hello, world', 0x0A msglen equ $ - msg _start: mov ecx, msg ; 文字列の場所を指定 mov edx, msglen ; 文字列の長さを設定 mov eax, 4 ; 出力のシステムコール mov ebx, 1 ; 標準出力を指定 int 0x80 ; システムコール実行 mov eax, 1 ; 終了のシステムコール mov ebx, 0 ; 正常終了の 0 に設定 int 0x80 ; システムコール実行 ;------------------------------------
プログラムを終了するサブルーチン
Exit: mov eax, 1 ; sys_exit mov ebx, 0 ; exit with code 0 int 0x80
異常終了
;------------------------------------ ; exit with ebx ExitN: mov ebx, eax ; exit with code ebx mov eax, 1 ; sys_exit int 0x80
writeのシステムコール
;------------------------------------ ; print string to stdout ; eax : top address ; edx : no of put char OutString: pusha mov ecx, eax mov eax, SYS_write mov ebx, 1 ; to stdout int 0x80 popa ret
;------------------------------------ ; get length of asciiz string ; eax : top address ; eax : return length StrLen: push ecx push edi mov edi, eax push eax xor eax, eax mov ecx, 0xFFFF ; no more than 65k chars. repne scasb pop ecx sub edi, ecx mov eax, edi pop edi pop ecx ret ;------------------------------------ ; print asciiz string ; eax : pointer to string OutAsciiZ: push edx push eax call StrLen mov edx, eax pop eax call OutString pop edx ret