CPU


AMD64 ISAとx64版Windowsについて



現在のPCのほとんどに搭載されている64bitCPUは、Intel製でもAMD製でも、「AMD64 ISA」という命令セットを採用している。(Intel製は「Intel64」だが、Intelが作ったAMD64互換のISAといえるもの)
マイクロソフトはAMD64とIntel64をまとめて「x64」と呼んでいる。

その昔Intelの互換CPUを作っていたAMDが今や普及型64bitCPUの先駆者であり、IntelがAMDの互換CPUを作る時代になってしまった。
ただ、AMD64の元になったアーキテクチャはIntel設計のIA-32であり、80386が存在しなければ今のAMD64は存在できないわけで、卵が先か鶏が先かというような状況でもある。

もとになっているIntelのIA-32のアーキテクチャは80386 ISAほぼそのものであり、これは8086のISAが元になっている。
そしてその8086のアーキテクチャは、8080が元になっている。(というか8080からの互換が考慮されている)
さらに8080のアーキテクチャは、8008の設計を僅かながら引きずっている。
古き8bitの時代から、継ぎ足しや継ぎ接ぎを繰り返しながら、16bitを経て32bitに進化してきたアーキテクチャといえる。
要するに、まああまり美しいとは言えない。(とはいえ、個人的にこの継ぎ接ぎのデコボコ自体は決して“悪”ではないと思う。互換性を最重視するという姿勢は、非常にユーザーに優しい考え方だと思うからだ。「美しさ」と「優しさ」は必ずしも一致しない)
この継ぎ足しや継ぎ接ぎを一掃して、ゼロからキレイな64bitのアーキテクチャを作ろうと、IntelはIA-64を設計するのだが、キレイな反面あまりに高価でIA-32エミュレートの実行速度があまりに遅いというシロモノになってしまったため、ほとんど普及しなかった。(EPICやVLIWは命令が冗長になり、並列実行できない部分ではNOPだらけでコードが水膨れを起こしたかのようにスカスカになる上、IA-64は高級言語コンパイラありきというような設計なので、アセンブリソースは非常に書きづらいし読みづらい。いろいろな理由から個人的にはあまりキレイとも思えないが)

つまりIntelはIA-32に一度見切りをつけ、捨ててしまったわけだ。
しかし、捨てる神あれば拾う神あり。
AMDが「過去との互換性こそビジネスでの勝利のカギである」と思ったかどうかは知らないが、IA-32との互換性を最大限に維持しながら64bitアーキテクチャに拡張し、この継ぎ足し継ぎ接ぎのデコボコして美しくない部分をキレイで使いやすいものに改良しはじめた。
当時世の中には 64bit RISCのCPUが出ていたので、これに似たアーキテクチャになっている。(もちろん継ぎ足し継ぎ接ぎの上にさらに継ぎ足しをしたものであり、根幹にあるのはCISCの設計思想だが)

IA-32はCISCプロセッサとして進化してきたので、命令は可変長。
レジスタは8本(eax, ecx, edx, ebx, esp, ebp, esi, edi)であり、それぞれが32bit。
一応、この8本はすべて同等で汎用だとされているが、実際にはeax/ecx/edxの3本がデータレジスタ、それ以外の5本がアドレスレジスタとして使用されることが多い。
8086からの慣習もあって、ebx/ebpはベースポインタとして、esi/ediはベースポインタからのインデックスとして使われ、さらにespはスタックポインタであり、PUSH/POP/CALL/RET命令で自動的に増減する。
そして、レジスタ間接アドレッシングの際に使用するレジスタによっては命令長が変化することもあり、「8本全てが同等で汎用」とは言いがたい。(例えば、[EAX]だとModR/Mだけですむが、[EBP]は8bitディスプレースメントと組み合わせるしかないので[EBP+00]となり1バイト多くなる。[ESP]となると、SIBを使わないと出来ない上、ベースポインタとして使用できないので、32bitディスプレースメントをゼロにした上でインデックス部分に使用して[00000000+ESP*1]と、5バイトも膨れてしまう)
ただでさえ少ないレジスタ本数なのに、過半数がアドレスレジスタという仕様は、いかに「メモリを叩いてなんぼ」という設計であるかが分かる。
ちなみにレジスタ名の由来は、1文字目の「e」はExtendの略。
a,c,d,bそれぞれの由来は、8008のレジスタ名まで遡るが、a以外のそれぞれに意味が付けられたのは8086からと思われる。
a=Accumulator
c=Counter
d=Data
b=Base
3文字目の「x」は8086のレジスタ名から来ているが、この由来は8080のニモニックに遡る。
8080では決められた組み合わせで8bitレジスタを2本ペアにして16bitレジスタとして使用できるようになっていて、このペアを使うときの命令に「X」が入っていたが、ここから来ているのではないかと思う。
残り4本の由来は、
sp=Stack Pointer
bp=Base Pointer
si=Source Index
di=Destination Index

