アセンブラ命令
アセンブラとアセンブラ命令
- アセンブラ (assembler)はアセンブリコード (
foo.s)を オブジェクトファイル (foo.o)に変換するプログラムです.- 例えば,
gcc -c foo.sとするとアセンブルできます.gcc -cは内部でasコマンドを呼んでいて,これがアセンブラ本体です. (gccに-vオプションを付けると,asコマンドの実行が見えます).
- 例えば,
- アセンブラ命令 (assembler directive)はアセンブラへの指示です.
- 例えば,
.textはアセンブラに「出力先を.textセクションに切り替えろ」と 指示しています. - GNUアセンブラのアセンブラ命令はすべてドット
.で始まります.
- 例えば,
- アセンブラ命令はCPUが実行しない命令なので, 疑似命令(pseudo instruction)とも呼ばれます. (が,分かりにくい用語なので本書では使いません).
アセンブラ命令と機械語命令
| 例 | 何が実行 | いつ実行 | バイナリファイル中に | |
|---|---|---|---|---|
| アセンブラ命令 | .text | アセンブラ | アセンブル時 | 残らない |
| 機械語命令 | addl $5, %eax | CPU | a.out実行時 | 残る |
- アセンブラがアセンブリコード(
*.s)からオブジェクトファイル(*.o)を作る際に,- アセンブラ命令(例:
.text)に従って処理をします.例えば,addl $5, %eaxを バイト列に変換した結果0x83 0xC0 0x05を.textセクションに出力します. - その結果,アセンブラ命令
.textはオブジェクトファイルには残りません. (オブジェクトファイル中に.textセクションはありますが, これはアセンブラ命令.textではありません).
- アセンブラ命令(例:
- 一方,機械語命令
addl $5, %eaxはオブジェクトファイルに残っています (バイト列0x83 0xC0 0x05と見た目は変わっていますが).a.outを実行したときに,CPUがこの機械語命令を実行します.
アセンブラの主な仕事
概要
アセンブラの主なお仕事は大きく次の4つです.
-
機械語命令やデータを2進数表現に変換する
- 例:
pushq %raxを0x50に変換する - 例:
.string "%d\n"を0x25 0x64 0x0A 0x00に変換する (.stringは自動的にヌル文字0x00を最後に付加します) - アセンブラにとって,機械語命令もデータも 「2進数にして出力する」という意味で,どちらも単なるデータです.
- 例:
-
変換した2進数を指定されたセクションに出力する
- アセンブラは各セクションごとにロケーションカウンタ (location counter)
を持っています.ロケーションカウンタは「機械語命令やデータを次に出力する際の
アドレス」です.
.align 8は「次に出力するアドレスを8の倍数にする(次の出力を8バイト境界にする)」というアセンブラ命令ですが, ロケーションカウンタという言葉を使うと 「ロケーションカウンタを8の倍数になるように増やす」と言い換えられます.
- アセンブラは各セクションごとにロケーションカウンタ (location counter)
を持っています.ロケーションカウンタは「機械語命令やデータを次に出力する際の
アドレス」です.
-
記号表 (symbol table)を作って,ラベルをアドレスに変換する
-
最後に変換したデータをバイナリ形式(LinuxではELF形式)にしてファイル出力する
セクションへの出力
セクションとは
バイナリファイルの構造はざっくり以下の図のようになっています.
- 基本的に,バイナリ (オブジェクトファイル
*.oや実行可能ファイルa.out)は セクション (section)という単位で区切られていて,それぞれ別の目的でデータが格納されます. - ヘッダ (header)は目次の役割で「どこにどんなセクションがあるか」という情報を保持しています.
代表的なセクション
| セクション名 | 説明 |
|---|---|
.text | 機械語命令(例:pushq %rbp)を置くセクション |
.data | 初期化済みの静的変数の値(例:0x1234)を置くセクション |
.bss | 未初期化(実行時に0で初期化する)の静的変数の値を置くセクション |
.rodata | 読み込みのみ(read only)の値(例:"hello\n")を置くセクション |
各セクションに出力する例
例えば,以下のアセンブリコードfoo.sがあるとします
(.rodataセクションを指定する際は,.sectionが必要です).
# foo.s
.text # .textセクションに出力せよ
pushq %rbp
movq %rsp, %rbp
.data # .dataセクションに出力せよ
.long 0x11223344
.section .rodata # .rodataセクションに出力せよ
.string "hello\n"
このfoo.sをアセンブラが処理すると以下になります.
-
pushq %rbpを2進数にすると0x55,movq %rsp, %rbpを2進数にすると0x48 0x89 0xe5なので, これら合計4バイトを.textセクションに出力します. -
.dataは「.dataセクションに出力せよ」,.long 0x11223344は 「0x11223344を4バイトの2進数として出力せよ」という意味です.0x11223344を2進数にすると0x44 0x33 0x22 0x11なので これら4バイトを.dataセクションに出力します. (出力が逆順になっているように見えるのは x86-64がリトルエンディアンだからです.) -
.section .rodataは「.rodataセクションに出力せよ」,.string "hello\n"は 「文字列"hello\n"をASCIIコードの2進数として出力せよ」という意味です."hello\n"を2進数にすると0x68 0x65 0x6c 0x6c 0x64 0x0a 0x00なので, これら7バイトを.rodataセクションに出力します. (最後の0x00はヌル文字'\0'です.C言語では文字列定数の最後に自動的に ヌル文字が追加されますが,アセンブリ言語では必ずしもそうではありません..stringはヌル文字を自動的に追加します. 一方,(ここでは使っていませんが).asciiはヌル文字を自動的に追加しません).ASCIIコードは
man asciiで確認できます.
セクションを確認する
-
セクションの内容は
objdump -hやreadelf -Sコマンドで表示できます. -
objdump -hの例と読み方
$ gcc -g -c add5.c
$ objdump -h add5.o
add5.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000013 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000053 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000053 2**0
ALLOC
3 .debug_info 00000066 0000000000000000 0000000000000000 00000053 2**0
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
4 .debug_abbrev 0000004d 0000000000000000 0000000000000000 000000b9 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
5 .debug_aranges 00000030 0000000000000000 0000000000000000 00000106 2**0
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
6 .debug_line 0000004f 0000000000000000 0000000000000000 00000136 2**0
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
7 .debug_str 00000093 0000000000000000 0000000000000000 00000185 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
8 .debug_line_str 00000089 0000000000000000 0000000000000000 00000218 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
9 .comment 0000002c 0000000000000000 0000000000000000 000002a1 2**0
CONTENTS, READONLY
10 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000002cd 2**0
CONTENTS, READONLY
11 .note.gnu.property 00000020 0000000000000000 0000000000000000 000002d0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
12 .eh_frame 00000038 0000000000000000 0000000000000000 000002f0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
| 欄 | 例 | 説明 |
|---|---|---|
| Idx | 1 | セクションの通し番号 |
| Name | .text | セクションの名前 |
| Size | 00000013 | セクションのサイズ (16進数,バイト) |
| VMA | 00000000 | 実行時のセクションのアドレス (virtual memory address, 16進数) |
| LMA | 00000000 | ロード時のセクションのアドレス (load memory address, 16進数) |
| File Off | 00000040 | ファイルオフセット(16進数,バイト) |
| Algn | 2**0 | セクションのアラインメント制約(バイト), 2**nは\(2^n\)を意味 |
| 属性 | 説明 |
|---|---|
| CONTENTS | このセクションには中身がある(つまり中身が空のセクションもある) |
| ALLOC | ロード時にこのセクションのためにメモリを割り当てる必要がある |
| LOAD | このセクションは実行するためにメモリ上にロードする必要がある |
| READONLY | メモリ上では「読み込みのみ許可(書き込み禁止)」と設定する必要がある |
| CODE | このセクションは実行可能な機械語命令を含んでいる |
| RELOC | このセクションは再配置情報を含んでいる |
readelf -Sの例と読み方
$ gcc -g -c add5.c
$ readelf -S add5.o
There are 21 section headers, starting at offset 0x640:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000013 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000053
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000053
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .debug_info PROGBITS 0000000000000000 00000053
0000000000000066 0000000000000000 0 0 1
[ 5] .rela.debug_info RELA 0000000000000000 00000410
00000000000000c0 0000000000000018 I 18 4 8
[ 6] .debug_abbrev PROGBITS 0000000000000000 000000b9
000000000000004d 0000000000000000 0 0 1
[ 7] .debug_aranges PROGBITS 0000000000000000 00000106
0000000000000030 0000000000000000 0 0 1
[ 8] .rela.debug_[...] RELA 0000000000000000 000004d0
0000000000000030 0000000000000018 I 18 7 8
[ 9] .debug_line PROGBITS 0000000000000000 00000136
000000000000004f 0000000000000000 0 0 1
[10] .rela.debug_line RELA 0000000000000000 00000500
0000000000000060 0000000000000018 I 18 9 8
[11] .debug_str PROGBITS 0000000000000000 00000185
0000000000000093 0000000000000001 MS 0 0 1
[12] .debug_line_str PROGBITS 0000000000000000 00000218
0000000000000089 0000000000000001 MS 0 0 1
[13] .comment PROGBITS 0000000000000000 000002a1
000000000000002c 0000000000000001 MS 0 0 1
[14] .note.GNU-stack PROGBITS 0000000000000000 000002cd
0000000000000000 0000000000000000 0 0 1
[15] .note.gnu.pr[...] NOTE 0000000000000000 000002d0
0000000000000020 0000000000000000 A 0 0 8
[16] .eh_frame PROGBITS 0000000000000000 000002f0
0000000000000038 0000000000000000 A 0 0 8
[17] .rela.eh_frame RELA 0000000000000000 00000560
0000000000000018 0000000000000018 I 18 16 8
[18] .symtab SYMTAB 0000000000000000 00000328
00000000000000d8 0000000000000018 19 8 8
[19] .strtab STRTAB 0000000000000000 00000400
000000000000000d 0000000000000000 0 0 1
[20] .shstrtab STRTAB 0000000000000000 00000578
00000000000000c6 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
| 欄 | 例 | 説明 |
|---|---|---|
| [Nr] | [1] | セクションの通し番号 |
| Name | .text | セクションの名前 |
| Type | PROGBITS | セクションのタイプ (以下参照) |
| Address | 0000000000000000 | セクションのアドレス |
| Offset | 00000040 | ファイルオフセット(16進数,バイト) |
| Size | 0000000000000013 | セクションのサイズ (16進数,バイト) |
| EntSize | 0000000000000000 | 表中の固定長のエントリのサイズ (エントリがない場合は0) |
| Flags | AX | セクションのフラグ (以下参照) |
| Link | 0 | 関連するセクションの通し番号([Nr]) (存在しない場合は0) |
| Info | 0 | 関連情報 (ない場合は0) |
| Align | 1 | セクションのアラインメント制約(バイト) |
| セクションのタイプ | 説明 |
|---|---|
| NULL | このセクションは使われていない |
| PROGBITS | このセクションはプログラムがフォーマットと意味を決める情報を含む |
| NOBITS | 未初期化の領域を含む (ファイル中ではサイズゼロ) |
| RELA | このセクションは再配置情報を含む (調整用の値(addend)を含む) |
| NOTE | 言語処理系が定義・使用するメモ書き |
| SYMTAB | このセクションは記号表 (symbol table) |
| STRTAB | このセクションは文字列表 (string table) |
| フラグ | 説明 |
|---|---|
| W (write) | このセクションは実行時に書き込み可能にしておく必要がある |
| A (alloc) | ロード時にこのセクションのためにメモリを割り当てる必要がある |
| X (execute) | このセクションは実行可能な機械語命令を含む |
| M (merge) | このセクション中のデータは重複を避けるためにマージ可能 |
| S (strings) | このセクションはヌル終端の文字列を含む |
| I (info) | このセクションはセクションのタイプに依存した付加情報を保持してる |
| L (link order) | このセクションはリンカに対して特別な順序を要求する |
| O (extra OS processing required) | このセクションはOS固有の特別な処理が必要である |
| G (group) | このセクションはセクショングループのメンバーである |
| T (TLS) | このセクションはスレッドローカルストレージを持つ |
| C (compressed) | このセクションの内容は圧縮されている (AやNOBITSとの併用はNG) |
| x (unknown) | このセクションは不明なセクションである |
| o (OS specific) | OS固有の意味を持つ |
| E (exclude) | このセクションは参照もメモリ割り当てもされなければ削除される |
| D (mbind) | このセクションは特別なメモリ領域に置く必要がある.Infoがそのメモリタイプを示す |
| l (large) | このセクションは(2GB以上の)largeコードモデルである |
| p (processor specific) | プロセッサ固有の意味を持つ |
readelf -tで,より詳細なセクション情報を表示可能readelf -nでNOTEセクションの内容を表示可能
記号表とアドレスへの変換
# sym-main.s
.text
.globl main
.type main, @function
main:
movl x(%rip), %eax # ラベル x の参照
ret
.size main, .-main
# sym-sub.s
.globl x
.data
.align 4
.type x, @object
.size x, 4
x: # ラベル x の定義
.long 999
$ gcc -c sym-main.s
$ gcc -c sym-sub.s
$ gcc sym-main.o sym-sub.o
$ nm sym-main.o
0000000000000000 T main
❶ U x
$ nm sym-sub.o
❷ 0000000000000000 D x
$ nm a.out | egrep ' x'
❸ 0000000000004010 D x
$ objdump -D ./a.out
0000000000001129 <main>:
1129: 8b 05 e1 2e 00 00 mov ❹ 0x2ee1(%rip),%eax # 4010 <x>
❻ 112f: c3 ret
(中略)
0000000000004010 <x>:
❺ 4010: e7 03 out %eax,$0x3
$ bc
obase=16
ibase=16
4010-112F
❼ 2EE1
- アセンブラは記号表を作り,ラベルを見つけるたびに記号表に加えます. 記号表はリンク時にも使うので,アセンブラは記号表をオブジェクトファイルに埋め込みます. 最終的に,記号表の情報を使って,ラベルをアドレスに変換します.
- 例えば,上記の例ではラベル
xを記号表で管理して,最終的にアドレス0x4010に変換しています.sym-main.sのmovl x(%rip), %eaxではラベルxが登場するので, アセンブラはラベルxを記号表に加えます.sym-main.s中には定義が無いので,nmコマンドで記号表を見ると「xは未定義 (❶ U x)」となっています.- 一方,
sym-sub.s中にラベルxの定義があるので,nmで調べると,「xの(仮の)アドレスは0番地 ❷」と表示されます. sym-main.oとsym-sub.oをリンクしたa.outを調べると, 「xのアドレスは0x4010番地 ❸」となってます.objdump -Dで(.dataセクションも含めて)逆アセンブルすると,xのアドレスは確かに❹0x4010番地となっていて,movl x(%rip), %eax中のラベルxは相対アドレス❹0x2EE1に なっています(❺ 0x4010 - ❻ 0x112F = ❼ 0x2EE1).
バイナリ形式として出力
- ここまでの処理で,アセンブラはセクション別に2進数のバイト列を生成しています.
- アセンブラはこれらのバイト列をバイナリ形式のフォーマットに従って ファイルに出力します.
- 本書の範囲ではバイナリ形式の詳細を知る必要はありませんが,
以下でELFバイナリ形式の全体の構造だけ説明します.
この知識がないと
readelfコマンドを使う際に「ヘッダが3種類ある」と混乱するからです.
ELFバイナリ形式の構造
- ELFには3種類のヘッダがあります
| ヘッダの種類 | 表示コマンド | 説明 |
|---|---|---|
| ELFヘッダ | readelf -h | ELFファイル全体の目次とメタ情報.必ずファイル先頭に存在 |
| セクションヘッダ | readelf -S | セクションの目次 |
| プログラムヘッダ | readelf -l | セグメントの目次 |
- ELFのセクションはリンクのための処理の単位です.
- ELFのセグメントは複数のセクションが1つになったもので,
実行時にメモリにロードするための処理の単位です.
- セクションと区別するために,「セグメント」という異なる名前が付いているがかえって紛らわしい.
- ELFヘッダ以外は,配置する場所や順番に決まりはないです. (セクションヘッダは(ヘッダという名前なのに)ファイルの最後に配置されることが多いです). セクションヘッダとプログラムヘッダの位置(オフセット)は ELFヘッダ中に書かれています.
ELFとDWARF
バイナリ形式 ELF は executable linkage format の略です. 「北欧神話でおなじみのエルフ(ELF)と来ればドワーフ(DWARF)も欲しいでしょ」 というダジャレで, (元々はELF形式のために)DWARFという名前のデバッグ情報形式が生まれたようです.
GNUアセンブラの文法
文
-
GNUアセンブラの文(statement)はアセンブリコードの構成要素であり, 上記の6つのいずれもが1つの文になります.
-
文は改行かセミコロン
;で区切ります.-
セミコロンで区切れば,複数の文を1行に書いて良いです. 以下はどちらでもOKです
pushq %rbp movq %rsp, %rbppushq %rbp; movq %rsp, %rbp
-
コメント
-
GNUアセンブラのコメントは(C言語と同様に)プログラムのメモ書きです. アセンブラは単に無視します.
-
行コメント: シャープ記号
#から行末までがコメントになります# これは行コメントです注: 行コメントに使う記号はアーキテクチャ依存です. 例えば,x86-64は
#ですが,ARMは@,H8は;,SPARCは!です. -
ブロックコメント: C言語と同じで
/*から*/までがコメントです. C言語と同様にブロックは入れ子禁止です(ブロックコメントの中にブロックコメントは書けません)/* これはブロックコメントです. 複数行でもOKです */ -
入れ子のコメントを書くには,C前処理命令
#if 0と#endifを使います.- ただし,ファイル拡張子を(大文字の)
.Sにする必要があります. - 拡張子を
.SにするとC前処理が実行されるので,#defineや#includeも使えます.
#if 0 これはC前処理命令を使ったコメントです. これは入れ子にできます. #endif - ただし,ファイル拡張子を(大文字の)
定数
| 種類 | 説明 | 例 |
|---|---|---|
| 10進数定数 | 0で始まらない | pushq $74 |
| 16進数定数 | 0xか0Xで始まる | pushq $0x4A |
| 8進数定数 | 0で始まる | pushq $0112 |
| 2進数定数 | 0bか0Bで始まる | pushq $0b01001010 |
| 文字定数 | '(クオート)で始まる | pushq $'J |
'(クオート)で囲む | pushq $'J' | |
\バックスラッシュでエスケープ可 | pushq $'\n | |
| 文字列定数 | "(ダブルクオート)で囲む | .string "Hello\n" |
- 上の例の最初の4つの定数は全部,値が同じです
- GNUアセンブラでは文字定数の値はASCIIコードです.
上の例では,文字
'J'の値は74です. - バックスラッシュでエスケープできる文字は,
\b,\f,\n,\r,\t,\",\\です. また\123は8進数,\x4Fは16進数で指定した文字コードになります. - 文字列定数では,
.stringと.ascizは自動的にヌル文字終端しますが,.asciiはヌル文字終端しません(必要なら明示的に\0と書く必要があります). 文字列定数は即値や変位には使えません.
式
leaq main+10(%rip), %rax # 加算
movq $1<<12, %rax # 左シフト
movq $1024*1024, %rax # 乗算
-
定数やラベルを書ける場所 (即値や変位)には式を書けます.
-
ただし,式には静的に(アセンブル時に)計算できるものだけが書けます.
- レジスタやメモリの値は参照できません.
-
上の例では加算,左シフト,乗算を使った例です.
-
単項演算子
| 演算子 | 意味 |
|---|---|
- | 2の補数による負数 |
~ | ビットごとの反転 (1の補数) |
- 2項演算子
| 演算子 | 意味 |
|---|---|
* | 乗算 |
/ | 除算 |
% | 剰余 |
<< | 左シフト |
>> | 右シフト |
| 演算子 | 意味 |
|---|---|
| | ビットOR |
& | ビットAND |
^ | ビットXOR |
! | ビットOR NOT (a | ~bと同じ) |
| 演算子 | 意味 |
|---|---|
+ | 加算 |
- | 減算 |
== | 等しい |
!= | 等しくない |
<> | 等しくない |
< | 小さい |
> | 大きい |
<= | 以下 |
>= | 以上 |
| 演算子 | 意味 |
|---|---|
&& | 論理AND |
|| | 論理OR |
- 一番上の表が最も優先度が高く,順番に下に行くほど優先度が下がります. 同じ表中の演算子同士は同じ優先度です.
- 述語演算子は,真の時は
-1(つまり全ビットが1),負の時は0(つまり全ビットが0)を返します. - 2023/9:
=が入ってる演算子を使うと,foo.s:9: Error: invalid character '=' in operand 1というエラーがでます.なぜなんだぜ
ラベルと識別子
シンボル
- シンボルはGNUアセンブラが使う一種の変数で,値を保持できます.
- GNUアセンブラのシンボル名に使える文字は英語の大文字と小文字,数字,
_,$,.です.- ただし,シンボル名は数字で始まってはいけません
- 大文字と小文字は区別します (case-sensitive)
$と.は使わない方が良いでしょう (即値やアセンブラ命令と紛らわしいから)
- シンボルは主にラベルに使います.
- ラベルはアセンブラが自動的にアドレスを割り当てます.つまり値がアドレスなシンボルがラベルです.
- アセンブラ命令
.setでシンボルの値をセットできます(あまり使いませんが)
.set x, 999 # シンボルxの値を999にする
ラベル
- シンボルの直後にコロン
:が来ると (例:add5:),それはラベルの定義になります. - そのラベルの値は
add5:を処理した時のロケーションカウンタ (次に出力するアドレス)になります. つまり,アセンブラ が自動的にラベルをアドレスに変換します. - 変換するアドレスが絶対アドレスか相対アドレスかはGNUアセンブラが自動的に判断します (例:
leaq foo(%rip), %raxのfooは相対アドレスになります) - ラベルは関数名や変数名などの識別子名 (identifier),ジャンプ先を表すために使います
- アドレスを書ける場所 (即値や変位)にはラベルを書いて良い
(例:
movq %rax, foo (%rip))
ラベルのスコープ
- 同じファイル中に同じラベル名があってはいけません(二重定義). グローバルではないラベルのスコープはそのファイル.
foo:
foo: # 二重定義でNG
- グローバルなラベルは他のファイルに同じラベル名があってもいけません. グローバルなラベルのスコープはリンクするファイルすべて.
# file1.s
.globl foo
foo:
# file2.s
.globl foo
foo: # 二重定義でNG
- 片方がweakなラベルなら二重定義でもOK
- weakラベルと同名のラベルがあれば,(エラーなしで)weakではない方のラベルが使われる
- weakラベルと同名のラベルがなければ,(エラーなしで)weakラベルが使われる
- weakラベルは「他の定義に上書きされても良い,デフォルトの関数や変数」を定義するのに便利
# file1.s
.globl foo
foo:
# file3.s
.weak foo
.globl foo
foo: # OK (二重定義にならない)
- C言語でも変数や関数をweakにできる
- GCC独自拡張機能である
__attribute__((weak))を使う
- GCC独自拡張機能である
// weak-main.c
#include <stdio.h>
__attribute__((weak)) void foo ()
{
printf ("I am weak foo\n");
}
int main ()
{
foo ();
}
// weak-sub.c
#include <stdio.h>
void foo ()
{
printf ("I am non-weak foo\n");
}
$ gcc -g weak-main.c
$ ./a.out
I am weak foo
$ gcc -g weak-main.c weak-sub.c
$ ./a.out
I am non-weak foo
記号表に含まれないラベル (.Lで始まるラベル)
// label.s
.text
.global foo1
foo1:
foo2:
.Lfoo3:
$ gcc -g -c label.s
$ nm label.o
0000000000000000 T foo1
0000000000000000 t foo2
- ELFバイナリの場合,
.Lで始まるラベルは記号表に含まれません. - コンパイラが出力するジャンプ先のラベルは
.Lで始まることが多いです (例:.LFB0,.LFE0,.L2). ジャンプ先のラベルはデバッグ等に不要なことが多いからです. - 上の例で
.Lfoo3は記号表に含まれないため,nmコマンドで出力されませんでした.
特別なドットラベル .
-
ドット
.は特別なラベルで,ロケーションカウンタ自身(つまり現在のアドレス)を表します. -
例:
fooの中身はfooのアドレスになりますfoo: .quad .foo: .quad foo # こう書いても同じ -
例:
.-add5は現在のアドレスからadd5のアドレスを引くので, 「関数add5の先頭から現在のアドレスまでの,機械語命令のバイト数」が計算できます.size add5, .-add5
数値ラベル
- 数値ラベル = 正の整数にコロン
:がついたもの - 数値ラベルは再定義が可能 (2重定義にならない)
- 参照時には
fかbを付けるb(backward)は後方で最初の同名のラベルを参照f(forward)は前方で最初の同名のラベルを参照
- インラインアセンブリコードで使うと 2重定義を気にせず使えるので便利.
変数名とラベル
int x1 = 111;
int main ()
{
}
.globl x1
.data
.align 4
.type x1, @object
.size x1, 4
x1:
.long 111
- 静的な変数
x1はそのままアセンブリコードのラベルx1になります. ラベルx1の値は「変数x1の実体が置かれるメモリ領域の先頭番地」になります.
// var2.c
void foo ()
{
static int x2 = 222;
}
int main ()
{
static int x2 = 333;
}
.data
.align 4
.type x2.1, @object
.size x2.1, 4
x2.1:
.long 222
.align 4
.type x2.0, @object
.size x2.0, 4
x2.0:
.long 333
- 関数内の静的変数
x2は,同名の変数と区別するため,コンパイラがx2.0,x2.1と数字を付け足しています.
.text
.globl main
.type main, @function
main:
-
関数
mainもそのままアセンブリコードでラベルmainになります. ラベルmainの値は「関数mainの実体が置かれるメモリ領域の先頭番地」になります. -
staticではない局所変数(自動変数)は記号表には含まれません.- 同じ関数が呼ばれるたびに,局所変数の実体がスタック上で確保されるため, アドレスが1つに確定しないからです.
- 局所変数はコンパイル時にベースポインタ (
%rbp)やスタックポインタ (%rsp)との 相対アドレスが確定します.局所変数はこの相対アドレスを使ってアクセスします.
アセンブラ命令
アセンブラ命令の種類
| 種類 | 例 | 意味 |
|---|---|---|
| セクション指定 | .text | 出力先を.textセクションにせよ |
| データ出力 | .long 0x12345678 | 4バイトの整数値0x12345678の2進数表現を出力せよ |
| 出力アドレス調整 | .align 4 | 4バイト境界にアラインメント調整せよ (4の倍数になるようにロケーションカウンタを増やせ) |
| シンボル情報 | .globl main | シンボルmainをグローバルとせよ |
| その他 | .ident "GCC.." | (ELF形式の場合)文字列"GCC.."を.commentセクションに出力する |
アセンブラ命令.identで指定した情報が,
本当にバイナリ中の.commentセクションに入ってるかを確認します.
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
確認にはここではobjdumpコマンドを使います.
$ objdump -s -j .comment ./a.out
./a.out: file format elf64-x86-64
Contents of section .comment:
0000 4743433a 20285562 756e7475 2031312e GCC: (Ubuntu 11.
0010 342e302d 31756275 6e747531 7e32322e 4.0-1ubuntu1~22.
0020 30342920 31312e34 2e3000 04) 11.4.0.
-j .commentは「.commentセクションだけを出力せよ」,
-sは「出力するセクションの中身をすべてダンプする」という指示です.
確かに.commentセクションに入っていました.
セクション指定
| アセンブラ命令 | 例 | 説明 |
|---|---|---|
.text | .text | 出力先を.textセクションに変更 |
.data | .data | 出力先を.dataセクションに変更 |
.bss | .bss | 出力先を.bssセクションに変更 |
.section セクション名 | .section .rodata | 出力先を指定したセクション名に変更 |
| セクション | 役割 |
|---|---|
.text | 機械語命令列を保持 |
.data | 初期化済みの静的変数を保持 |
.bss | 未初期化の静的変数を保持 |
.rodata | 読み込みのみ(書き込み禁止)データを保持 (例: Cの文字列定数) |
- アセンブラ命令
.textや.dataなどは何度でも指定できます
Cの変数とセクションの対応
// var3.c
int x1 = 10; // コンパイル時に.dataセクションに確保
int x2; // コンパイル時に.bssセクションに確保
static int x3 = 10; // コンパイル時に.dataセクションに確保
static int x4; // コンパイル時に.bssセクションに確保
extern int x5; // 何も確保しない
int main (void)
{
int y1 = 10; // 実行時にスタック上に確保
int y2; // 実行時にスタック上に確保
static int y3 = 10; // コンパイル時に.dataセクションに確保
static int y4; // コンパイル時に.bssセクションに確保
extern int y5; // 何も確保しない
}
$ gcc -g var3.c
$ nm ./a.out
0000000000001129 T main
0000000000004010 D x1
0000000000004020 B x2
0000000000004014 d x3
0000000000004024 b x4
0000000000004018 d y3.0
0000000000004028 b y4.1
| シンボルタイプ | 説明 |
|---|---|
| T | .textセクション中のシンボル(関数名) |
| D | .dataセクション中のシンボル |
| B | .bssセクション中のシンボル |
| U | 参照されているが未定義のシンボル |
- 初期化済みの静的変数は
.dataセクションに置かれます - 未初期化の静的変数は
.bssセクションに置かれます - 大文字のシンボルタイプはグローバルスコープ,小文字はファイルスコープを表します
extern int x5;は「x5の型はintだよ」とコンパイラに伝えるだけなので,実体は確保しません(念のため)
データ配置
| アセンブラ命令 | 例 | 説明 |
|---|---|---|
.byte 式, ... | .byte 0x11, 0x22 | 1バイトデータを2つ (0x11と0x22)出力 |
.word 式, ... | .word 0x11 | 0x11を2バイトデータとして出力 |
.long 式, ... | .long 0x11 | 0x11を4バイトデータとして出力 |
.quad 式, ... | .quad 0x11 | 0x11を8バイトデータとして出力 |
| `.string" 文字列, ... | .string "hello" | 文字列"hello"を出力(ヌル文字を付加する) |
| `.ascii" 文字列, ... | .ascii "hello\0" | 文字列"hello\0"を出力(ヌル文字を付加しない) |
| `.asciz" 文字列, ... | .asciz "hello" | 文字列"hello"を出力(ヌル文字を付加) |
| `.fill データ数,サイズ,値 | .fill 10, 8, 0x1234 | 0x1234を8バイトデータとして10個出力(サイズと値は省略可能.省略時はそれぞれ1と0になる) |
出力アドレス調整 (アラインメント)
| アセンブラ命令 | 例 | 説明 |
|---|---|---|
.align 式 | .align 8 | 出力先アドレスを8バイト境界にせよ (ロケーションカウンタを8の倍数に増やせ) |
.p2align 式 | .p2align 3 | 出力先アドレスを\(2^3=8\)バイト境界にせよ |
.space 式 | .space 3 | ロケーションカウンタを3増やせ (.skipでも同じ) |
.zero 式 | .zero 3 | 3バイトのゼロを出力せよ |
.org 式 | .zero 510 | ロケーションカウンタを510にせよ(増やす方向のみ) |
.p2alignのp2は「2のべき乗 (power of 2)」を意味します.
シンボル情報
| アセンブラ命令 | 例 | 説明 |
|---|---|---|
.globl シンボル | .globl foo | シンボルfooをグローバルにせよ |
.type シンボル, 型 | .type main, @function | シンボルmainの型は関数とせよ |
.size シンボル, サイズ | .size main, .-main | シンボルmainのサイズを.-mainの計算結果(単位はバイト)とせよ |
.local シンボル | .local foo | シンボルfooをローカルにせよ |
.comm シンボル, サイズ, アラインメント | .comm foo, 4, 4 | .bssセクションにシンボルfooを作成せよ(サイズは4バイト,アラインメントは4バイト境界で) |
-
.typeでは型を@function(関数)か@object(普通のデータ)で指定します. (ELFの仕様によれば,@section,@file,@notypeなども使えます.これらはreadelf -sの出力にシンボルの型として出てきます). -
.commについて補足します.int x; // グローバル変数に対して,
gcc -Sは.globl x .bss .align 4 .type x, @object .size x, 4 x: .zero 4というアセンブリコードを出力します.一方,
static int y;に対して,
gcc -Sは# ❶ .bss .align 4 .type y, @object .size y, 4 y: .zero 4ではなく
# ❷ .local y .comm y,4,4を出力します(
.localが必要なのは.localが無いと.commで指定したシンボルがグローバルになってしまうからです)..comm y,4,4の最初の4は定義するyのサイズが4,次の4はアラインメント制約が4バイトであることを示しています. 実は❶と❷は全く同じ意味なのです(なのでグローバル変数の場合と統一して,❶を出力してくれた方が分かりやすくて良いと思うのですが…).
# var4.s
.data
.bss
.align 4
.type x, @object
.size x, 4
x:
.zero 4
.local y
.comm y,4,4
.text
.globl main
.type main, @function
main:
endbr64
ret
.size main, .-main
$ gcc -g var4.s
$ readelf -s ./a.out
symbol table '.symtab' contains 37 entries:
Num: Value Size Type Bind Vis Ndx Name
(中略)
12: 0000000000004014 4 OBJECT LOCAL DEFAULT 24 x ❸
13: 0000000000004018 4 OBJECT LOCAL DEFAULT 24 y ❹
$ nm ./a.out
(中略)
0000000000004014 b x ❺
0000000000004018 b y ❻
念のため,readelf -sとnmで記号表の中身を比べると❸〜❻は全く同じになりました.
AT&T形式とIntel形式
コマンド等での選択
gccでは,-masm=att(デフォルト),-masm=intelで出力するアセンブリコードの形式を選択可能です
$ gcc -S -masm=intel add5.c
$ cat add5.s
.intel_syntax noprefix
.text
.globl add5
.type add5, @function
add5:
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
mov eax, DWORD PTR -4[rbp]
add eax, 5
pop rbp
ret
.size add5, .-add5
-
アセンブリコード中では,アセンブラ命令
.att_syntax(デフォルト)と.intel_syntaxで,どちらの記法を使うか選択可能です -
objdump -dで逆アセンブルする際は,-M att(デフォルト)と-M intelで選択可能です.
$ objdump -d -M intel add5.o
add5.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <add5>:
0: f3 0f 1e fa endbr64
4: 55 push rbp
5: 48 89 e5 mov rbp,rsp
8: 89 7d fc mov DWORD PTR [rbp-0x4],edi
b: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
e: 83 c0 05 add eax,0x5
11: 5d pop rbp
12: c3 ret
AT&T形式とIntel形式の主な違い
- オペランドの順序,即値,レジスタの表記が異なります
| AT&T形式での例 | Intel形式での例 | 説明 | |
|---|---|---|---|
| オペランドの代入の順序 | addq $4, %rax | add rax, 4 | AT&T形式では左→右 Intel形式では右→左に代入 |
| 即値の表記 | pushq $4 | push 4 | AT&T形式では即値に$がつく |
| レジスタの表記 | pushq %rbp | push rbp | AT&T形式ではレジスタに%がつく |
- オペランドのサイズ指定方法が異なります
- AT&T形式では命令サフィックス(例えば,
movbのb)で指定します - Intel形式では
BYTE PTRなどの記法を使います
- AT&T形式では命令サフィックス(例えば,
| AT&T形式の サイズ指定 | Intel形式の サイズ指定 | メモリオペランドの サイズ | AT&T形式での例 | Intel形式での例 |
|---|---|---|---|---|
b | BYTE PTR | 1バイト(8ビット) | movb $10, -8(%rbp) | mov BYTE PTR [rbp-8], 10 |
w | WORD PTR | 2バイト(16ビット) | movw $10, -8(%rbp) | mov WORD PTR [rbp-8], 10 |
l | DWORD PTR | 4バイト(32ビット) | movl $10, -8(%rbp) | mov DWORD PTR [rbp-8], 10 |
q | QWORD PTR | 8バイト(64ビット) | movq $10, -8(%rbp) | mov QWORD PTR [rbp-8], 10 |
- メモリ参照の記法が違います
| AT&T形式 | Intel形式 | 計算されるアドレス | |
|---|---|---|---|
| 通常のメモリ参照 | disp (base, index, scale) | [base + index * scale + disp] | base + index * scale + disp |
%rip相対参照 | disp (%rip) | [rip + disp] | %rip + disp |
-
一部の機械語命令のニモニックが違います
- 変換系の命令
記法(AT&T形式) 記法(Intel形式) 何の略か 動作 c␣t␣c␣␣␣convert ␣ to ␣ %rax(または%eax,%ax,%al)を符号拡張
詳しい記法
(AT&T形式)詳しい記法
(Intel形式)例 例の動作 サンプルコード cbtwcbwcbtw%al(byte)を%ax(word)に符号拡張cbtw.s cbtw.txt cwtlcwdecwtl%ax(word)を%eax(long)に符号拡張cbtw.s cbtw.txt cwtdcwdcwtd%ax(word)を%dx:%ax(double word)に符号拡張cbtw.s cbtw.txt cltdcdqcltd%eax(long)を%edx:%eax(doube long, quad)に符号拡張cbtw.s cbtw.txt cltqcdqecltd%eax(long)を%rax(quad)に符号拡張cbtw.s cbtw.txt cqtocqocqto%rax(quad)を%rdx:%rax(octuple)に符号拡張cbtw.s cbtw.txt
- ゼロ拡張,符号拡張の命令
記法(AT&T形式) 記法(Intel形式) 何の略か 動作 movs␣␣op1, op2movsxop2, op1movsxdop2, op1move with sign-extention op1を符号拡張した値をop2に格納 movz␣␣op1, op2movzxop2, op1move with zero-extention op1をゼロ拡張した値をop2に格納
詳しい記法 例(AT&T形式) 例(Intel形式) 例の動作 サンプルコード movs␣␣r/m, rmovslq %eax, %rbxmovsxd rbx,eax%rbx=%eaxを8バイトに符号拡張した値movs-movz.s movs-movz.txt movz␣␣r/m, rmovzwq %ax, %rbxmovzx rbx,ax%rbx=%axを8バイトにゼロ拡張した値movs-movz.s movs-movz.txt
␣␣に入るもの何の略か 意味 bwbyte to word 1バイト→2バイトの拡張 blbyte to long 1バイト→4バイトの拡張 bqbyte to quad 1バイト→8バイトの拡張 wlword to long 2バイト→4バイトの拡張 wqword to quad 2バイト→8バイトの拡張 lqlong to quad 4バイト→8バイトの拡張