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 を使う)