すべてのレジスタは「e」を取っ払うことで16bitレジスタとしても使用できる。(このとき上位16bitは無効になる)
しかし継ぎ足し継ぎ接ぎの結果、eax, ecx, edx, ebxの4本だけは8bitレジスタとしても使えるようになっている。
このとき上位16bitは無効になり、下位16bitが8bitずつ2本のレジスタに分かれ、合計8本の8bitレジスタとして使える。
すなわち、al, ah, cl, ch, dl, dh, bl, bhの8本。(LはLow、HはHiの略)
データレジスタのa, d, cはまだしも、アドレスレジスタのbが8bitに分解できるメリットはよく分からない。
8086が「新規の16bit CPU」として設計されたのではなく、「拡張された8bit CPU」というスタンスで設計された背景があるため、当時は8bitレジスタが8本、アドレスレジスタが4本、セグメントレジスタが4本の合計16本のレジスタを持つCPU、という捉え方ができたのだろう。
そんな背景があるため、sp, bp, si, diの4本は、IA-32でも8bitレジスタとしては使用できない。
さらにセグメントレジスタは32bitに拡張されず、16bitのまま本数だけが2本増え、レジスタではなくセレクタとなった。
ちなみにIntelは当時iAPX432という32bit CPUを設計していて、8086はそれが普及するまでの「つなぎ」として設計していたのだが、結局iAPX432は仰々しいものになり値段もかなり高価なものになり、期待していたほどは普及せず、8086のほうが普及してしまった。

「同等で汎用」と言うからには、すべてのレジスタを8bit/16bit/32bitとして使用できるように、さらに命令長が変化することなくデータとしてもアドレスとしても使用できるようになっているべきではあるものの、互換性の足枷ともいえるデコボコした仕様が、IA-32には存在している。

AMDはこのようなIA-32を、互換性を保ったまま64bitのアーキテクチャにし、よりキレイで使いやすいものにしようとした。

IA-32の部分はデコボコのまま丸ごと「レガシーモード」というモードで取り込んだ。
ここには「リアルモード」「プロテクトモード」「仮想86モード」の3つのモードがある。
さらに「ロングモード」という新しいモードを増設した。
ロングモードには「64bitモード」「互換モード」の2つがある。
つまり、1つの中に5つのモードが混在するCPUになった。
この中の「ロングモード-64bitモード」のみ、AMD64のすべての機能・性能が使えるようになっていて、このモードだけデコボコを可能な限りフラットにしてある。

以下は、この「ロングモード-64bitモード」の拡張がどのように行われているかを記している。

まずとにかくIA-32はレジスタの本数が足りない。
そこで、汎用レジスタr8~r15の8本を増やし、全部で16本にした。
そしてすべてのレジスタを64bit/32bit/16bit/8bitで使用できるようにした。
しかしレジスタの本数やビット長を変更するということは命令長が変わるという事実に直結するため、IA-32完全互換を維持したまま増やすことはできない。
レジスタやレジスタ長を指定するビットフィールドに何ビット必要かは、そのCPUが持つレジスタ本数やレジスタ長で決まってくる。
CPUを新規設計するときは大して問題にはならないが、既存のISAを拡張するときは大問題だ。
つまり、命令を今よりもある程度長くしないと、レジスタ本数もレジスタ長も拡張できないことになる。
しかし同時にIA-32の互換性が失われることは出来る限り避けなくてはならない。
8086から今日のIA-32に至るまで、レジスタの本数がまったく増えていないのはISAの互換性という大きな足枷に起因するわけだ。

通常、既存のISAを拡張するときは「プリフィックス」を用いてエスケープすることが多い。
命令には存在しない1バイトを命令の前に置いて、「ここから先は別の命令だよ」とCPUに教える。
例えば、1バイトの命令では0x00~0xffの256種類しか存在できないが、このうち0xfeと0xffをプリフィックスにしたとする。
そうすると、1バイトの命令は0x00~0xfdまでの254種類になる。
0xfeを前置して0x00~0xfd、さらに0xffを前置して0x00~0xfdとすると、1命令が2バイトになってしまうデメリットはあるものの、508命令を追加することができる。
1バイト命令が254種類、2バイト命令が508種類、合計762種類の命令を作ることができる。
これでも足りなければ、今度はプリフィックスを2重3重にしていく。
0xfeffの2バイトを前置すれば、さらに254種類増やせる。(あまり格好の良い拡張ではなくなってくるが)
Z80や80386などでは、こうした1バイト~2バイトのプリフィックスを前置することで命令を拡張していている。

