Capture The Frog

かえるぴょこぴょこw

MENU

アセンブリ言語入門 その7

今回は、配列と文字列をやっていこうと思います。

 

配列

 配列は、メモリ内の連続した場所に格納されるため簡単にアクセスできます。

スタックは使わないです。

例えば、int型の4つの要素を含む配列だったら、C言語

int nums[4]={1,2,3,4}

と表せます。同じ処理をアセンブラでしようとすると、4つの要素しかないのにも関わらず、5箇所のメモリアドレスを使用します。これが文字列だったら最後尾にNULL文字が格納されているからとわかりますが、これは配列なのでNULL文字は含みません。

では、なぜ5箇所もメモリアドレスを使用するのかというと、

配列名(1つ)と配列の要素(4つ)を格納するからです。

配列が呼び出されると、まず最初に配列名のメモリアドレスを指します。

配列名のメモリアドレスは、nums[1]のメモリアドレスを指します。

そして、nums[1]のメモリアドレスは数字とnums[2]へのメモリアドレスを指します。

このように、数珠つなぎのように配列は構成されています。

そのため、アセンブリの配列要素は、

・配列のベースアドレス(配列名のアドレスのこと)

・要素のインデックス

・配列内の各要素のサイズ

高級言語で配列を使うと、アセンブリでは


[配列名 + 0 * <バイト表記による各要素のサイズ>]

 に変換されます。

バイト表記による各要素のサイズとは、C言語だと

1バイト   char

2バイト   short int 

4バイト   int

        long int

                        float

8バイト   double

                        long double

っていう感じです。実際にアセンブリで先程のCのコードを書いていきます。


nums[0] = [nums+0*4] = [0x4000+0*4] = [0x4000] = 1
nums[1] = [nums+1*4] = [0x4000+1*4] = [0x4004] = 2
nums[2] = [nums+2*4] = [0x4000+2*4] = [0x4008] = 3
nums[3] = [nums+3*4] = [0x4000+3*4] = [0x40012] = 4
  ➀         ➁            ➂            ➃       ➄

こんな感じになります。たくさん文字が書いてあって、分からなくなるかもしれないですが、 落ち着いて見ていきましょう。

説明が難しいので左から順に➀、➁、➂、➃、➄としますね。

➀は、配列のインデックスを指してます。

➁は先程説明した通りです。*4になっているのは、int型が4バイトだからです。

➂は、nums[0]のメモリアドレスからどれだけ離れているかを示しています。 

でも、構造的には➁と同じで配列名がnums[0]のメモリアドレスに変わっただけです。

➃は、➂の[ ]内の計算をしたものでそれぞれの要素のメモリアドレスです。

➄は、配列の要素です。

また、メモリアドレスに 16進数が含まれている場合は、それは配列になります。

 

難しそうに見えますが、落ち着いてみると意外とわかります。

文字列

配列には文字列も含まれます。

文字列といっても配列が分かっていれば、大丈夫です。

文字列は、文字の配列なので最後尾にNULL文字が入ります。ということと、新しい命令をいくつか覚えていくだけです。

文字列命令の大きな特徴としては、b,w,dのいずれかが、それぞれの命令の後ろに来ることです。

bは、1バイトの文字を操作し、wは2バイトを、dは4バイトの文字データを扱います。

↓1文字あたりのバイトがまとめられていてわかりやすいです。

https://software.fujitsu.com/jp/manual/manualfiles/m150018/b1ws1136/04z200/b1136-i-03-04.html

 

文字列を扱うレジスタは、EAX,ESI,EDIで、ESI・EDIレジスタは文字列演算を実行シた後、方向フラグ(df)によってインクリメント(i++)か、デクリメント(i--)されます。 

CLD命令は、この方向フラグをクリアします。

すると、ESI・EDIレジスタはインクリメントされます。

逆に、STD命令は方向フラグを設定するためESI・EDIレジスタはデクリメントされます。

 

