assembly language
概要
アセンブリ言語はビット列命令に対応した文字列命令を利用するプログラミング言語の種類。ビット列に対応する文字列命令(ニーモニック)を利用する。アセンブリ言語を用いることで、機械語相当の低水準なコードをより直感的に記述できる。
Memo
objdumpでlsコマンドを逆アセンブルする
objdump -d -M intel /bin/ls | head -n 30
/bin/ls: file format elf64-x86-64
Disassembly of section .init:
0000000000004000 <.init>: 4000: f3 0f 1e fa endbr64 4004: 48 83 ec 08 sub rsp,0x8 4008: 48 8b 05 b1 df 01 00 mov rax,QWORD PTR [rip+0x1dfb1] # 21fc0 <__gmon_start__@Base> 400f: 48 85 c0 test rax,rax 4012: 74 02 je 4016 <free@plt-0x66a> 4014: ff d0 call rax 4016: e8 c5 2b 00 00 call 6be0 <__sprintf_chk@plt+0x1f00> 401b: e8 d0 30 01 00 call 170f0 <_obstack_memory_used@@Base+0x6940> 4020: 48 83 c4 08 add rsp,0x8 4024: c3 ret
Disassembly of section .plt:
0000000000004030 <.plt>: 4030: ff 35 2a dc 01 00 push QWORD PTR [rip+0x1dc2a] # 21c60 <_obstack_memory_used@@Base+0x114b0> 4036: f2 ff 25 2b dc 01 00 bnd jmp QWORD PTR [rip+0x1dc2b] # 21c68 <_obstack_memory_used@@Base+0x114b8> 403d: 0f 1f 00 nop DWORD PTR [rax] 4040: f3 0f 1e fa endbr64 4044: 68 00 00 00 00 push 0x0 4049: f2 e9 e1 ff ff ff bnd jmp 4030 <free@plt-0x650> 404f: 90 nop 4050: f3 0f 1e fa endbr64 4054: 68 01 00 00 00 push 0x1
objdumpでオブジェクトファイルの中身を確認する
abc.cがあるとする。このデータがオブジェクトファイルではどう表現されているかを見る。
char str[] = "ABC";
$ gcc -fno-pic -fomit-frame-pointer -c -o abc.o abc.c $ objdump -s -j .data abc.o
abc.o: file format elf64-x86-64 Contents of section .data: 0000 41424300 ABC.
文字列ABCが、4バイト(0x41, 0x42, 0x43, 0x00)に変換されたことがわかる。
各種コマンド
$ gcc -c add.c
$ gcc -S -fno-pic -fomit-frame-pointer add.c
$ as -o add.o add.s
$ as -a -o add.o add.s
GAS LISTING add.s page 1 1 .file "add.c" 2 .text 3 .globl i 4 .data 5 .align 4 6 .type i, @object 7 .size i, 4 8 i: 9 0000 7B000000 .long 123 10 .text 11 .globl main 12 .type main, @function 13 main: 14 .LFB0: 15 .cfi_startproc 16 0000 F30F1EFA endbr64 17 0004 8B050000 movl i(%rip), %eax 17 0000 18 000a 05C80100 addl $456, %eax 18 00 19 000f 89050000 movl %eax, i(%rip) 19 0000 20 0015 B8000000 movl $0, %eax 20 00 21 001a C3 ret 22 .cfi_endproc 23 .LFE0: 24 .size main, .-main 25 .ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0" 26 .section .note.GNU-stack,"",@progbits 27 .section .note.gnu.property,"a" 28 .align 8 29 0000 04000000 .long 1f - 0f 30 0004 10000000 .long 4f - 1f 31 0008 05000000 .long 5 32 0: 33 000c 474E5500 .string "GNU" 34 1: 35 .align 8 36 0010 020000C0 .long 0xc0000002 37 0014 04000000 .long 3f - 2f 38 2: 39 0018 03000000 .long 0x3 40 3: 41 001c 00000000 .align 8 42 4: GAS LISTING add.s page 2 DEFINED SYMBOLS *ABS*:0000000000000000 add.c add.s:8 .data:0000000000000000 i add.s:13 .text:0000000000000000 main NO UNDEFINED SYMBOLS
ニーモニック
- ニーモニックは処理内容に応じて各機械語命令に与えられた文字列・命令語。機械語のオペコードに相当する
- オペランドは命令の対象・引数
機械語との対応
アセンブリ言語と機械語の表現は一対一で対応している。これに対して、高級言語とアセンブリ言語は一対一対応していない。コンパイラの実装によって結果は異なる。
アセンブリ言語と機械語を比較する読み方。gccの-aオプションでコンパイルすると、機械語を見られる。
18 000a 05C80100 addl $456, %eax 18 00
これは、メモリ0018番地に、16進数で0x05, 0xc8, 0x01, 0x00という5バイトの機械語を生成したことを意味する。
- 1つのアセンブリ言語の命令につき1バイト(ret)から5バイト(movl, addl)の機械語が生成されている
.
から始まる命令に対しては機械語が生成されていない
Reference
Simple 8-bit Assembler Simulator in Javascript
シミュレータ。可視化がわかりやすい。
インテル関連ドキュメント・リンク集
インテル関連のリンク集。
Archives
DONE アセンブリに触れてみよう - Qiita
アセンブリの解説。
- raxのA = Accumulator
- rbxのB = Base address
- rcxのC = Count register
- rdxのD = Data register
その他のレジスタ。
- ripのipはInstruction Pointer。次に実行する命令のアドレスを記録する
- rbpのbpはBase Pointer。現在の処理のスタックフレームの底部分のアドレスを記録する
- rspのspはStack Pointer。スタック領域に積まれているデータのうち、一番小さいアドレスを記録する
DONE 独習アセンブラ 新版 | 大崎 博之 |本 | 通販 | Amazon
IA-32アーキテクチャの章までちゃんと読んだ。あとはいろんなアーキテクチャの解説があったが、そこはざっくりとしか読んでいない。
メモ。
- 「アセンブリ言語のプログラムを、アセンブラを使ってアセンブルする」というのが正しい用法になる
- アセンブリ: 組み立てるという「行為」
- アセンブル: 組み立ての「動作」
- アセンブラ: 組み立てる「人や物」
- gccコマンドそのものはCコンパイラの本体ではなく、Cコンパイラのフロントエンドになっている
- gcc実行の流れ。コンパイル・アセンブル・リンク
- C言語(ソースコード)
- アセンブリ言語(ソースコード)
- 機械語(オブジェクトファイル)
- 機械語(実行ファイル)
- ビットという名前は Binary digIT から来ている
- フランス、ドイツ、イタリアでは小数の区切りにはカンマを用いる
- 文字情報を含んだバイナリファイルやバイナリデータを扱うプログラムを書いたり読んだりする場合には、文字の表現に対する理解が不可欠
- 制御文字は図形を表示するためのものではない。端末エミュレータ上で出力しても画面には何も表示されない。反対に、図形を表示するための文字をグラフィック文字という
- ASCII
- JISキーボードの数字キーの記号配列は、ASCIIの範囲に対応している
&
の文字コードが知りたい- JISキーボードを見ると
&
は6
キーにある - したがって
&
の文字コードは0x20 + 6 = 0x26
- 0x30 ~ 0x39 には数字の0~9が配置されている。なので数字の文字コードは 0x30 + 数 とわかる
- 0x41 ~ 0x5a には英大文字A~Zが配置されている。Aは1文字目のアルファベットなので、Aの文字コードは0x40 + 1 = 0x41。Cは3文字目のアルファベットなので、Cの文字コードは0x40 + 3 = 0x43
- 0x61 ~ 0x7a には英小文字a~zが配置されている。Aとaの文字コードを2進数で表記すると1ビットだけが異なるように意図して配置されている
- つまり配置として、多くの記号の文字コード < 英大文字の文字コード < 小文字の文字コードとなる。なので、文字コードでソートしたときに記号、大文字、小文字という順番になる
- タブは制御文字とグラフィック文字の両方の側面を持っている
- JISキーボードの数字キーの記号配列は、ASCIIの範囲に対応している
- 制御文字LFはラインフィードの意味。タイプライタ時代に用紙を読み込む指示をしていたから。当時のテレタイプには行を送るだけのものと、行を送ったあとキャリッジリターン(行頭に移動)するものがあった。Unixではラインフィードを「改行(行送り+行頭に移動」の意味で解釈している、といえる
- 一方でWindowsでの改行は制御文字CRとLFの2文字である。これはラインフィードを「単なる行送り」の意味で解釈しているといえる。キャリッジリターンCRで行頭に戻して、その後ラインフィードLFで1行進めている、といえる
- キャリッジリターンという言葉は昔のタイプライタから来ている。キャリッジはタイプライタの用紙を動かす部分の名称。タイプライタは活字が刻印されたハンマーを打ち付けることによって印字する。ハンマーではなく用紙のほうを移動させる。キャリッジリターンはキャリッジを戻すという操作を意味している。キャリッジを戻すことにより、用紙のほうを移動させるという仕組みになっている
- DELが 0x7f(0b1111111)に配置されているのは、紙テープがすべて穿孔されていれば間違いがあり意味のないデータとして取り扱っていたため
- Unicode
- 世界中の言語で使用されている文字を統一的に扱う規格であるUnicodeと、Unicodeの文字エンコーディング体系であるUTF-8やUTF-16
- Unicodeではそれぞれの文字に割り当てられている整数をコードポイントと呼ぶ。例えば「あ」のコードポイントは0x3042
- 文字とUnicodeのコードポイントが必ずしも1対1で対応しない。アクセントや濁音、半濁音がついた文字は複数の表現がある
- Unicodeを使って文字をバイト列として表現するためには、符号化文字の集合に加えて、文字エンコーディング体系が必要になる
- UTF-32, UTF-16, UTF-8のいずれかでエンコードされたバイト列を見ても、それがどの文字エンコード体系でエンコードされたのかはわからない。区別のために、BOM(Byte Order Mark)を使用できる
- Unicodeの特殊文字BOM(コードポイントU+FEFF)をUnicodeテキストの先頭にうめこんでおくと、エンコードしたバイト列から、このバイト列はどの文字エンコーディング体系でエンコードされたものかわかるようになる
- Unicodeテキストの先頭にU+FEFFがあればそれはBOMを意味するが、Unicodeテキストの2文字目以降にU+FEFFがあれば、それは「幅がゼロの改行できない空白」を意味する。
- なのでBOMを表示・印刷しても何も見えない
- ドットから始まる命令はアセンブラの疑似命令であり、ディレクティブとも呼ばれる
- アセンブラの疑似命令は、CPUが実行する命令ではなく、プログラマからアセンブラへの指示
- .gdbinitでプリント対象の変数を指定できる
$ gcc -q add
オプションは起動時のメッセージを抑制する- C言語の規格では、各型の最小のビット数と、それぞれの型のビット数の大小関係のみが規定されていて、実際のビット数はそれぞれの処理系によって異なる
- ドットから始まる命令はアセンブラの疑似命令。
.L
から始まるラベルは局所ラベル - アセンブリ言語によるプログラミングにおけるスタックの利用法
- 計算結果をスタックに一時保存する。複雑な計算をするとレジスタの数が不足するため。スタックはメモリ上に確保されるので、広大なメモリ空間を活用できる
- 関数やサブルーチンと呼ばれる小さなまとまりのプログラムを実現するためにスタックを利用する
- 関数呼び出しを実現するために必要なこと
- 呼び出しにどのように戻るか、リターンアドレスの管理
- 呼び出し元から関数に引数をどのように渡すか
- 関数から呼び出し元に返り値をどのように返すか
- コンピュータ内部で情報を保存できるのはレジスタとメモリしかない。したがって情報の受け渡しには、レジスタを使うかメモリを使うかの二択になる
call アドレス
で、リターンアドレスをスタックにプッシュし、指定されたアドレスにジャンプできるret
で、スタックからリターンアドレスを取り出して、そのアドレスにジャンプできる- スタックの一番上のアドレス = レジスタESP
cmpl $0x1, 0x10(%esp)
- $0x1 → 整数1
- 0x10(%esp) → ESPレジスタの値+0x10
- を比較する、ということ
- サブルーチンの呼び出しアドレスは、最終的に隣家によって結合された時点で確定する。オブジェクトファイルがまだリンクされていないと、アドレスが未確定のままになっている
- writeシステムコールでは、レジスタEBXでファイルディスクリプタを、レジスタECXで文字列が格納されているアドレスを、レジスタEDXで出力する文字列長を指定する
- GNUアセンブラ(GAS)は、Cコンパイラの裏方で動作するアセンブラとして設計された
- CコンパイラはC言語で書かれたプログラムをコンパイルし、アセンブリ言語のプログラムを生成する
- GASは、Cコンパイラが生成したアセンブリ言語のプログラムを機械語に変換する
main
,.LFB0
,i
はシンボル。L
や.L
から始まるシンボルは局所シンボルmain:
はラベル。ラベルは末尾にコロンを付与する- 局所ラベルは「数字b」「数字f」によって参照できる
- ピリオドから始まる命令はGASの疑似命令(アセンブラに対する指示)
- アセンブラにおけるセクションとは、ある連続したアドレスの範囲
- GASはそれぞれのセクションを0番地から開始する。機械語のプログラムを格納するセクションも、プログラムが使用するデータを格納するセクションも、すべて0番地から開始する。機械語のプログラムも、プログラムが使用するデータも、最終的にはメモリ上のどこかのアドレスに配置されて実行される
- アセンブラが生成したオブジェクトファイルを、実際の適切なメモリ上の番地に配置するのはリンカの役割。リンカが、各セクションを配置するアドレスを決定することを再配置とよぶ。GASはそれぞれのセクションが実際のメモリのどこに配置されるかには関与しない
- GASは機械語のプログラムを.textセクションに格納する
- .dataセクションはプログラムが使用するデータを格納する
- .bssセクションは初期化されていないデータ領域のためのセクション。初期化されていないデータの中身は保持しなくてよく、大きさだけ持っていればよいのでデータ削減になる
- C言語における関数呼び出しは、C言語で書かれたプログラムを呼び出しているのではなく、もともとはC言語で書かれたプログラムだったが、現在はコンパイルが済んで機械語になっているプログラムを呼び出している
movl $123, %eax
とあるときの$123を即値という。アドレスの123番地ではなく、123という「値そのもの」を意味するから。値が指す先にあるものではなく、値が即時に表すものなので即値(immediate value)- ビットシフト命令はハードウェアで実行しなければならない処理がはるかに簡単。乗数や除数が2のべき乗であれば、MUL命令やDIV命令を使用せずに、ビットシフト命令を用いることで高速な計算が可能になる
- NOP命令は何もしない命令。CPUはNOP命令を単に読み飛ばす。NOP命令は、バイナリファイルを書き換えて、特定の命令を無効化するのに使う
- CALL命令はリターンアドレスをスタックにプッシュしたあとでEIPレジスタの値を変更する
- IA-32アーキテクチャでアドレスは32ビット(4バイト)なので、スタックポインタの値を4だけ減らす
DONE x86-64プロセッサのスタックを理解する - Qiita
図を交えたわかりやすい解説。