つまり、互換性を維持したまま拡張するには、プリフィックスを使えばいい。
しかしこのような拡張は、CPUが最初に設計された時点で将来の拡張を考慮して、どこまでを命令に、どこからをプリフィックスにするかをあらかじめ決めておく必要がある。
さもなければ、拡張を余儀なくされた時点で、何かの命令を犠牲にしてプリフィックスとトレードオフする羽目になってしまう。

残念ながらIA-32にはプリフィックスの「空き」はない。
しかたなく何かの命令とトレードオフし、そこを拡張用のプリフィックスとして割り当てることになる。
普通に考えれば、1バイト命令を1つ犠牲にしてプリフィックスに割り当て、「ここからは64bit命令だよ」とCPUに知らせればいい。
その次の1バイトに、レジスタ指定とレジスタ長指定の拡張用ビットを入れる。
さらに次からIA-32のプリフィックス、命令コード、ModR/M、SIB、オペランド・・となる。
つまりこの考え方だと、命令長はIA-32よりも必ず2バイト長くなることになり、かなり冗長な命令になってしまう。
命令でもなんでもないモノに2バイトも使ってしまうのはさすがに無駄だし、命令のフェッチ毎にメモリを叩くわけなので実行速度にも影響する。

そこでもう少し深く考えてみる。
レジスタの本数を倍にし、さらにレジスタ長が64bitであることを指定するためには、あと4bitあればいい。
命令は1バイト(8bit)単位なので、残り4bitだけをプリフィックスとすることができれば無駄が無い。
つまり上位4bitをエスケープのためのプリフィックス、下位4bitを拡張用のビットとするわけだ。
そうすれば命令長がIA-32より1バイト長くなるだけで済む。
しかしその代わり4bit分、つまり16個もの命令とトレードオフにしなくてはならない。
そんなにたくさんの命令を削除しなくてはならないのだが、何を犠牲にしたものか。
IA-32には8086からの名残で、レジスタのincとdec命令が1バイトのものと2バイトのものがあり、2つずつダブって存在している。(具体的には、ModR/Mを使わないものと使うものの2種類)
レジスタが8本なので、incで8命令、decで8命令、合計16命令分がダブっている。
このうち2バイトのものはなんのメリットもないのでIA-32ではほとんど出番がなかった。
そこでAMD64では、inc/dec命令をこの2バイトのものだけ使うようにし、1バイトのinc/dec命令だったコード(0x40~0x4f)を犠牲にしてプリフィックスにしている。
この16種類のプリフィックスを「REXプリフィックス」と呼ぶ。
AMD64のコードは、REXプリフィックス(エスケープ用4bit+レジスタ拡張用4bit)+IA-32の命令コード+ModR/M+SIB+オペランド・・という構成になる。
つまり、AMD64の64bit命令はほとんどが“0x4”(0100)から始まるということになり、CPUを新規設計したときよりも、4bitほど無駄に命令が長くなってしまうわけだが、互換と拡張の絶妙なバランスを実現していると思う。
この仕組みから、AMD64では32bitアーキテクチャのままレジスタの本数を倍にすることはできないし、同時にIA-32では出来ていたリアルモードで32bit長レジスタにアクセスするようなこと(レジスタ8本のまま64bitアーキテクチャとして使うといった)ことはできない。
余談だがこのREXプリフィックスを付けると、前記したsp, bp, si, diの4本のレジスタも、それぞれ8bitレジスタspl, bpl, sil, dilとして使えるようになる。(その代わりah, ch, dh, bhの上位8ビットは使えなくなる。[spl, bpl, sil, dil]と[ah, ch, dh, bh]はトレードオフとなる)
このREXプリフィックスの採用がAMD64とIA-32の非互換部分でもっとも大きな点だが、他にも削除された命令や追加された命令も多数あって、細かいところで互換性はもう少し犠牲になっている。

この仕様から、IA-32用にコンパイルされたコードを「ロングモード-64bitモード」で実行しようとするとinc/dec命令をREXプリフィックスとして誤認してしまうため、暴走してしまう。
この場合は再コンパイルが必要だ。