MOVSX(全般・MOVSB(1バイト・MOVSW(2バイト・MOVSD(4バイト)命令

これらのはESIレジスタで指定されたアドレスからEDIレジスタで指定されたアドレスに決められたバイト数移動する。

 

REP命令

上の命令たちは、1文字しか移動できないので、マルチバイトの内容をコピーするためには、上の命令たちの前にREP命令を置く。そしたら、NULL文字も一緒にいどうされる。

STOSX(全般・STOSB(1バイト・STOSW(2バイト・STOSD(4バイト)命令

それぞれのレジスタから、EDIで指定されたメモリアドレスへバイトを移動する。

 

 

では、最後の総復習としてアセンブリを読んで「アセンブラ言語入門」シリーズを終わりにしましょう。

 

総復習


push ebp
mov ebp, esp
sub esp, 14h
mov dword ptr [ebp-14h], 1
mov dword ptr [ebp-11h], 2
mov dword ptr [ebp-1ch], 3
mov dword ptr [ebp-5], 0

loc_401023:
cmp dword ptr [ebp-5], 3
jge loc_40103E
mov eax, [ebp-5]
mov ecx, [ebp+eax*4-14h]
mov [ebp-9], ecx
mov edx, [ebp-5]
add edx, 1
mov [ebp-4], edx
jmp loc_401023

loc_40103E:
xor eax, eax
mov esp, ebp
pop ebp
ret
結構長いですよね。。
でも、1行ずつしっかりと落ち着いてみれば読み解けるはずです。
では、さっそく読み解いていきましょー

1,
メモリアドレスをそれぞれ分かりやすく変えます。
loc_401023:は、while文だと分かるのでwhileに書き換えます。
dword ptrは、プログラムの動きに直接は関係ないから消します。
loc_40103Eがなんの処理かわかりませんが、関数なので somethingと書き換えます
14hを10進数に直します。

 push ebp
mov ebp, esp
sub esp, 20
mov a, 1
mov b, 2
mov c, 3
mov d, 0

while:
cmp d, 3
jge something
mov eax, d
mov ecx, [ebp+eax*4-20]
mov e, ecx
mov edx, d
add edx, 1
mov d, edx
jmp while

something:
xor eax, eax
mov esp, ebp
pop ebp
ret

大体はこれで読みやすくなりました。

それぞれがどんな動きをしているかざっくり見ると、

1番上のセクションは、something関数を使うための関数インポートとそれぞれのメモリアドレスに値を格納しています。

2番目のwhileセクションは、

jgeは、より大きいか等しいときにsomethingにジャンプするので、

dが3より小さいときだけ処理が続けられるっていうことです。

処理の内容は今は少し大変なので下の2、で読み解いていきたいと思います。

3番目のsomething関数は、

return 0;

関数エピローグですね。特に何もしていません。

 

2, 高級言語に翻訳していく

実質的に関係のないプログラムを消す

メモリアドレスに16進数のものを配列に変える。


sub esp, 20   ;ESP=20
mov a, 1      ;anon[0]=1
mov b, 2      ;anon[1]=2
mov c, 3      ;anon[2]=3
mov d, 0      ;d=0

while:        
cmp d, 3      ;while(d<3){
jge something
mov eax, d    ;EAX=d=0
mov ecx, [ebp+eax*4-14h] ;ECX=ebp[eax*4-20]
mov e, ecx    ;e=ECX
mov edx, d    ;EDX=d
add edx, 1    ;EDX=EDX+1
mov d, edx    ;d=EDX
jmp while     ;}

something:
xor eax, eax  ;return 0
ret

 

3,より詳しくまとめる

anon[0]=1
anon[1]=2
anon[2]=3
d=0

while(d<3){
EAX=d
ECX=anon[anon+d*4]
e=ECX
EDX=d
d=d+1
}
return 0;


これからも他のシリーズやっていくのでお願いします。
もし、このシリーズに付き合ってくれた人がいたのなら、
本当にありがとうございました。

アセンブリ言語入門 その6

 

今回は、関数について書いていきたいと思います。

 

関数に触れる前に

関数が呼び出されると、制御は別のメモリアドレスに移動します。

CPUはそのメモリアドレスで処理を実行し、処理が終わると元のメモリアドレスに戻ってきます。関数のパラメータ、ローカル変数、フロー制御はすべてスタックと呼ばれる

メモリの重要な領域に保存されます。

まずスタックについて軽く説明していきます。

スタック

スタックとはスレッド作成時にOSによって作られるデータ構造です。

そして、LIFOを採用しています。(後入れ先出し、Last In First Outの略ね。

つまり、PUSH・POP命令でスタックのデータをやりとりするということです。

 

PUSH命令・POP命令

 PUSH命令は、4バイト値をスタックにプッシュ(格納し、

POP命令は、4バイト値をスタックからポップ(取りだします。


push source        ;sourceをスタックの最上部にプッシュします。
pop des            ;スタックの最上部の値をdesにコピーします。

スタックは、上位アドレスから下位アドレス方向へデータを積み上げていきます。

つまり、スタックが作成されるとESPレジスタ(スタックポインタ)がスタックの上位アドレス(先頭)を指しています。

スタックにデータをプッシュするとESPレジスタの値が4バイトずつ減少し、

スタックからデータをポップするとESPレジスタの値が4バイトずつ増加します。

 

 

関数

アセンブリ言語のCALL命令を利用して、関数を呼び出すことができます。


call fanction     ;関数の呼び出し

関数を大きなメモリアドレスだと考えるとわかりやすいです。

関数が終了されるとリターンアドレスがスタックから取り出され、メモリアドレス

から実行が継続されます。

 

関数の終了には、RET命令を使います。

この命令はスタックの最上部にあるリターンアドレスをポップします。

そして、制御はポップされたアドレスに移行します。

 

関数パラメータと戻り値

x86において、関数が受け取るパラメータはスタックにプッシュされ

戻り値がEAXレジスタに格納されます。

C言語アセンブラでおなじ処理を書いていきます。


int test(int a,int b)
{
   int x,y;
   x=a;
   y=b;
   return 0;
}
int main(){
test(2,3)
return 0;
}

これをアセンブリで表すと、


test:
push ebp ;フレームポインタ(EBPをスタックに保存する
mov ebp, esp ;EBP=ESP
上の2行の処理を関数プロローグという。
sub esp, 8 ;ESPレジスタを減らしている
mov eax, [ebp+8] ;関数の実際の処理
mov [ebp-4], eax |
mov ecx, [ebp+0ch] |
mov [ebp-8], ecx ↓  
xor eax, eax ;eaxの値を0にする。return 0;を示す
mov esp, ebp ;ESPはEBPに格納されているメモリアドレスを指す。
上の2行を関数エピローグという。
pop ebp          ;スタックから古いEBPを復元する
ret             ;スタックの先頭の戻りアドレスがポップ。制御が戻る

main: push 3 ;3をスタックにプッシュ
push 2 ;2をスタックにプッシュ
call test ;test関数を呼び出す
add esp, 8 ;test関数が実行された後、制御がここに戻ってくる
xor eax, eax        ;レジスタ値をクリアにする。return 0;を示す。

 

 という感じです。スタックが登場して少しややこしくなりました。

関数プロローグとは、関数が利用できる環境のセットアップをしています。

そして、関数エピローグとはプロローグとは逆のことをして元の環境を復元します。

また、main関数はtest関数を呼び出しているので"呼び出し元関数"。

test関数は呼び出されているので"呼び出し先関数"といいます。

main関数の


add esp, 8

 は、test関数で使用したスタックにプッシュされたパラメータを削除する役割があり、

関数を呼び出す前のスタックポインタに戻しています。

 

 

今回は、関数についてやってきましたが少し難しかったです。

ここは、自分でも少しわからないところがあるので、後で見返してみようと思いました。

みなさんも、おつかれさまでした。

次回は、配列と文字列をやっていきます。これでアセンブラ言語入門は一区切りつけたいと思います。

qwertytan.hatenablog.jp

アセンブリ言語入門 その5

 

今回は、While文についてやっていきます。

While文

 While文には、初期化・継続条件・アップデート文が必要です。初期化は一度だけ行われて継続条件までアップデート文に従って処理を続ける。ってとこでしょうか。

それでは、前回と同様C言語と見比べてみましょう。

一応、for文
int i;
for(i = 0 ; i < 7 ; i + +){
}

これをWhile文で表すと、

int i = 0;
while ( i < 5 ){
   i++;
}

アセンブリでこれを表すときには、ジャンプを利用します。

if文でもジャンプを使いましたがWhile文でも使います。

whileの中身の処理を行って、継続条件に合わなくなったら、whileからジャンプして抜けます。では、実際に見てみましょう


mov [i], 0             ;初期化

while_start: ;while文スタート!
cmp [i], 5 ;条件にあっているか判断・フラグを設定
jge end ;上の処理で条件にあったなかったらendにジャンプ
mov eax, [i] ;EAX=i
add eax, 1 ;アップデート文
mov [i], eax ;戻す
jmp while_start ;while文の一番最初にジャンプ!
end:

んん、なんか古典的ですよね、、

コメントでも一応書きましたが、ここに改めて流れをまとめておくと

 

初期化

while文スタート!

条件にあっているか判断・フラグを設定

上の処理で条件にあったなかったらendにジャンプ

アップデート文

while文の一番最初にジャンプ!

 

一番最後で後ろ向きのジャンプが入っているのがwhile文の特徴です。

前にぴょこぴょこしてって、後ろにぴょこってするんですよね(謎

今回のwhile文自体は前回の内容がわかっていれば、そんなに難しくないと思います。

 

実践編

 


mov dword ptr [ebp-8], 1
mov dword ptr [ebp-4], 1

loc_401014:
cmp dword ptr [ebp-4], 4
jge short loc_40102E
mov eax, [ebp-8]
add eax, [ebp-4]
mov [ebp-8], eax
mov ecx, [ebp-4]
add ecx, 1
mov [ebp-4], ecx
jmp short loc_401014

loc_40102E:

 

これが、while文だって言えるのは、やっぱりいちばん最後の

jmp short loc_401014

があるからですね。見逃したら迷子になる。

 

1,

とりあえず読みやすくする。

[ebp-8]をa

[ebp-4]をbと置きました。

そして、while文だと分かったのでwhile:とend:に書き換えました。

dword ptrはプログラムの動きに直接関係ないので、消しました。

 

mov a, 1
mov b, 1

while:
cmp b, 4
jge short end
mov eax, a
add eax, b
mov a, eax
mov ecx, b
add ecx, 1
mov b, ecx
jmp short loc_401014

end:

この作業にも結構なれてきましたね。

jgeとは、比較結果が大きい場合、等しい場合にジャンプするので、

比較結果がbより小さい、等しくないときはwhileの処理を実行するということです。

 

 

2,

高級言語に変換していく


a=1
b=1
while(b<4){
eax=a eax=eax+b a=eax ecx=b ecx=ecx+1 b=ecx }

 こんな感じですかね。でも、まだちょっと読みにくいので整理します。


a=1
b=1
while(b<4){ eax=a+b a=a+b ecx=b ecx=b+1 b=b+1 }

 できました!

最初に比べたらだいぶ早くなってきたのかなあと思います。

皆さんもぜひ実際に手を動かして、セルフ逆アセンブラしてみるのもいいかもしれません。

お疲れ様でした。

次回は、関数についてやっていきたいと思います。

qwertytan.hatenablog.jp

 

アセンブリ言語入門 その4

 

 

条件分岐

 分岐命令は、実行制御を別のメモリアドレスに移します。

分岐を実行するためアセンブリではジャンプ命令がよく利用されます。

ジャンプには、無条件ジャンプと条件付きジャンプの2つがあります。

 

無条件ジャンプ

無条件ジャンプでは常にジャンプを行います。

JMP命令で示し、異なるメモリアドレスでコードを実行するようにCPUに指示します。

Cのgoto文に似ています。

 

JMP <jump address>                      ;ジャンプ先アドレスに実行制御が移動し、そこから

                実行が開始されます。

条件付きジャンプ

条件付きジャンプでは、一定の条件に基づき指定されたメモリアドレスに処理が移動します。ジャンプを利用するには、フラグを変更できる命令が必要です。

その命令は算術演算やシフト演算で実行することができます。

x86では、ディスティネーションからソースを引き算し、ディスティネーションに結果を格納すること無くフラグを変更するCMP命令が提供されています。

 

cmp eax, 5                                     ;EAXから5を引き算する。結果がゼロになるためゼロ

                   フラグを設定するが、結果は格納されない。

 

もう一つの結果を格納せずにフラグを変更する命令は、TEST命令です。

TEST命令は、ビットごとにAND命令を実行し、フラグを設定します。

eax=0とする。

test eax, eax                                  ;EAXがゼロであるため、ゼロフラグが設定される。

                                       しかし、結果は格納されない。

 

通常、CMP命令とTEST命令は条件付きジャンプ命令と一緒に格納される。

条件付きジャンプには何種類かある。しかし、一般的な形は変わらない。

jcc <address>

上のccの部分は、条件を示します。

この表は覚える必要はないので眺める感じで見てみてください。

 命令 ジャンプ条件  フラグ 

 ja

より上  cf=0 & zf=0 
 jb より下  cf=1 
 jc キャリーがある  cf=1 
 je 等しい  zf=1 
 jge より大きいか等しい  sd=of 
 jle より小さいか等しい  zf=1 | sf ! of 
 jnae より上でなく等しい  cf=1 
 jnbe より下でなく等しい  cf=0 & zf=0 
 jne 等しくない  zf=0 
 jnge より大きくなく等しくない  sf ! of 
 jnle よる小さくなく等しくない  zf=0 & sf=of 
 jnp パリティがない  pf=1 
 jpo パリティが基数 

pf=0 

 jz ゼロである zf=1 
 jae より上か等しくない  cf=0 
 jbe より下か等しくない cf=1 | zf=1
 jcxz  CXレジスタが0  
 jg  より大きい  zf=0 & sf=of
 jl  より小さい  sf ! of
 jna  より上でない  cf=1 | zf=1
 jnb  より下でない  cf=0
 jnc  キャリーがない  cf=0
 jng  より大きくない  zf=1 | sf ! of
 jnl  より小さくない  sf=of
 jno  オーバーフローがない  of=0
 jns  符号がない  sf=0
 jo  オーバーフローがある  pf=1
 jpe

 パリティが偶数

 pf=1
 js  符号がある  sf=1
     

if文

 アセンブリにおいて、条件を確かめるプログラムの役割を担うのは上で書いたとおり、

条件付きジャンプで行います。

C言語との対応で見てみましょう。

if(x==0){
 x=5;
}
x=2;

これをアセンブリで表そうとしたときに、(x==0)をどのように表せばいいか戸惑いますよね。しかし、ここは条件付きジャンプ命令をつかいます。

ここで(x==0)が表しているのは、xが0と等しいかということですよね。つまり、ゼロフラグzfが0であれば、x=5を。そうでない場合は、x=2とすればよいのです。

cmp dword ptr [x], 0
jne end_if
mov dword ptr [x], 5
end_if
mov dword ptr [x], 2

アセンブリでは、JNE命令(等しくない場合ジャンプ・等しければ処理を続ける)が利用されていることに注意してください。

ちょっとわかりにくくなってきましたよね。

しかし全部読むと割と分かってくるので、もう少しお付き合いください。

 

If-else文

C言語との対応で見ていくとわかりやすいので先程と同じやり方でいきます。

if(x==0){
 x=5;
} else{
 x=2; }

アセンブラで表すと、

cmp dword ptr [x],0
jne else
mv dword ptr [x],5
jmp end

else:
mov dword ptr [x],2

end:

どうですか。なんとなく分かってきましたよね。

cmpでフラグを立てて、異なる場合はジャンプする。

else文だったら、else部にジャンプする。

そして、そこでの処理を書く。

 言葉で説明するよりコード見たほうが断然わかりやすいですよね笑

 

if-elseif-else文

C言語のコード


if(x==0){
  x=5;
}
else if(x==1){
  x=6;
}
else{
  x=7;
}

これをアセンブリで表すと、


cmp dword ptr [x], 0
jne else_if
mov dword ptr [x], 5
jump short end

else_if:
cmp dword ptr [x], 1
jne else
mov dword ptr [x], 6
jump short end

else:
mov dword ptr [x], 7

end: 

アセンブリのif文全般で気をつけないといけないこととしては、それぞれの処理の最後につくjump short end とかend:を書くことですかね。あと、判断(x==0)とかが高級言語とは反対なのもいやらしいです。

 

 

今回は、ここで終わりにします。

わかんないなってところは何回も見直すとなんとなくわかります。

最初はなんとなくでいいんです。ぼやっとでもわかれば。

自分も書いてるうちに分かってきたことも結構あります。

では、お疲れ様でした。

次回はwhile文についてやりたいと思います。

qwertytan.hatenablog.jp

アセンブリ言語入門 その3

頭の片隅に置いておくと理解がすすむもの

・バイト→ビット ×8
・ビット→バイト /8
アセンブリとは、マシンコードをアセンブリコードに変換すること

x86 CPUには、8つの汎用レジスタがある。
 EAX・EBX・ECX・EDX・ESP・EBP・ESI・EDI
 これらは全て32ビット(4バイト)のサイズ
 プログラムは、これらのレジスタに32(4バイト),16(2バイト),8(1バイト)ビットの値と         

 してレジスタにアクセスすることができる。


・各レジスタの下位16ビット(2バイト)には
 AX・BX・CX・DX・SP・BP・SI・DI
 これらは、全て16ビットだから16,8ビットの値として、プログラムはレジスタにアク 

 セスできる。
 後で、アセンブリ命令の格納先として出てくるので理解しとく。

 

・ちなみにc言語だと
 1バイト   char
 2バイト   short int
 4バイト   int
         long int

         float
   float(浮動小数点は、EAXなどの汎用レジスタではなく)浮動小数レジスタ(sd・snが

   使用される。 
   8バイト         double
                         long double

  となっている。


・リトルエンディエン方式とは、

 下位バイトは下位アドレスに格納され、後続のバイトはメモリ内の連続した上位アド 

 レスに格納される。
 「リトルエンディエン方式とは、データをバイト単位で並べる際のやりかたの一つ

 で、「最後」のバイトから順番にデータを並べる方式のこと」

  https://wa3.i-3-i.info/word11428.htmlより引用

 

・実行する操作を指定する部分を"オペコード"という
 そして、操作される対象を"オペランド"と呼ぶ

 オペランド デスティネーション, ソース
 命令    格納先,  格納元

 みたいな感じ。
 今回は、デスティネーション(格納先)を先に書くやり方で書いていく。

 この書式をIntel Syntaxと呼ぶ。MASM・NASMで採用されている。


・;の後は、コメント文

 

 

 

今回は、加減乗除に引き続きビット演算とシフト演算についてやっていきます。  

ビット演算

ビットを操作する方法について書いていきます。

ビットは、右端から順に番号が振られています。

右端のビットは最下位ビットと呼ばれ、左に行くほどビット位置は増加します。

同じ論理がword,dwordにも適用されます。

 

NOT命令

1つのオペランドだけを取り、全てのビットを反転します。

EAXに、11111111 11111111 00000000 00000000が格納されている場合

 

not eax      ;EAXを反転した00000000 00000000 11111111 11111111が

           EAXに格納されます。

 

論理演算 

AND命令・OR命令・XOR命令

ビット単位でのAND・OR・XOR演算を実行し、その結果をディスティネーションに格納します。

これらの操作は、CやPythonでの実行結果と似ています。

 

and bl, cl     ;bl=bl&clと同じ

or eax, ebx    ;eax=eax | ebxと同じ

xor eax, ebx     ;eax=eax^ebxと同じ

 

 

SHR命令・SHL命令

SHR命令は右論理シフト、SHL命令は左論理シフトをします。

両命令とも、ディスティネーション(dstのビットをcountで指定されたビット数だけ左右に論理シフトします。

ちなみに、1論理右シフトしたら値は2倍になり、1左論理シフトしたら値は1/2倍されます。

 

shl dst, count        ;countで指定されたビット数左論理シフトする。

shr dst, count        ;countで指定されたビット数右論理シフトする。

 

ROL命令・ROR命令

SHIFT命令に似ていますが、シフト命令と違いずれたビットを削除するのではなく

反対側に回転する。

 

rol al, 2          ;ALに0100 0100が格納されていた場合、実行後のALには0001 0001                                           が格納されます。

 

 

今回は、ビット演算と論理演算についてやってきました。

次回は、条件分岐をやっていきたいと思います!

qwertytan.hatenablog.jp

 

 

 

 

 

 

アセンブリ言語入門 その2

頭の片隅に置いておくと理解がすすむもの

・バイト→ビット ×8
・ビット→バイト /8
アセンブリとは、マシンコードをアセンブリコードに変換すること

x86 CPUには、8つの汎用レジスタがある。
 EAX・EBX・ECX・EDX・ESP・EBP・ESI・EDI
 これらは全て32ビット(4バイト)のサイズ
 プログラムは、これらのレジスタに32(4バイト),16(2バイト),8(1バイト)ビットの値と         

 してレジスタにアクセスすることができる。


・各レジスタの下位16ビット(2バイト)には
 AX・BX・CX・DX・SP・BP・SI・DI
 これらは、全て16ビットだから16,8ビットの値として、プログラムはレジスタにアク 

 セスできる。
 後で、アセンブリ命令の格納先として出てくるので理解しとく。

 

・ちなみにc言語だと
 1バイト   char
 2バイト   short int
 4バイト   int
         long int

         float
   float(浮動小数点は、EAXなどの汎用レジスタではなく)浮動小数レジスタ(sd・snが

   使用される。 
   8バイト         double
                         long double

  となっている。


・リトルエンディエン方式とは、

 下位バイトは下位アドレスに格納され、後続のバイトはメモリ内の連続した上位アド 

 レスに格納される。
 「リトルエンディエン方式とは、データをバイト単位で並べる際のやりかたの一つ

 で、「最後」のバイトから順番にデータを並べる方式のこと」

  https://wa3.i-3-i.info/word11428.htmlより引用

 

・実行する操作を指定する部分を"オペコード"という
 そして、操作される対象を"オペランド"と呼ぶ

 オペランド デスティネーション, ソース
 命令    格納先,  格納元

 みたいな感じ。
 今回は、デスティネーション(格納先)を先に書くやり方で書いていく。

 この書式をIntel Syntaxと呼ぶ。MASM・NASMで採用されている。


・;の後は、コメント文

 

算術演算

アセンブリ言語加減乗除をやっていきます。
これらの命令は2つのオペランド(ソースとディスティネーション)を使って行います。

加算は、ADD命令
減算は、SUB命令
掛け算は、MUL命令
割り算は、DIV命令を使います。

では、早速やっていきましょう。

 

 

ADD命令(足し算

ADD命令は、ソースとディスティネーションを足し、結果をディスティネーションに格納します。
結果に基づいて、eflagsレジスタのフラグを立てたり、クリアしたりします。
これらのフラグは、条件文で利用することができます。

add eax, 24                 ;eax=eax+24と同じ
add eax, ebx               ;eax=eax+ebxと同じ
add [ebx], 24               ;ebxに指定されているアドレスに格納された値に42を足す。

 

 

SUB命令(引き算

SUB命令は、ディスティネーションからソースを引き算し、結果をディスティネーションに格納します。
結果に基づいて、eflagsレジスタのフラグを立てたり、クリアしたりします。
これらのフラグは、条件文で利用することができます。
また、引き算の結果が
0の場合は、ゼロフラグ(zf
マイナス値になる場合は、キャリーフラグ(cfを設定します。

ebxの指しているメモリの値を24とする。

sub eax, 24               ;eax=eax-24と同じ
sub [ebx], 24             ;ebxは24だから、
                                  ebx=ebx-24
           =0となり、zfが設定される。
sub [ebx], 25             ;実行結果は、-1になるのでcfが設定される。

 

 

MUL命令(掛け算

MUL命令は、オペランドを一つだけ取ります。
そのオペランドは、AL,AX,EAXレジスタのいずれかの内容で掛け算されます。
掛け算の結果が、AX,DX,AX,EDX,EAXレジスタに格納されます。
MUL命令のオペランド
8ビット(1バイトの場合は、ALレジスタが掛け算され、その積がAXレジスタに格納されます。
16ビット(2バイトの場合は、AXレジスタと掛け算され、その積がDX・AXレジスタに格納されます。
32ビット(4バイトの場合は、EAXレジスタと掛け算され、その積がEDX・EAXレジスタに格納されます。
積が2つのレジスタに格納される理由は、2つの値が掛け算されると出力値が入力値よりもはるかに大きくなる場合があるからです。

mul ebx     ;EBXは、EAXレジスタと掛け算され、結果はEDXとEAXに格納され 

          る。
mul bx       ;BXは、AXレジスタと掛け算され、結果はDX・AXレジスタに格納

          される。 

 

 

DIV命令(割り算

DIV命令は、レジスタまたはメモリ参照のいずれかを使い、1つのオペランド(a÷bのbを取ります。
割り算を実行するためには、EDX・EAXレジスタに(a÷bのaを入れる必要があります。
EDXには、最上位のdword値(4バイトの値が入ります。
DIV命令が実行された後、商はEAXに格納され、余りはEDXレジスタに格納されます。

 

div ebx                  ;EBXにより割られた結果は、EDXとEAXに格納されます。

練習問題

mov dword ptr [ebp-5], 16h
mov dword ptr [ebp-8], 5
mov eax, [ebp-5]
add eax, [ebp-8]
mov [ebp-0ch], eax
mov ecx, [ebp-5]
sub ecx, [ebp-8]
mov [ebp-0ch], ecx

 

解法

1,

オペランドのややこしいレジスタの値を同じレジスタ同士で、わかりやすい文字に置きかえます。


mov dword ptr a, 16h
mov dword ptr b, 5
mov eax, a
add eax, b
mov c, eax
mov ecx, a
sub ecx, b
mov c, ecx

 

2,

今までのコメントと同じように書いていきます。
mov dword ptr a, 16h       ;dword値16hをaに格納する。
mov dword ptr b, 5      ;dword値5をbに格納する。
mov eax, a          ;EAXにaを格納する。
add eax, b           ;EAX=EAX+b
mov c, eax             ;cにEAXの値を格納する。
mov ecx, a          ;ECXにaを格納する。
sub ecx, b           ;ECX=ECX-b
mov c, ecx          ;cにECXの値を格納する。

 

 

3,

だいぶ読めてきました。
全てのコメントを擬似言語に直します。
mov dword ptr a, 16h    ;a=dword(16h)
mov dword ptr b, 5     ;b=dword(5)
mov eax, a          ;EAX=a
add eax, b        ;EAX=EAX+b
mov c, eax          ;c=EAX
mov ecx, a        ;ECX=a
sub ecx, b            ;ECX=ECX-b
mov c, ecx           ;c=ECX

 

4,

擬似言語から読み解いていきます。
a=dword(16h)
b=dword(5)
EAX=dword(16h)
EAX=dword(16h)+dword(5)
c=dword(16h)+dword(5)
ECX=dword(16h)
ECX=dword(16h)-dword(5)
c=dword(16h)-dword(5)

 

5,

16進数を10進数に直して、dwordは、プログラムの動きに直接関わってこないので消します。16hのhは、この数字が16進数であると示しているので、10進数で16hは1×16+6×1=22となります。
a=22
b=5
EAX=22
EAX=22+5
c=22+5
ECX=22
ECX=22-5
c=22-5
となります!

 


こんな感じで、アセンブリ言語から高級言語(Cとかに読み解くことができます!

今回は、加減乗除のやり方とアセンブリ言語から高級言語に読み替えることができました。
次回は、ビット演算に関してやっていきます!

qwertytan.hatenablog.jp

アセンブリ言語入門 その1

今回から、何記事かにわたってアセンブラ言語について書いていきたいと思います。少し前に、マルウェアの分析を初めて1本記事を書いたのですが、

qwertytan.hatenablog.jp

マルウェアの挙動の分析をしていてアセンブラがわからなくて悔しい部分があったのでアセンブリ言語について勉強しようと思いました。

 

まず、最初に

頭の片隅に置いておくと理解がすすむもの

・バイト→ビット ×8
・ビット→バイト /8
アセンブリとは、マシンコードをアセンブリコードに変換すること

x86 CPUには、8つの汎用レジスタがある。
 EAX・EBX・ECX・EDX・ESP・EBP・ESI・EDI
 これらは全て32ビット(4バイト)のサイズ
 プログラムは、これらのレジスタに32(4バイト),16(2バイト),8(1バイト)ビットの値と         

 してレジスタにアクセスすることができる。


・各レジスタの下位16ビット(2バイト)には
 AX・BX・CX・DX・SP・BP・SI・DI
 これらは、全て16ビットだから16,8ビットの値として、プログラムはレジスタにアク 

 セスできる。
 後で、アセンブリ命令の格納先として出てくるので理解しとく。

 

・ちなみにc言語だと
 1バイト   char
 2バイト   short int
 4バイト   int
         long int

         float
   float(浮動小数点は、EAXなどの汎用レジスタではなく)浮動小数レジスタ(sd・snが

   使用される。 
   8バイト         double
                         long double

  となっている。


・リトルエンディエン方式とは、

 下位バイトは下位アドレスに格納され、後続のバイトはメモリ内の連続した上位アド 

 レスに格納される。
 「リトルエンディエン方式とは、データをバイト単位で並べる際のやりかたの一つ

 で、「最後」のバイトから順番にデータを並べる方式のこと」

  https://wa3.i-3-i.info/word11428.htmlより引用

 

・実行する操作を指定する部分を"オペコード"という
 そして、操作される対象を"オペランド"と呼ぶ

 オペランド デスティネーション, ソース
 命令    格納先,  格納元

 みたいな感じ。
 今回は、デスティネーション(格納先)を先に書くやり方で書いていく。

 この書式をIntel Syntaxと呼ぶ。MASM・NASMで採用されている。


・;の後は、コメント文

 

アセンブリ命令

mov命令

mov dst, src
この命令はデータを格納元から格納先へ移動する。
Cとかの代入に似ている。

レジスタへの定数の格納

movの最初のバリエーションは、定数(コード中に直接書かれた値をレジスタへ格納する例。

mov eax, 30 ;eax=30という代入操作と同じで、30をEAXレジスタに代入する。
mov bx, 7 ;bx=7とおなじ
mov eax, 64h ;16進数の64h(100)をEAXに代入

 

レジスタからレジスタへの値の移動

mov eax, ebx ;EAXの値をEBXに代入する。EAX=EBXと同じ

実践
mov eax, 20
mov ebx, eax
さあ、EBXはいくつでしょう。
eax=20
ebx=eaxなので、EBXは20となる。

 

 

メモリからレジスタへの値の移動

アセンブリ言語でメモリからレジスタに値を移動するには、値のメモリアドレスが必要。
値はメモリにリトルエンディエン方式で格納されている。
メモリアドレスはの中にいれて表す。
そして、その
の番地だけを指しているのではなくて、その番地からはじまるデータ全体を指す。

メモリアドレス

0x403204
0x403203 00
0x403202 00
0x403201 00
0x403200 64

mov eax, [0x403200]
これは、メモリアドレスに代入されているデータ全体を指すから、
eaxには、00 00 00 64が代入される。

そして、4バイトだということをオペコード、オペランドで指定する必要がないことに注目
格納先のレジスタのサイズに基づいて移動するバイトが自動的に決まる。

 

 

内が、レジスタ・レジスタ+レジスタ・レジスタ+定数の場合

Cのポインタみたいな働きをする。内がポインタ
全て[]内に指定されたメモリアドレスに格納されている値をレジスタに移動する命令コード

mov eax, [ebx] ;EBXレジスタで指定されたレジスタの値をEAXに格納している。
mov eax, [ebx+ecx] ;EBX+ECXで指定されたアドレスの値をEAXに格納している。
mov eax, [ebp-4] ;EBP-4で指定されたレジスタで指定されたアドレスの値を EAXへ格納する。

 

レジスタからメモリへの値の移動

メモリアドレスをデスティネーションに置き、レジスタをソースにすることで
メモリにレジスタの値を書き込む。

mov [0x403200], eax ;0x403200から始まるメモリ位置にeaxの4バイトの値を格納
mov [ebx], eax ;eaxの4バイトの値をebxで指定したメモリアドレスに格納

 

dword ptr・word ptr

これらのメモリは定数値をメモリ内に格納する命令
dword ptrは、dword値(4バイトの値をメモリ内に移動する。
word ptrは、word値(2バイトの値をメモリに移動する。

mov dword ptr [40200], 13498h ;dword値0x13498を0x40200へ格納する。
mov dword ptr [ebx], 100 ;dword値100をebxが指すメモリアドレスへ格納する。
mov word ptr [ebx], 100 ;word値をebxが指すメモリアドレスへ格納する。

 

 

 

LEA命令

LEAとは、Load Effictive Address,(有効なアドレスを読み込むの略称。
値の代わりにアドレスを読み込む。

lea ebx, [0x403200] ;アドレス0x43200をEBXに格納する。
lea eax, [ebx] ;EBXに0x43200が格納されていた場合、EAXにも0x43200が格納される。

 

 

今回は、レジスタとメモリアドレスの代入について書いてきました。

代入は、C言語での基本的な代入・ポインタを知っていれば理解できそうです。

次の記事では実践的なアセンブラ言語の使い方について書いていきます!

qwertytan.hatenablog.jp