Compiler Construction Lecture No.2
Menu
実行系のArchitecture
Compilerのtargetにはいろいろあるが、あるComputer上でもっとも高速なのは、そのComputerのmachine language(機械語)である。したがって、machine language がもっとも重要なCompiler targetだということができる。ここでは、いくつかのCPUのProgramming Modelを学び、その実行を調べる方法を学ぶ。CPUのprogramming modelは、以下の要素からなる。
- Registers
- program counter
- stack, frame pointer
- index
- data
- condition code
- Instruction
- load, store instruction メモリからデータを取り出す、格納する
- addressing mode レジスタからアドレスを計算する
- arithmetic 数値演算
- branch instruction jumpやsubroutine call、条件分岐
- etc その他
個々のCPUには、それぞれ特徴があり、compilerを作る際にはその特徴を考慮する必要がある。しかし、極端に異なるachitecutreを持っているものは少なく、どれをとっても同じようなものだともいうことができる。
すべてのCPUに詳しくなる必要はないが、どれか一つの専門家にはなっていたい所である。
Little-Endian, Big-Endian
Computer には memory がつきものだが、memory は、1 byte (= 8bit)単位でlinear address (byte addressing) がふられている。しかし、最近のCPUは、16bit, 32bit, 64bit 単位(word)で処理をおこなう。従って、wordをどのように byte addressing に割り振る(map)かという問題がある。これには主に2種類の mapping があり、それぞれLittle-Endian, Big-Endian と呼ばれている。どちらが優れているかという議論はあったが、現在では卵のとがっている方から割るか、丸い方から割るかぐらいの違いしかないと認識されているようだ。
Endian をgdbで表示する
long check = 0x123456789abcdef; main() { char i, *ptr; ptr = (char *)✓ i = ptr[1]; return i; }32bit machine の場合は、
long check = 0x12345678;ぐらいが良い。long は 64bit であることが多い。( LP64 )
これをコンパイルして、
% gdb a.out GNU gdb 6.3.50-20050815 (Apple version gdb-563) (Wed Jul 19 05:17:43 GMT 2006) (gdb) b main Breakpoint 1 at 0x1da8 (gdb) run Starting program: /private/tmp/a.out Reading symbols for shared libraries . done Breakpoint 1, 0x00001da8 in main () (gdb) x/20b &check 0x2014 <check>: 0x12 0x34 0x56 0x78 0xa0 0x00 0x1f 0xdc 0x201c <check+8>: 0xa0 0x01 0x1b 0x1c 0xa0 0x00 0x1f 0x98 0x2024 <check+16>: 0xa0 0x00 0x5c 0x00 (gdb)と表示してみよう。0x12, 0x34, 0x56, 0x78 の順にメモリに格納されていることがわかる。
x は examine の略らしい。/20b は、表示するフォーマットを表す。
(gdb) help x Examine memory: x/FMT ADDRESS. ADDRESS is an expression for the memory address to examine. FMT is a repeat count followed by a format letter and a size letter. Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char) and s(string), T(OSType). Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes). The specified number of objects of the specified size are printed according to the format.Intel と PowerPC で調べてみること。
つまり、Mac OS X では、Intel/PowerPC でEndianが異なる。したがって、Endian に依存したプログラムをしてはいけない。(特にネットワークでバイナリを送る場合)
cf. htons, ntohs
Alignment
もう、このmappingは、CPU - memory 間の転送も考慮する必要がある。同じ 1 word を転送するのでも、Busが64bit幅だったりすれば、そのアドレス下位3bitの値によって、一回で転送できる場合とそうでない場合がある。これを word alignment の問題という。当然、一回で転送できる方が高速に動作する。つまり、32bit integer を格納する場合、配列などは、4byte 単位の alignment を持つ。
0x1231などのアドレスにintを格納するとalignmentがずれる。
0x1230 や 0x1234ならば良い。
アセンブラでは、
.align 4という疑似命令で制御されていることが多い。
実際には、CPU - memory 間の転送は、直接行われるわけではなく、キャッシュ間の転送として現れるので、パフォーマンスとして影響が出ることは少ない。
しかし、IntelのSSE2 (Pentinum 4 以降)では、XMM レジスタが16byte alignment を要求する。alignment がずれると、CPU が例外を発生する。
これらの問題は、普通のプログラミングでは考える必要はないが、効率の良いMachine Languageを生成する場合には考慮する必要がある。
malloc 時に alignment を指定できる malloc を使用する。
posix_memalign
Addressing Mode
実際にCPUとmemoryのdataのやり取りをするのが、load/store 系の命令である。CPUの命令の中で大きな割合を占める。Addressing modeとは、register や命令で、どのように memory address を指定するかを決める方法である。昔は、MC6809のように豊富なIndex modeを持つものが歓迎された。今では、RISC Architecture という、simple な Address mode を持つ命令が好まれている。
現在のプログラミングでは、配列(array)やリスト(list)、構造体(structure)のアクセスが重要なので、Addressing mode は、それを1命令で実現しやすいように設計されているのが普通だ。だいたい図のようなAddressing mode を採用しているものが多い。
いろいろなCPUのProgramming Model
Intel x86 32bit mode
現在もっとも良く使われているCPU。しかし、16bit modeを使っている所も多いだろう。ここでは、比較的きれいなarchitectureを持っている32bit mode のみを紹介する。IA32 と呼ばれている。8bit CPU(8080)の拡張によってできたCPUなので、32bit modeでもそれをかなり引きずっている。命令は8bitの可変長命令である。80386によって、16bit/32bit切替と仮想記憶がサポートされ爆発的な成功を納めた。最近のPentinum II では、内部でRISC 命令に変換してから実行するというようなことをしている。16bit ではregisterの役割が偏っていたが、32bitでは若干対称性が良くなっている。4本のAccumulator、4本のIndex register を持つ。
Addressing modeには以下のようなものがある。
Immediate movb al,$F0 constantをloadする extended movb al,[$F000] 指定されたaddressからloadする indexed movb al,[EAX+5] indexとoffsetで示されたaddress からload する accumulator offset indexed mov [EBX+EAX] accumulator offset indexed mov [EBX+EAX+5]さらに、x86には segement register というのがあり、それによってvirtual memory (メモリ空間) を切り替えることができる。しかし、普通はすべて同じメモリ空間が設定されている。他のCPUでもデータ(data)とコード(code)は別空間にできるものが多い。
演算はレジスタとレジスタの間で行うことが多い。 もう少し詳しくは、インテル386のアセンブラ を参照すること。
Intel EMT64 64bit mode
IA32を、AMDが拡張したものをIntelが採用した64bit mode。register が16本に増えて、rip (program counter)を含めて対称的に扱えるようになっている。128bit XMMレジスタも16本に増えた。
x86 の命令に関しては、このサイトが便利x86 Instruction Set Reference
ARM
iPhone などで使われている CPU。16本のレジスタ、16本の浮動小数点レジスタ。ARM32 の命令 (簡単な方) ARM64 (Aarch) の命令
Java byte code
Java の byte code は、構文木を直接表しているので、CPU 命令よりも構文よりの命令が多い。LLVM byte code
LLVM も変数の宣言などの構文よりの命令がたくさん入っている。コンパイラ出力を読む
LLVM compiler には、-S option があり、Assembelr sourceを生成することができる。簡単なCのプログラムを書いてcompileして見るとどんな命令が生成されるかがわかる。
gdb というdebuggerを使って、そのAssemberの実行を一命令づつ調べることができる。
int a[10]; main() { return a[4]; }は、clang -O -S test.c によって、(RISCの場合は-Oを付けた方がより 分かりやすいコードがでることが多い)
.file "test.c" .version "01.01" gcc2_compiled.: .text .align 4 .globl main .type main,@function main: pushl %ebp movl %esp,%ebp movl a+16,%eax leave ret .Lfe1: .size main,.Lfe1-main .comm a,40,4 .ident "GCC: (GNU) 2.8.1"という形test.s にcompileされる。これをgdbで実行するには、これをさらにcompileして、
gcc -g test.c gdb a.outとする。
(gdb) b main Breakpoint 1 at 0x80483cf: file test.c, line 4. (gdb) r Starting program: /local/kono/public_html/lecture/1999/compiler/99-10-25/a.out Breakpoint 1, main () at test.c:4 4 return a[4]; (gdb) disass Dump of assembler code for function main: 0x80483cc <main>: pushl %ebp 0x80483cd <main+1>: movl %esp,%ebp 0x80483cf <main+3>: movl 0x80494e4,%eax 0x80483d4 <main+8>: leave 0x80483d5 <main+9>: ret End of assembler dump. (gdb) stepi 5 } (gdb) stepi 0x80483d5 in main () at test.c:5 5 } (gdb) p $eax $1 = 0 (gdb)とすれば良い。
問題 2.1
以下のprogram check_endian.c がある。
int check = 0x12345678; main() { char i, *ptr; ptr = (char *)✓ i = ptr[1]; return i; }このprogramをcompileしたassemblerを、i386, emt64 のCPUで表示させて見よ。また、gdb で i にどのような値が入るかを確認せよ。そのCPUは、Little-Endian か Big-Endian かを答えよ。また、 trace の結果を、確認せよ。
Endian の変換はどのような時に必要になるか。どのようにすれば実現できるか?
Unix には、Builtin のEndianの変換関数がある。それを探し出せ。また、その実装がどうなっているかを調べよ。(ヒント: man -k を使う)