X86アセンブラ/x86アーキテクチャ
x86アーキテクチャ[編集]
x86アーキテクチャは8個の汎用レジスタ (GPR) と6個のセグメントレジスタ、1個のフラグレジスタ、1個の命令ポインタを持っている。
64ビットCPUでは、さらに上述以外の補助的なレジスタもあるが、とりあえず上述のレジスタが基本的なので、上述のレジスタについて説明する。
汎用レジスタ (GPR)[編集]
汎用レジスタのうち、特に重要な8個には、下記のような名前がついている。また、その8個のレジスタの使用目的は下記の様に規定されている。
- RAX/EAX/AX/AL/AH : 「アキュムレータレジスタ」と呼ばれる。算術演算操作の結果が格納される。
- RCX/ECX/CX/CL/CH : 「カウンタレジスタ」と呼ばれる。シフトローテート命令とループ命令に使用される。
- RDX/EDX/DX/DL/DH : 「データレジスタ」と呼ばれる。算術演算操作とI/O操作に使用される。
- RBX/EBX/BX/BL/BH : 「ベースレジスタ」と呼ばれる。セグメントモードでのDS(後述)に格納されたデータを指し示すために使用される。
- RSP/ESP/SP : 「スタックポインターレジスタ」と呼ばれる。スタックのトップを指し示すポインタ。
- RBP/EBP/BP : 「スタックベースポインタレジスタ」と呼ばれる。スタックのベースを指し示すのに使用される。
- RSI/ESI/SI : 「ソースレジスタ」と呼ばれる。ストリーム操作コマンド(たとえば MOV命令)でのソース(入力元)へのポインタとして使用される。
- RDI/EDI/DI : 「デスティネーションレジスタ」と呼ばれる。ストリーム操作コマンドでの転送先(←英語でデスティネーションという. 出力先のようなもの)へのポインタとして使用される。
インテルの用意している命令コマンドの中には、特定のレジスタを対象にしたものがあるので、上述のレジスタの規約を守らなければならない。
たとえばintelが用意するアセンブラコマンドの enter 命令は、対象レジスタがebpレジスタとespレジスタだと規定されている事が、2004年のデベロッパーマニュアルで読み取れる(『中巻A:命令セット・リファレンスA-M』3-217)。
反対意見もあり、一説では、上述のレジスタは他の用途にも使える[1]ともいう言説を紹介している文献もあるが、しかし当のインテル社自体、いくつかの命令コマンドの対象を特定のレジスタに適用しているので、他の用途に使うのはあまり推奨されないだろう。
実際、一般的な入門書では、上述のような用途(たとえばEAXはアキュムレータに使うなど)で使うのが、推奨されている[2][3]。
さて、これら目的の規定されたレジスタとは別に、特に目的の規定されていないレジスタがあり、R8 から R15 までの番号のレジスタも汎用レジスタである[4]、たとえば一時的な値の格納に使われたりするレジスタである[5]。主に64ビットCPUで、R8 〜 R15 が追加されている[6]。 また、これら R8 〜 R15 には特に目的は規定されておらず、任意の目的に使用することができる[7]。
ただし、ABIの規定により、R10がCPUのソフトウェア割込みであるシステムコールに使われる[8]など、いくつかの取決めがある。
命令ポインタ(RIP/EIP/IP)とフラグレジスタ(dflags, eflags)は、(8個の)汎用レジスタではない。命令ポインタなどについては、別の節で説明する。

