グローバル変数を扱うアセンブリを書いているときに、最初よく分からずに非PIEでのコードを書いてしまっていた。
最近のLinuxディストリビューションにインストールされているGCCはデフォルトでPIEとしてリンクしようとするため、非PIEのアセンブリコードはリンクに失敗してしまう。
そのあたりで少し調べたことをメモしておこうと思う。
なお、この記事ではx86-64のアセンブリを対象とし、アセンブリの記法にはIntel記法を使用する。また、検証環境はLinuxベースを前提としている。
PIEとは
PIEとは、Position Independent Executable(位置独立実行形式)のことで、メモリアクセスが全て相対アドレスにより表現されており、メモリ上のどの位置に配置されても正しく実行できるというもの。
似たような用語でPICというものがあるが、こちらはPosition Independent Code(位置独立コード)のことで、PICで構成された実行形式をPIEと呼ぶ(と思う)。
PICは主に共有ライブラリなどで使用される技術だったが、最近はセキュリティの側面から実行形式にもPICを適用する流れがあるようだ。(位置独立コードのWikipedia記事)
GCCの対応
GCCはもちろんPIEに対応しているが、GCCのビルド時のオプションに--enable-default-pieを付けるとデフォルトでPIEとして実行形式を出力するようになる。
自分の環境(Debian buster)にインストールされているGCCを確認すると、デフォルトPIEでビルドされていることが分かる。
$ gcc -v
...
Configured with: ... --enable-default-pie ...
...
確認用コード
やりたい処理をCで書くとこんな感じになる。整理のためのコードなので特に意味はない。
long g;
int main() {
g = 1;
return g;
}
グローバル変数をlongで宣言しているのはアセンブリでレジスタの扱いを64ビットに揃えるためで、Cのコードでみた場合はintで宣言したほうが自然にみえる気もするが、アセンブリ優先で書いてみた。
非PIEのアセンブリ
非PIEのアセンブリはこのようになる。
.intel_syntax noprefix
.bss
g:
.long 0
.text
.globl main
main:
push rbp
mov rbp, rsp
mov rax, OFFSET FLAT:g # グローバル変数のアドレスを取得
mov QWORD PTR [rax], 1
mov rax, QWORD PTR [rax]
pop rbp
ret
OFFSET FLAT:gでグローバル変数gのアドレスを取得しているわけだが、これは絶対アドレスでの表現となる。そのため、このコードをPIEとして出力することができず、エラーになってしまう。
$ gcc global_no_pie.s -o global_no_pie
/usr/bin/ld: /tmp/ccJR4M58.o: relocation R_X86_64_32S against `.data' can not be used when making a PIE object; recompile with -fPIC
/usr/bin/ld: final link failed: nonrepresentable section on output
collect2: error: ld returned 1 exit status
エラーメッセージで「recompile with -fPIC」として出ているが、この場合は-fPICを付けても結果は変わらない。
このメッセージはリンカであるldが出力しているようで、おそらくldはアセンブリが直接渡されたのではなく、(高級言語からアセンブリを出力するという意味での)コンパイル時に-fPICがついていないため、PIEとしてリンクすることのできないアセンブリが出力され、それがldに渡されていると想定しているのだと思う。
特に理由がなければ、下で説明するようなPIEとして出力可能なコードにすべきとは思うが、-no-pieのオプションを付ければ非PIEとしてリンクすることもできる。
$ gcc global_no_pie.s -o global_no_pie -no-pie
PIEのアセンブリ
PIEとして出力できるアセンブリはこのようになる。
.intel_syntax noprefix
.bss
g:
.long 0
.text
.globl main
main:
push rbp
mov rbp, rsp
lea rax, QWORD PTR g[rip] # グローバル変数のアドレスを取得
mov QWORD PTR [rax], 1
mov rax, QWORD PTR [rax]
pop rbp
ret
やりたいことは非PIEと同じでグローバル変数のアドレスを取得したいのだが、ここではleaという命令を用いてripからの相対アドレスを計算している。ripはインストラクションポインタであるため、これから実行される命令がある場所からの相対値でグローバル変数にアクセスしているということになる。
おまけ
アセンブリについて調べたいとき、GCCが出力するアセンブリを確認することがよくあるが、gccやobjdumpのデフォルトではAT&T記法で出力されたり表示されたりする。Intel記法にするには以下のようにオプションを付ければいい。結構コマンドごとに違って覚えにくいんだよね、、、
$ objdump -M intel -d <file>
$ gcc <file> -S -masm=intel
gccで出力されるアセンブリでの(初期値なしの)グローバル変数の宣言には.commというディレクティブが使用されるようだ。.bssセクションに変数を確保することを意味するらしい。
.comm g, 8, 8
# これと同じ
.bss
g:
.long 0
まとめ
アセンブリでグローバル変数を扱う際の、PIE対応について整理した。
PIEと非PIEそれぞれでのリンカの処理とかもう少し深く理解したいが、まだまだ勉強不足。