1 第 4 回 コンパイラ実装会 配布資料 Brainf*ck から入るコンパイラ入門 Brainf*ck は難解言語としてよくネタにされますが、字句解析が不要 Brainf*ck トランスレータ なためバイナリ関係に集中できて、最初のターゲット言語として向 いていると思います。 1 Brainf*ck から入るコンパイラ入門 ...................... 1 2 はじめに.............................................. 1 3 64bit ............................................ 1 2.2 シンボル名のプレフィックス ....................... 2 1. インタプリタ 2.3 記法 ............................................. 2 2. C 言語へのトランスレータ 呼び出し規約.......................................... 2 3.1 3. アセンブリ言語へのトランスレータ i386(Win) ........................................ 2 4. JIT スタックの効率化............................... 2 5. 実行可能形式でバイナリ出力 3.1.1 4 5 6 次の順番で少しずつ自己完結的にしていくと良いと思います。 2.1 3.2 i386(ELF) ........................................ 2 3.3 i386(Mac) ........................................ 3 最初にインタプリタから作るのは、バイナリやアセンブラの知識を 3.4 x86_64(Win) ...................................... 3 必要とせずに、Brainf*ck 自体に慣れるためです。 3.5 x86_64(ELF) ...................................... 3 3.6 x86_64(Mac) ...................................... 3 いきなりバイナリを出力するのは難しいので、まずは C 言語から出 変数とメモリ.......................................... 3 力して、少しずつバイナリまで自前で出力する方向に持って行きま 4.1 メモリ ........................................... 3 4.2 初期値 ........................................... 3 4.2.1 mov............................................ 4 4.2.2 lea............................................ 4 4.3 退避 ............................................. 4 4.4 増減 ............................................. 4 4.5 メモリの増減 ..................................... 5 す。 2 具体的なコードについて取り上げる前に、私の考えについて簡単に 触れておきます。 ループ................................................ 5 5.1 相対ラベル ....................................... 5 5.2 前・後 ........................................... 5 5.3 ネスト ........................................... 5 2.1 i386(ELF) 非 PIC.................................. 6 6.2 i386(ELF) PIC .................................... 6 6.2.1 GOT............................................ 6 6.2.2 まとめ......................................... 7 6.3 x86_64(ELF) 非 PIC................................ 7 6.4 x86_64(ELF) PIC .................................. 7 6.5 i386(Mac) ........................................ 7 6.6 x86_64(Mac) ...................................... 8 7 おわりに.............................................. 8 8 これから.............................................. 8 8.1 OS 関連の勉強会................................... 8 8.2 その他の構想 ..................................... 8 ハッシュタグは #cmpimpl です。 今回掲載したサンプルコードはすべて CC0(いわゆるパブリッ ら始める方が良いと考えていました。 64bit(x86_64)は 32bit(i386)の拡張なので、32bit 時代の事情 を知らないと理解できない仕様がある。 同じことは 32bit についても言えるが、16bit はセグメン トを意識するなど 32bit より難しい点があるため、最初に 16bit をやるべきかは微妙。 Alpha のように最初から 64bit として設計された ISA なら 問題ないと思うが、Alpha はもう製造されていない。 64bit OS では 32bit コードも動くものがあるため、テストのた めに OS を入れ替えることは少ないかも。 32bit を理解していれば独力で(わざわざ説明しなくても) 64bit 化できるのではないか。 あまりこれを強調すると押し付けになってしまうため、暗黙の了解 として敢えて触れないようにしていました。しかし大半の方が 64bit で開発されていたため、32bit の説明がそのままでは適用できないケ クドメイン)で提供します。 ースが続出して、混乱が生じました。 http://sciencecommons.jp/cc0/about 64bit 私自身は以下の理由により、マシン語の勉強は i386(32bit x86)か 位置独立コード........................................ 5 6.1 はじめに 実装例は以下を参照してください。 そもそも上記は一般論で、Brainf*ck のトランスレータを作成するの https://github.com/mason-/cmpimpl/blob/master/python/bf に歴史的背景の理解が本当に必要なのか?と考え直しました。ISA tran.py が何であれ、アセンブリ言語に慣れることが何よりも大事なはずで す。そのため今回は 64bit についても説明します。 1 2.2 # putchar('A'); シンボル名のプレフィックス pushl $65 システムコールを直接呼ぶと完全に OS に依存してしまうため、libc calll _putchar を呼び出す方法で説明しました。Win32 を念頭に説明したため C 関数 addl $4, %esp のシンボル名にプレフィックスのアンダースコアを付けていました が(例: _purchar)、プレフィックスが不要な ELF 環境では動かない .intel_syntax noprefix ため混乱が生じました。 # putchar('A'); push 65 勉強会での Mac 率を考えると 64bit ELF を前提にするのも躊躇する call _putchar ため、プレフィックスについては環境ごとに説明しようと思います。 add esp, 4 2.3 記法 3.1.1 スタックの効率化 私が慣れている Intel 記法で説明しました。しかし Mac の as は GNU 関数の呼び出し後に毎回スタックを片付けると冗長です。最初にス binutils ではなく Intel 記法を受け付けないことが判明したため、 タックを確保して、複数の処理を行ってから、最後に片付ける方法 AT&T 記法が避けられなくなりました。 に書き換えます。 記法自体はアセンブリ言語の本質ではないため、あまり拘っても仕 # スタックを確保 方がありません。説明は AT&T 記法を中心にして、補助的に Intel 記 subl $4, %esp 法も併記する方針に切り替えようと思います。 # putchar('A'); movl $65, (%esp) 3 calll _putchar 呼び出し規約 # putchar('B'); 今回のトランスレータでは、OS ごとの差異を吸収するためシステム movl $66, (%esp) コールを直接呼ばずに libc を呼びます。以下に呼び出し規約をまと calll _putchar めます。 # スタックを解放 addl $4, %esp アーキテクチャ 引数 接頭辞 i386(Win) スタック あり .intel_syntax noprefix i386(ELF) スタック なし # スタックを確保 i386(Mac) スタック(16 バイトアラインメント) あり x86_64(Win) x86_64(ELF) x86_64(Mac) スタック 32 バイト予約 + RCX, RDX, R8, R9, スタック RDI, RSI, RDX, RCX, R8, R9, スタック ELF と同じ(スタックは 16 バイトアライ ンメント) sub esp, 4 # putchar('A'); なし mov dword ptr [esp], 65 call _putchar なし # putchar('B'); あり mov dword ptr [esp], 66 call _putchar # スタックを解放 これらを具体的に見ていきます。 3.1 add esp, 4 i386(Win) 3.2 引数をスタックに積んで call します。スタックは呼び出し元で後片 i386(ELF) シンボルに接頭辞を付けない以外は Windows と同じです。AT&T 記法 付けをします。シンボルに接頭辞としてアンダースコアを付けます。 のみ示します。 関数の戻り値は eax で返します。 subl $4, %esp ※この規約は cdecl と呼ばれています。スタックの後片付けが異な movl $65, (%esp) る stdcall という規約もありますが、ここでは取り上げません。 calll putchar addl $4, %esp AT&T 記法と Intel 記法を併記します。AT&T 記法では命令の後にサイ ズを示す接尾辞 l(long=32bit)が付き、即値の接頭辞として$、レ ジスタに%が付きます。2 オペランド命令の順番が逆になります。 2 3.3 movl $65, %edi i386(Mac) callq putchar シンボルには接頭辞が付きます。関数呼び出し時の esp は 16 バイト 境界に揃える必要があります。SDK の as は GNU binutils ではなく スタックの操作が発生しないため、非常に単純です。 AT&T 記法しか受け付けません。 3.6 x86_64(Mac) ※nasm などサードパーティーのアセンブラを使用すれば Intel 記法 も可能ですが、一部文法の違いによりコンパイラが出力したアセン レジスタの使用方法は x86_64(ELF)と同じです。ただし i386(Mac)と ブリを流用するときに修正が必要となります。書き方を調べるのに 同じように rsp は 16 バイト境界に揃える必要があります。 コンパイラの出力を利用するのが手軽なため、今回は AT&T 記法を使 用します。 subq $8, %rsp movl $65, %edi subl $12, %esp callq _putchar movl $65, (%esp) addq $8, %rsp calll _putchar i386 の例と同じように、call で呼ばれた直後と仮定して rsp のアラ addl $12, %esp インメントを揃えています。 スタックを 12 バイト確保しているのは、この部分が call で呼ばれ た直後だと仮定しているためです。call 命令は戻り先をスタックに 4 変数とメモリ 積むため、呼ばれた時点で既に 4 バイトが使用中となります。 Brainf*ck には変数が 1 つあります。トランスレータではこれをレジ このようにアラインメントを揃える必要がある場合、i386(Win)で最 スタに割り当てます。 初に示した随時 push するやり方では毎回パディングが発生するため、 効率がかなり悪くなります。 ABI により関数呼び出しで破壊されないレジスタが決められていま す。保存されるレジスタを使えば、値の破壊を気にせずに変数とほ 3.4 x86_64(Win) ぼ等価に使用できます。今回は 32bit では esi、64bit では r12 を使 用します。 32bit ではレジスタが 8 個でしたが、64bit では倍の 16 個に増えま した。レジスタに余裕が出来たため、引数も最初の 4 つまではレジ ※64bit で esi を使用すると、関数(putchar など)を呼び出したと スタ(RCX, RDX, R8, R9)で渡すようになりました。ただしスタッ きに値が破壊される可能性があります。 クはその 4 つのレジスタの分を確保しておく必要があります。呼び 出し元でスタックに値を入れる必要はありませんが、呼び出し先で 4.1 メモリ 必要に応じて退避するのに使用されます。 Brainf*ck の仕様では 30,000 バイトのメモリを持つと定義されてい シンボルの接頭辞はありません。32bit と 64bit で接頭辞の扱いが異 ます。変数はメモリのアドレスを指すポインタとして使用します。 なるのは、今回取り上げた中では Windows だけです。 アセンブリでは.comm として bss にメモリを確保します。 subq $32, %rsp movl $65, %ecx # char mem[30000]; callq putchar .comm mem, 30000 addq $32, %rsp 4.2 初期値 ecx を操作すると rcx の上位 32bit はクリアされます。そのため rcx ではなく ecx に mov してもゴミは残りません。 mem の指すアドレスをレジスタに入れます。単純に mov すると mem の指すメモリの中身が入ってしまうため、lea 命令を使用します。 3.5 x86_64(ELF) # i386 Windows とは呼び出し規約が異なります。引数は最初の 6 つまでをレ .comm mem, 30000 ジスタ(RDI, RSI, RDX, RCX, R8, R9)で渡して、残りをスタック .text で渡します。Windows のようにレジスタ渡しの分までスタックを確保 leal mem, %esi しておく必要はありません。 ※Mac OS X ではデフォルトで PIC(位置独立コード)を要求される 3 ため、単純に lea で処理すると警告されます。PIC は 6 位置独立コー main は CRT から呼び出されます。esi/r12 は呼び出し元に対して値 ドで説明しています。 を保存しなければならないため、main から抜ける際に元の値に戻す 必要があります。そのため main の最初で push して、最後で pop し ます。main 自体の戻り値は 0 とするため、eax に 0 を mov して ret 4.2.1 mov します。 lea がどういう命令かを説明する前に、比較対象として mov 命令を見 main は通常の C 関数と同じ扱いのため、呼び出し規約で必要とされ てみます。 る場合は接頭辞のアンダーバーを付けます。以下では接頭辞が必要 ない ELF の例を示します。 レジスタに即値を代入する場合、mov 命令を使うのが基本的な方法で す。 # i386(ELF) # eax = 0x1234 .comm mem, 30000 movl $0x1234, %eax # AT&T .text mov .globl main eax, 0x1234 # Intel main: AT&T 記法では$を外すとメモリの中身が対象となります。Intel 記法 pushl %esi ではアドレスであることを示すブラケットで囲みます。 leal mem, %esi # ... # eax = *(long *)0x1234 popl %esi movl 0x1234, %eax movl $0, %eax mov # AT&T eax, [0x1234] # Intel ret メモリが以下の内容であれば、eax=0x12345678 となります。 # x86_64(ELF) .comm mem, 30000 00001230: 00 01 02 03 78 56 34 12 aa bb cc dd 00 00 00 00 .text .globl main main: メモリから 1 バイトだけ取って上位をゼロで埋める場合は movzx 命 令を使用します。AT&T 記法では movzbl となりますが、ニーモニック pushq %r12 が movz、ソースのサイズが b、デスティネーション(レジスタ)の leaq mem, %r12 サイズが l となります。 # ... popq %r12 # eax = *(char *)0x1234 movl $0, %eax movzbl 0x1234, %eax ret movzx # AT&T eax, [0x1234] # Intel 4.4 増減 メモリが先ほど示した内容であれば、eax=0x78 となります。 本来、変数の値を増減させる際に、変数の指すアドレスがメモリ空 間からはみ出さないようにチェックする必要があります。今回は説 4.2.2 lea 明を単純化するためチェックは行いません。 指定したアドレスが指すメモリの内容ではなく、アドレスの値を代 Brainf*ck では変数は 1 ずつ増減します。x86 では 1 だけ増やす命令 入する命令が lea です。メモリの内容は一切関係ありません。 が inc、1 だけ減らす命令が dec です。 # eax = 0x1234 leal 0x1234, %eax lea # AT&T # i386 eax, [0x1234] # Intel incl %esi # esi++ decl %esi # esi— 先ほどの mem の例では、mem がアドレスとして扱われているため、メ モリの中身ではなく、アドレスを値として取り出すのに lea を使用 # x86_64 しています。 incq %r12 # r12++ decq %r12 # r12— 4.3 退避 4 4.5 か後(b)かをラベルの接尾辞に添えて指定します。相対ラベルは重 メモリの増減 複しても構いません。 Brainf*ck にはメモリの中身を 1 バイト単位で増減させる命令があ ※前・後の方向については次のセクションで取り上げます。 ります。ニーモニックの接尾辞 b でバイト指定にして、オペランド を括弧で囲みます。 0: # i386 cmpb $0, (%esi) incb (%esi) # (*(char *)esi)++ jz 0f # ↓方向に0:を探す decb (%esi) # (*(char *)esi)— # ... jmp 0b # ↑方向に0:を探す # x86_64 0: incb (%r12) # (*(char *)r12)++ decb (%r12) # (*(char *)r12)— 前・後 5.2 AT&T 記法での括弧は、Intel 記法でのブラケットに相当します。AT&T 前か後かという表現はパーサの進行方向を基準としています。パー 記法での接尾辞が Intel 記法ではキーワードとして表記されるため、 サはプログラムを先頭から見ていくため、画面での下が前に相当し 見た目は長くなります。 ます。 # i386 後 .intel_syntax noprefix ↓ inc byte ptr [esi] # (*(char *)esi)++ ↓ dec byte ptr [esi] # (*(char *)esi)— ↓ 前 # x86_64 .intel_syntax noprefix 直感的に判断すると画面での上が前のように思えてしまうため、慣 inc byte ptr [r12] # (*(char *)r12)++ れが必要な表現です。 dec byte ptr [r12] # (*(char *)r12)-ネスト 5.3 5 ループ ループがネストする場合、ネストレベルを相対ラベルの値に割り当 てます。うまくペアができていることを確認してください。 Brainf*ck では変数の指すメモリの中身が 0 以外の間だけ回るルー プがあります。 0: # while (*(char *)esi) { ... } cmpb $0, (%esi) start_loop: jz 0f 1: cmpb $0, (%esi) jz end_loop cmpb $0, (%esi) # ... jz 1f # ... jmp start_loop jmp 1b end_loop: 1: jmp 0b cmp 命令で 0 と比較して、一致した場合(jz)は end_loop に飛んで 抜けます。jmp は無条件で start_loop に飛びます。 0: ループが複数ある場合、ラベル(start_loop, end_loop)に別の名 上記では読みやすいようにインデントしています。Python とは異な 前を付ける必要があります。連番を振れば簡単そうですが、ループ り、インデントに文法的な意味はありません。 がネストすると厄介です。 6 5.1 位置独立コード 相対ラベル Mac OS X ではデフォルトで位置独立コード(PIC)が要求されます。 ラベルを局所化するテクニックとして相対ラベルがあります。数字 プログラムがどのアドレスにロードされるか決められないため、グ のみのラベルが相対ラベルとして扱われます。参照元から見て前(f) ローバル変数のアドレスを即値ではなく、プログラムカウンタから 5 00000010 R_386_32 の相対で得たテーブルからポインタを取り出す必要があります。 mem (後略) 非 PIC と PIC を比較するため、まずは ELF を見てみます。 6.2 i386(ELF) PIC i386(ELF) 非 PIC 6.1 PIC ではコードがどのように変化するか見てみます。リンカによって PIC ではないコードでは、オブジェクトコードの段階ではゼロが埋め 埋め込まれる値があまりにも多いため、逆アセンブルではなくコン 込まれて、リンク時にアドレスが埋め込まれます。 パイラのアセンブリ出力を見ます。 例として C 言語コードのコンパイルとリンクを追ってみます。テス $ gcc -m32 -fPIC -S test.c ト環境は NetBSD/amd64 で、gcc -m32 を指定して 32bit のコードを生 成しています。 mem にアクセスしている部分と、関係する部分を抜粋します。 test.c call __i686.get_pc_thunk.cx int mem[1]; addl $_GLOBAL_OFFSET_TABLE_, %ecx movl mem@GOT(%ecx), %eax movl $1, (%eax) int main(void) { mem[0] = 1; return 0; __i686.get_pc_thunk.cx: } movl (%esp), %ecx ret C 言語と対比するため-g オプションでデバッグ情報を埋め込みます。 オブジェクトコードを生成して逆アセンブルします。 i386 ではプログラムカウンタ(eip)を直接取得することができない ため、__i686.get_pc_thunk.cx で call 命令によってスタックに積ま $ gcc -m32 -g -c test.c れた戻り先アドレスを ecx に入れることで、間接的に eip を取得し $ objdump -S test.o ています。具体的には次のようなことがやりたかったのだと思われ ます。 main()でグローバル変数が関係するのは以下の部分です。mem のアド レスが 0 になっているのが分かります。 %eip, %ecx addl $_GLOBAL_OFFSET_TABLE_, %ecx movl mem@GOT(%ecx), %eax movl $1, (%eax) mem[0] = 1; movl e: 15: c7 05 00 00 00 00 01 movl $0x1,0x0 00 00 00 6.2.1 GOT リンクして逆アセンブルします。 ecx に足されている$_GLOBAL_OFFSET_TABLE_は定数ではなく、リンカ $ gcc -m32 -o test test.o がプログラムカウンタを考慮して値を埋め込みます。次の例を見る $ objdump -S test と理解しやすいかもしれません。 先ほどの部分に mem のアドレスが埋め込まれているのが分かります。 got.s addl $_GLOBAL_OFFSET_TABLE_, %ecx mem[0] = 1; addl $_GLOBAL_OFFSET_TABLE_, %ecx 80486ea: c7 05 d0 98 04 08 01 80486f1: 00 00 00 movl $0x1,0x80498d0 $ gcc -m32 -nostdlib got.s $ objdump -d a.out オブジェクトファイルにはリンク時にアドレスを埋め込む場所が記 (中略) 録されています。リンカがこれを見てアドレスを埋め込みます。 8048074: 81 c1 0c 10 00 00 add $0x100c,%ecx 804807a: 81 c1 06 10 00 00 add $0x1006,%ecx $ objdump -x test.o (中略) ecx に命令のアドレスが入っていれば、計算結果は常に同じアドレス RELOCATION RECORDS FOR [.text]: になります。 OFFSET TYPE VALUE 6 0x8048074 + 0x100c = 0x8049080 0x8049080 + 0x1006 = 0x8049080 Global Offset Table(GOT)とは、簡単に言えばグローバル変数や 0x40089e + 1049690 = 0x500cf8 6.4 x86_64(ELF) PIC 即値のポインタが入っているテーブルです。値そのものではなく、 PIC では 32bit と同じように GOT 経由でアクセスします。 ポインタが入っています。更に間接参照してようやく値に到達でき ます。 $ gcc -fPIC -S test.c eip → GOT → ポインタ → 目的の値 該当部分を抜粋します。 GOT に値を入れれば間接参照が 1 段階減りますが、巨大な配列を定義 すればすぐに GOT が埋まってしまうため、それを避けるためにこの movq mem@GOTPCREL(%rip), %rax ような仕様になっていると思われます。プログラムカウンタに movl $1, (%rax) $_GLOBAL_OFFSET_TABLE_を足して得られるアドレスは、GOT の先頭で はなく中央です。相対アドレッシングは符号付きで表現されるため、 たった 1 命令で GOT からポインタを取得しています。GOT の中心から 負の値も活用するための仕様です。 相対計算せずにいきなり算出しています。32bit と比べて非常に単純 になっています。 ちなみに MIPS や Alpha では、GOT の中央を指す専用のレジスタ(GP) が存在します。それらの CPU ではレジスタが 32 個と多いため、1 つ 6.5 i386(Mac) くらいレジスタを割り当ててもほとんど問題にはなりません。 さて、いよいよ Mac OS X です。デフォルトで PIC となります。 6.2.2 まとめ $ gcc -m32 -S test.c まとめると、プログラムカウンタ相対で GOT(ポインタが集まってい るテーブル)のアドレスを求めて、そこからポインタを取り出して 該当部分を抜粋します。 目的のアドレスにアクセスするわけです。 .comm x86_64(ELF) 非 PIC 6.3 _mem,4 (中略) call 64bit ではプログラムカウンタ(rip)が直接取得できるようになっ ___i686.get_pc_thunk.cx L00000000001$pb: たため、即値は埋め込まずにプログラムカウンタ相対でアドレスを 取得します。 leal L_mem$non_lazy_ptr-L00000000001$pb(%ecx), %e movl (%eax), %eax movl $1, (%eax) ax $ gcc -S test.c (中略) .section __TEXT,__textcoal_nt,coalesced,pure_instruc 該当部分を抜粋します。 tions movl $1, mem(%rip) .weak_definition ___i686.get_pc_thunk.cx .private_extern ___i686.get_pc_thunk.cx 逆アセンブルすると以下の通りです。 ___i686.get_pc_thunk.cx: movl (%esp), %ecx $ gcc -g test.c ret $ objdump -S a.out .section __IMPORT,__pointers,non_lazy_symbol_pointer (中略) s mem[0] = 1; 400894: c7 05 5a 04 10 00 01 L_mem$non_lazy_ptr: movl $0x1,1049690(%rip) .indirect_symbol _mem # 500cf8<mem> .long 0 40089b: 00 00 00 .subsections_via_symbols (後略) ELF での$_GLOBAL_OFFSET_TABLE_に相当する部分が自動ではなく泥 臭い表現になっていますが、やっていることはほぼ同じです。 プログラムカウンタは次の命令を指しているため、相対アドレスを 計算すると以下の通りとなります。これがコメントの意味です。 7 6.6 のネイティブ呼び出しに挑戦 x86_64(Mac) PE 勉強会 CIL 編 .NET のバイナリを分析。CIL はいわゆるバイトコード。 シンボルにプレフィックスが付いている以外は ELF の PIC と同じで す。 PE32+勉強会 64bit 用の PE。32bit との比較を中心に。 $ gcc -S test.c ユーザーモードインタプリタ実装会 x86 以外の CPU を実装しながら勉強 該当部分を抜粋します。 デバイスエミュレータ実装会(NetBSD 編) ドライバのソースを見ながら動かす movq _mem@GOTPCREL(%rip), %rax movl $1, (%rax) 7 Alpha や PowerPC の NT を動かすことを目指す おわりに 以上で Mac/Win/ELF で Brainf*ck のアセンブリ言語トランスレータ の実装に必要な知識の説明は終了です。複数のアーキテクチャを比 較したため複雑になりました。しかし自分の使っているアーキテク チャで実装したいというのが人情ですから、ある程度は網羅的な説 明になるのは避けられません。 ※こうなると収集が付かなくなる恐れがあるので、入り口として i386-pe に的を絞ったのが PE 勉強会だったというわけです。 8 これから Brainf*ck で実行可能形式バイナリが出力できるようになれば、次は Forth のようなスタック式の逆ポーランド表記言語に進むのが良い と思います。 8.1 OS 関連の勉強会 コンパイラがある程度動くようになれば、それを使って OS を作って みるのが面白いと考えています。 DOS COM 勉強会 COM(16bit x86 コード)を出力するコンパイラを作る ブートローダ実装会 DOS COM 勉強会で作った 16bit コンパイラを使って実装 プロテクトモード勉強会 16bit の世界から 32bit の世界へ OS 実装会 プロテクトモードに入れば PE 用コンパイラが使えるよ! 8.2 その他の構想 こんなことができたら楽しいなと思っているもの。 COM 勉強会 Windows の Component Object Model をメモリレイアウトや PE の観点から探る デバイスエミュレータ実装会(NT 編) WP7 ネイティブ DLL 勉強会 ARM バイナリと COM の知識を組み合わせて、Windows Phone 7 8
© Copyright 2024 Paperzz