しかし、64bit版Windows(x64版Windows)では、32bitのアプリを再コンパイルすることなく普通に起動し実行することができる。
何故そんなことが可能なのかというと、Windows上のWOW64が、32bit版Windowsの振りをしているのだ。
考え方はエミュレートと同じようなもの。
x64版Windowsでは、CPUは常に「ロングモード-64bitモード」で動いていている。
本来AMD64には「ロングモード-互換モード」というモードが備わっており、このモードを使用するとロングモードのままIA-32コードを走らせることが出来る。
OSが64bitモードと互換モードのスイッチングを管理してやれば、AMD64コードとIA-32コードの混在が可能である、というのがAMD64の最大の売りであるはずだった。
しかし、前記したとおりx64版Windowsではこの「互換モード」はまったく使用されていない。
何故そのような設計になっているのだろうか。
それは64bit版Windowsの歴史に起因する。
実は64bit版Windowsには2種類ある。
もともと64bit版Windowsというのは、IA-64を採用したCPUであるItanium版として設計・開発が始まった。
当時は64bit版Windowsといえば、IA-64版しかなかったのだ。
IA-64はIA-32とはまったくコードが別物であり、Itaniumに搭載されていたIA-32エミュレータはあまりに速度が遅かった。
そこでマイクロソフトはIA-64版WindowsのWOW64にソフトウェアのIA-32エミュレータを組み込んだ。
これはItanium内蔵のIA-32エミュレータよりも2倍程度の速度で実行可能だった。
やがてマイクロソフトがx64版Windowsの設計・開発を始めたとき、このIA-64版の仕組みを変えなかった。
何故かと言えば、IA-64版とx64版、「同じソースをコンパイルすれば、どちらでも動く」という仕様にしたかったからだ。
そんな背景があるため、x64版Windowsに搭載されているWOW64にもこの仕組み(ソフトウェアのIA-32エミュレータ)を使っている。
前記したとおり、IA-32とx64のコードはほぼ同じであり、エミュレーション時には最小限のコード変換ですむためにオーバーヘッドがほとんど生まれず、IA-64版よりもはるかに高速に動作できる、ということであり、「x64版Windows上でIA-32用コードがそのまま走っている」というわけではない。
すなわち、すごく簡単にいうと「REXプリフィックスになるため犠牲になってしまったinc/decの16命令はWOW64がうまくコード変換しながら実行している」というわけだ。
実際にはWOW64は他にもいろいろと世話を焼いている。
ABI呼び出しもWOW64が32bit版Windowsのふりをすることで実現しているし、32bit版DLLやメモリの再マッピングから、構造体の32bit→64bit変換までも行っている。
要するに、32bitのアプリケーションは、根底では完全に64bitのコードになって動いている。
つまり理論上、32bitコードの実行速度はエミュレーションをしていない分64bit版Windowsよりも32bit版Windows上の方が速い、ということになる。(とはいえカーネルを含むOS自体の実行速度は64bit版の方が速いし、体感速度で分かる人はいないだろうが)

この仕組みのおかげで、x64とIA-64のWindowsソース(とアプリケーションのソース)は完全に同じ物であり、コンパイルすることでそれぞれのアーキテクチャ用のコードを生成することができるようになっている。

というわけで、x64版Windowsでは、AMD64アーキテクチャで本来可能であるはずの32bitコードと64bitコードのシームレスな相互呼び出し機能(64bitモードと互換モードのスイッチング)は全く生かされていない。
そういえばCP/Mは8080コードのみで書かれているので、Z80で動かした場合にZ80固有のレジスタや機能が余っていたし、MS-DOSは8086コードで書かれているので、80286や80386で動かした場合にも同じように機能が余っていた。
必ずしもOSはCPUのすべてを使い切っていない、という歴史がある。

余談だが、メモリアドレスの話。
実はAMD64内部では物理アドレス空間は48bitリニアではなく、32bit単位で65536ページに分割されている。
IntelがIA-32で拡張した4GB単位のページングをCPUレベルで行っているにすぎず、プログラマーからはあたかもフラットでリニアなメモリ空間があるように見えるようになっているだけだ。
そもそも今の時代プログラマーにとって重要なのは仮想アドレスであり、物理メモリモデルがどうなっているかなど、C言語はもとよりアセンブリでコーディングする場合ですら関係ない。
そしてCPUにとって重要なのは、仮想アドレス空間をどう物理アドレスにマッピングするかということで、それを実現するための手段がセグメントだろうがページングだろうがリニアでフラットだろうが関係ない時代になっているということだ。

ほかにもAMD64は、開発当時において普及していたx86系CPUが装備するほとんどの仕様を詰め込んでいる。
悪く言えば思想の一貫性がないのだが、この「ゼロから作り替えようとせず、独自のものを押しつけようとせず、下手にシンプルにしようとせず、良いか悪いか論ずることなくデファクトスタンダードをとことん改良する」という姿勢が、AMD64の一番の勝因だったのではないかと思う。





[ 戻る ]