汎用レジスタの最初の4個は、4通りのアクセスの仕方ができる。
- 64ビットアクセス: RAX、RCX、RDX、RBX
- 32ビットアクセス: EAX、ECX、EDX、EBX(80386以降で拡張され使用可能)
- 16ビットアクセス: AX、CX、DX、BX
- 8ビットアクセス: AL、CL、DL、BL(AX、CX、DX、BXの(下位)LSB側8bit)
- 8ビットアクセス: AH、CH、DH、BH(AX、CX、DX、BXの上位(MSB)側8bit)
64ビットCPUにおける「EAX」とは、RAXに使っているレジスタの下位32ビットのことである。けっして、RAXレジスタとは別にEAXレジスタが用意されているわけではない[9]。
なので、上述の一覧では32ビット「アクセス」のように言っているわけである。
同様に、64ビットCPUではRAXレジスタの下位16ビットを「AX」と読んでいる。
RAX以外のRCXやRDXも同様である。RCXの下位32ビットがECXであり、下位16ビットがCXである。
なお、現代的なCPUでは、さらに128ビット用のレジスタ(XMM0 〜 XMM15)も用意されているが[10]、しかし入門的でないので128ビット用レジスタについては本ページでは説明を省略する。
- 経緯など
8ビットアクセスの命令は、昔のintel 8008 の命令幅が8ビットだった時代の名残り(なごり)である。 [11]
8ビットアクセスのALなどの末尾のLは、下位(Low)のこと [12]。AHなどの末尾HはHighのこと。
8ビットを基準に、拡張 extended したので、16ビットには「X」(extend の2文字目)をつけてAX,CX,DX,などのように言うようになった(もともと8ビットでは A,C,D などと呼んでいた.。ALやAHなどのL,Hは、その拡張のころにつけられた末尾)。
さらに32ビットに拡張する際、Extend の1文字目eを付けて区別して、eax,ecx,edx のように呼ぶようになった。
rax などの頭文字 r は、64ビット用のこと。
最後の4個は、2通りのアクセス方法がある。
- 32ビットアクセス: ESP、EBP、ESI、EDI(80386以降で拡張され使用可能)
- 16ビットアクセス: SP、BP、SI、DI
セグメントレジスタ[編集]
セグメントとは、メモリ空間について「先頭アドレスとサイズ」を定義し一纏めにした領域(いわば配列)のようなものであり、用途(スタック領域を指し示す・コード領域を指し示す・データ領域を指し示す)毎に、そのメモリ領域の先頭を示すポインタであるのがセグメントレジスタである(これはリアルモードの場合、プロテクトモードではセグメントディスクリプタが入る)。
6個のセグメントレジスタは以下のとおりである。
- SS : スタックセグメント。スタックへのポインタ。
- CS : コードセグメント。コードへのポインタ。
- DS : データセグメント。データへのポインタ。
- ES : エクストラセグメント。追加のデータへのポインタ。(EはExtraのEである)
- FS : Fセグメント。さらに追加のデータへのポインタ。(FはEの次である)
- GS : Gセグメント。さらにさらに追加のデータへのポインタ。(GはFの次である)
ほとんどの現代的なオペレーティングシステム(LinuxやMicrosoft Windowsなど)で動くほとんどのアプリケーションは、ほとんど全てのセグメントレジスタが同じ場所を指し示すメモリモデルを採用しており(代わりにページングを使用している)、事実上これらの使用は無効になっている。 FSやGSはこの決まりの例外となっており、スレッド独自のデータを指し示すのに使われる。
EFLAGSレジスタ[編集]
EFLAGSは1個の32ビットレジスタで、操作の結果やプロセッサの状態の格納と制御のための判断材料として使用される。
これらのビットの名前は、以下のとおりである。
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ID | VIP | VIF | AC | VM | RF |
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
0 | NT | IOPL | OF | DF | IF | TF | SF | ZF | 0 | AF | 0 | PF | 1 | CF |
ビットの名前が0または1になっているものは予約されているので、変更されるべきではない。
これらのフラグの使い方はそれぞれ以下のようである。 | |
---|---|
0. | CF : キャリーフラグ。最後の算術演算操作で加算においてレジスタの大きさを越えてビットのキャリー(桁上がり)かボロー(桁借り)をした場合にセットされる。これは、キャリーの生じた加算やボローの生じた減算の次の操作がされた場合に、1個のレジスタだけ扱うことのできる値であるか確認するのに使われる。 |
2. | PF : パリティフラグ。結果の最下位バイトに値1 のビットが偶数個含まれている場合にセットされ、奇数個の場合にはクリアされる。 |
4. | AF : 調整フラグ。2進化10進 (BCD) 演算の算術演算でキャリーまたはボローが生じたらセットされる。 |
6. | ZF : ゼロフラグ。操作の結果がゼロ (0) になった場合にセットされる。 |
7. | SF : 符号フラグ。操作の結果が負となった場合にセットされる。 |
8. | TF : トラップフラグ。ステップバイステップのデバッギングをする場合にセットする。 |
9. | IF : 割り込み可能フラグ。割り込みを有効化したい場合にセットする。 |
10. | DF : 方向フラグ。ストリームの方向を制御する。セットするとストリング命令においてポインタがデクリメントされる(通常はインクリメントされる)。 |
11. | OF : オーバーフローフラグ。符号付き算術演算の結果がレジスタに格納できないほど大きい値になった場合にセットされる。 |
12-13. | IOPL : I/O特権レベルフィールド(2ビット)。現在のプロセスのI/O特権レベルを示す。 |
14. | NT : ネストタスクフラグ。割り込みチェーンを制御する。現在のプロセスが次のプロセスにリンクされている場合にセットされる。 |
16. | RF : 再開フラグ。デバッグ例外への応答を制御する。 |
17. | VM : 仮想8086モード。8086互換モードにある場合にセットされる。 |
18. | AC : アラインメントチェックフラグ。メモリチェックでアラインメントチェックが有効な場合にセットされる。 |
19. | VIF : 仮想割り込みフラグ。IFの仮想イメージ。 |
20. | VIP : 仮想割り込み保留フラグ。割り込みが保留されている場合にセットされる。 |
21. | ID : 識別フラグ。セットできればCPUID命令がサポートされる。 |
命令ポインタ[編集]
命令ポインタ (EIP) は、分岐が起きない前提で、次に実行する命令のアドレスを保持している。
EIPはcall命令の直後にのみ読むことができる。
メモリ[編集]
x86アーキテクチャはリトルエンディアンであり、16bitからなるワードや32bitからなるロングワードデータなどのマルチバイトデータの値は最下位バイトから順に書き込まれる。 これはバイト単位の順序についてであり、ビット単位の順序についてではない。
つまり、32ビットの値である0xB3B2B1B0は(以下0xは16進数を示す)、x86システムのメモリ上では以下のように表現される。
B0 | B1 | B2 | B3 |
同様に、32ビットの倍精度ワード0x1BA583D4 は、メモリ上では以下となる。
D4 | 83 | A5 | 1B |
したがって、メモリをバイト単位で表示すると0xD4, 0x83, 0xA5, 0x1Bのように見える。
2の補数表現[編集]
2の補数というのは、2進数で負の整数を表現する標準的な方法である。 数値の符号を変えるには、全てのビットを逆にし、1を足せば良い。
00012 | |
これの各ビットを反転させると以下のようになる |
11102 |
これに1を足す。 |
11112 |
00012は10進数での1である。
11112は10進数での-1である。
アドレッシングモード[編集]
x86の各機械語は、動作の対象として(オペランドと呼ぶ)、上記レジスタ、メモリ空間あるいは数値(即値と呼ばれる)にアクセスするが、その方法は命令により限定されている。
アドレッシングモードとは、オペランドにアクセスする方法を示す、名称である。
- レジスタアドレッシング
mov ax, bx ; レジスタbxの内容をaxにコピーする
- イミディエイト(即値)
- (値がその場で与えられる)
mov ax, 1 ; 値1をレジスタaxに設定する
あるいは、
mov ax, 0x010C ; 値0x010Cをレジスタaxに設定する
- 直接メモリアドレッシング
mov ax, [102h] ; 実際の値は、DS:0として 0 + 102= 0x102であるメモリアドレスの値が設定される。
- 直接オフセットアドレッシング
- (アドレスを修正するために算術演算をする)
byte_tbl db 12,15,16,22,..... ; バイトテーブル
mov al,[byte_tbl+2]
mov al,byte_tbl[2] ; 上と同じ
- レジスタ間接
- (オペランドのアドレスを格納したレジスタへのポインタを書く)
mov ax,[di]
- インダイレクトアドレスに使われるレジスタはBX、BP、SI、DIである。
- ベース-インデックス
mov ax,[bx + di]
- 例えば、配列について言えば、bxはベースのアドレスであり、diが配列のインデックスとなる。
- ディスプレイスメント付きベース-インデックス
mov ax,[bx + di + 10]
スタック[編集]
スタックとは後入れ先出し方式のデータ構造で、データはスタックに上からプッシュされ、入れた順序を逆にポップしてくる。
mov ax, 006Ah
mov bx, F79Ah
mov cx, 1124h
push ax
AXの値をスタックの最上部にプッシュするとする。スタックは$006Aという値を保持している。
push bx
BXの値を同様にスタックにプッシュする。スタックは$006Aと$F78Aの値を保持している。
push cx
同様にCXの値もプッシュすると、スタックは$006A、$F79A、$1124を保持することとなる。
call do_stuff
何かをする。関数はレジスタを使わないものとする。
pop cx
スタックに最後にプッシュされた要素をポップさせてCXに読み込む。CXの値は$1124となり、スタックには$006A、$F79Aが保持されている。
pop bx
スタックの最上部の要素をポップさせてBXに読み込む。BXの値は$F729となり、スタックには$006Aしかなくなる。
pop ax
スタックの最上部の要素をAXに読み込む。AXの値は$006Aとなり、スタックは空となる。
スタックは通常、引数を関数や手続きに渡すために使われる。また、call命令が使われた時に制御フローを監視するためにも使われる。 その他のスタックのよくある使い方は、一時的にレジスタの値を保存しておくことである。
CPUの動作モード[編集]
2010年代の現代のパソコンのアーキテクチャも、深層部は ほぼ16ビット時代の名残り(なごり)のままである。
市場のパソコンのアーキテクチャは事実上、昔からの技術のツギハギになっており、16ビットCPUの名残のモード(「リアルモード」)の上に32ビットCPUのモード(「プロテクトモード」という)を付け加え、さらにその上に64ビットCPUのモードを付け加えるという、ツギハギのような状態になっているのである。
コンピュータの電源投入の起動直後、CPUはまず(16ビット時代の名残りである)リアルモードを実行し、それを土台に(32ビット時代の名残りの)プロテクトモードに切り替え、必要ならさらに64ビット時代のモードに切り替える、という仕組みになっている。
リアルモード[編集]
リアルモードとは、16ビットCPUの時代のモードである。
この16ビットCPU時代のモードの構造が比較的に単純[13]だからか、2010年代の今でもOSなどの深層部に残っており、ブート起動直後に動作するのは実は16ビット時代のモードである。この、ブート直後などの16ビット時代のモードのことを「リアルモード」とIT業界では呼んでいる。
リアルモードは、仮想メモリをサポートしていないし、マルチタスク処理をサポートしていない[14]。
リアルモードは元々の(1978年の)インテル8086プロセッサからの遺産である(※ 1980年代の「80386」でなく「8086」。80386とは別物)。一般的にはこれについて何も知る必要はない ←(いや、アセンブラの技術者なら知る必要あるだろ。)(MS-DOS用のプログラムを書いたり、BIOSから直接実行されるブートローダを書いたりしない限りは)。
- ※ 一般的なアセンブラプログラマーがブートローダを書かないとか、勝手に英語版著者は決め付けないでほしい。つうか、(OS自作などの目的で)ブートローダーでも書かない限り、いまどきアセンブラの使い道のほうが少ないだろう。株式会社マイナビ『自作エミュレータで学ぶX86アーキテクチャ』なんて、ブートローダについて長々と説明しているぞ。
- 経緯
インテル8086はメモリにアクセスするのに20ビットのアドレスを使っていた。しかし、プロセッサ自体は16ビットであったため、インテルは、20ビットのアドレッシングスペースを16ビットワードに割り当てる方法を用意するという、アドレッシングスキームを発明した。現在のx86プロセッサは、後方互換性のため、若干の違いはあるがこの8086の動作を真似た動作モードである、リアルモードで起動する。
- 詳細
リアルモードでは、セグメントレジスタとオフセットレジスタの両方が最終的なメモリアドレスを得るために使われる。セグメントレジスタの値は、16倍(つまり左に4ビットシフト)し、オフセットが加算されてメモリアドレスを得る。これによって1MBのスペースが使えるようになった。しかし、このアドレッシング方法によって、セグメントレジスタの値をその最大値である0xFFFFにすることによって、1MBを超える領域にアクセスできるようになった。 8086と8088では、このエリアへの全てのアクセスによってメモリを使い切ることとなるが、80286以降では、この1MB以降に最大65520バイトまで、A20アドレスラインを有効化することにより、同様な方法でアクセスすることができる。参照: A20ゲートの物語
リアルモードのセグメントとプロテクトモードでのマルチセグメントメモリモードに共通するの利点は、全てのアドレスがあるアドレスから相対的な位置として与えられるということである(これをセグメントベースアドレスと呼ぶ)。プログラムはそれ自身のアドレススペースを持つことができ、完全にセグメントレジスタを無視することができる。そして、プログラムを実行するためにはポインタを移動させる必要がない。プログラムは近くにあるコールを実行し、同じセグメント内でジャンプする。そしてデータは常にセグメントベースアドレスから相対的にある(リアルモードアドレッシング方法では、セグメントレジスタに格納された値から計算される)。
これはDOSの*.COMフォーマットがしていることである。ファイルの内容はメモリに読み込まれ、そのまま実行される。しかし、リアルモードセグメントは常に64KBであるため、COMファイルは64KBより大きくすることができない(実際には65280バイトまで可能であるが、DOSはセグメント最初の256を管理用に使う)。長い間、これは問題にはならなかった。
プロテクトモード[編集]
フラットメモリモデル[編集]
LinuxやWindowsといった現代的なオペレーティングシステムでプログラミングする場合には、基本的にはフラット32ビットモードでプログラミングをする。 全てのレジスタはアドレッシングに使うことができ、完全に32ビットのレジスタを16ビットレジスタの代わりに使うことで、より効率的になる。 加えて、セグメントレジスタは一般的にはフラットモードでは使われず、これに触れるのは悪い考えであるといえる。
マルチセグメントメモリモデル[編集]
(未執筆)
日本語版での参考文献および脚注[編集]
- 参考文献
- 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行
- 内田公太・上川大介 著『自作エミュレータで学ぶX86アーキテクチャ』、株式会社マイナビ (※出版社名)、2015年8月30日 初版 第1刷 発行
- Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、
- 脚注
- ^ 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行、27ページ
- ^ 内田公太・上川大介 著『自作エミュレータで学ぶX86アーキテクチャ』、株式会社マイナビ (※出版社名)、2015年8月30日 初版 第1刷 発行
- ^ 以前の編集で、レジスタの用途を「慣習的に」としていたが、同じ命令でもレジスタによって命令語長が違う、あるいは実行にかかるマシンサイクルに差異があるなど明確な理由がある。
- ^ 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行、26ページ
- ^ Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、11ページ
- ^ 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行、27ページ
- ^ 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行、27ページ
- ^ Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、11ページ
- ^ 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行、27ページ
- ^ 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行、29ページ
- ^ 内田公太・上川大介 著『自作エミュレータで学ぶX86アーキテクチャ』、株式会社マイナビ (※出版社名)、2015年8月30日 初版 第1刷 発行、81ページ
- ^ 内田公太・上川大介 著『自作エミュレータで学ぶX86アーキテクチャ』、株式会社マイナビ (※出版社名)、2015年8月30日 初版 第1刷 発行、81ページ
- ^ 内田公太・上川大介 著『自作エミュレータで学ぶX86アーキテクチャ』、株式会社マイナビ (※出版社名)、2015年8月30日 初版 第1刷 発行、43
- ^ Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、49ページ