アセンブラ命令

アセンブラとアセンブラ命令

  • アセンブラ (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, %eaxCPUa.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 %rax0x50 に変換する
    • 例: .string "%d\n"0x25 0x64 0x0A 0x00に変換する (.stringは自動的にヌル文字 0x00を最後に付加します)
    • アセンブラにとって,機械語命令もデータも 「2進数にして出力する」という意味で,どちらも単なるデータです.
  • 変換した2進数を指定されたセクションに出力する

    • アセンブラは各セクションごとにロケーションカウンタ (location counter) を持っています.ロケーションカウンタは「機械語命令やデータを次に出力する際の アドレス」です. .align 8は「次に出力するアドレスを8の倍数にする(次の出力を8バイト境界にする)」というアセンブラ命令ですが, ロケーションカウンタという言葉を使うと 「ロケーションカウンタを8の倍数になるように増やす」と言い換えられます.
  • 記号表 (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進数にすると 0x55movq %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 -hreadelf -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
説明
Idx1セクションの通し番号
Name.textセクションの名前
Size00000013セクションのサイズ (16進数,バイト)
VMA00000000実行時のセクションのアドレス (virtual memory address, 16進数)
LMA00000000ロード時のセクションのアドレス (load memory address, 16進数)
File Off00000040ファイルオフセット(16進数,バイト)
Algn2**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セクションの名前
TypePROGBITSセクションのタイプ (以下参照)
Address0000000000000000セクションのアドレス
Offset00000040ファイルオフセット(16進数,バイト)
Size0000000000000013セクションのサイズ (16進数,バイト)
EntSize0000000000000000表中の固定長のエントリのサイズ (エントリがない場合は0)
FlagsAXセクションのフラグ (以下参照)
Link0関連するセクションの通し番号([Nr]) (存在しない場合は0)
Info0関連情報 (ない場合は0)
Align1セクションのアラインメント制約(バイト)

セクションのタイプ説明
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 -nNOTEセクションの内容を表示可能

記号表とアドレスへの変換

# 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.smovl x(%rip), %eax ではラベルxが登場するので, アセンブラはラベルxを記号表に加えます.sym-main.s中には定義が無いので, nmコマンドで記号表を見ると「xは未定義 (❶ U x)」となっています.
    • 一方,sym-sub.s中にラベルxの定義があるので, nmで調べると,「xの(仮の)アドレスは0番地 ❷」と表示されます.
    • sym-main.osym-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 -hELFファイル全体の目次とメタ情報.必ずファイル先頭に存在
セクションヘッダ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, %rbp
      
      pushq %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進数定数0x0Xで始まるpushq $0x4A
8進数定数0で始まるpushq $0112
2進数定数0b0Bで始まる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), %raxfooは相対アドレスになります)
  • ラベルは関数名や変数名などの識別子名 (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))を使う
// 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重定義にならない)
  • 参照時には fbを付ける
    • 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.0x2.1と数字を付け足しています.
    .text
    .globl  main
    .type   main, @function
main:
  • 関数mainもそのままアセンブリコードでラベルmainになります. ラベルmainの値は「関数mainの実体が置かれるメモリ領域の先頭番地」になります.

  • staticではない局所変数(自動変数)は記号表には含まれません.

    • 同じ関数が呼ばれるたびに,局所変数の実体がスタック上で確保されるため, アドレスが1つに確定しないからです.
    • 局所変数はコンパイル時にベースポインタ (%rbp)やスタックポインタ (%rsp)との 相対アドレスが確定します.局所変数はこの相対アドレスを使ってアクセスします.

アセンブラ命令

アセンブラ命令の種類

種類意味
セクション指定.text出力先を.textセクションにせよ
データ出力.long 0x123456784バイトの整数値0x12345678
2進数表現を出力せよ
出力アドレス調整.align 44バイト境界にアラインメント調整せよ
(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, 0x221バイトデータを2つ (0x110x22)出力
.word 式, ....word 0x110x11を2バイトデータとして出力
.long 式, ....long 0x110x11を4バイトデータとして出力
.quad 式, ....quad 0x110x11を8バイトデータとして出力
`.string" 文字列, ....string "hello"文字列"hello"を出力(ヌル文字を付加する)
`.ascii" 文字列, ....ascii "hello\0"文字列"hello\0"を出力(ヌル文字を付加しない)
`.asciz" 文字列, ....asciz "hello"文字列"hello"を出力(ヌル文字を付加)
`.fill データ数,サイズ,値.fill 10, 8, 0x12340x1234を8バイトデータとして10個出力
(サイズと値は省略可能.省略時はそれぞれ1と0になる)

出力アドレス調整 (アラインメント)

アセンブラ命令説明
.align.align 8出力先アドレスを8バイト境界にせよ
(ロケーションカウンタを8の倍数に増やせ)
.p2align.p2align 3出力先アドレスを\(2^3=8\)バイト境界にせよ
.space.space 3ロケーションカウンタを3増やせ (.skipでも同じ)
.zero.zero 33バイトのゼロを出力せよ
.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 -snmで記号表の中身を比べると❸〜❻は全く同じになりました.

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, %raxadd rax, 4AT&T形式では左→右
Intel形式では右→左に代入
即値の表記pushq $4push 4AT&T形式では即値に$がつく
レジスタの表記pushq %rbppush rbpAT&T形式ではレジスタに%がつく
  • オペランドのサイズ指定方法が異なります
    • AT&T形式では命令サフィックス(例えば,movbb)で指定します
    • Intel形式では BYTE PTRなどの記法を使います
AT&T形式の
サイズ指定
Intel形式の
サイズ指定
メモリオペランドの
サイズ
AT&T形式での例Intel形式での例
bBYTE PTR1バイト(8ビット)movb $10, -8(%rbp)mov BYTE PTR [rbp-8], 10
wWORD PTR2バイト(16ビット)movw $10, -8(%rbp)mov WORD PTR [rbp-8], 10
lDWORD PTR4バイト(32ビット)movl $10, -8(%rbp)mov DWORD PTR [rbp-8], 10
qQWORD PTR8バイト(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, op2movsx op2, op1
    movsxd op2, op1
    move with sign-extentionop1を符号拡張した値をop2に格納
    movz␣␣ op1, op2movzx op2, op1move with zero-extentionop1をゼロ拡張した値を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 word1バイト→2バイトの拡張
    blbyte to long1バイト→4バイトの拡張
    bqbyte to quad1バイト→8バイトの拡張
    wlword to long2バイト→4バイトの拡張
    wqword to quad2バイト→8バイトの拡張
    lqlong to quad4バイト→8バイトの拡張