だから、ポイントに右。 Linux、NASM、およびQEMUを使用して作成します。 これは簡単にインストールできるため、この手順は省略してください。
読者は少なくとも基本レベルでNASMの構文に精通している(ただし、ここでは特に複雑なことはない)ことを理解し、レジスタとは何かを理解しています。
基礎理論
コンピューターの電源を入れたときにプロセッサを起動する最初のことはBIOSコード(またはUEFI、ただしここではBIOSについてのみ説明します)です。
BIOSをオンにした直後に、電源投入時セルフテスト(POST)が開始されます-電源投入後のセルフテスト。 BIOSはメモリの状態をチェックし、接続されているデバイスを検出して初期化し、レジスタをチェックし、メモリのサイズを決定します。
次の手順では、OSを起動できる起動ディスクを特定します。 ブートディスクは、最初のセクターの最後の2バイト(1セクター= 512バイトであるため、最初のセクターはドライブの最初の512バイトを意味する)が55(AA)(16進形式)であるディスク(またはその他のドライブ)です。 ブートディスクが見つかるとすぐに、BIOSは最初の512バイトをアドレス0x7c00のRAMにロードし、このアドレスのプロセッサに制御を渡します。
もちろん、これらの512バイトでは、本格的なオペレーティングシステムに適合しません。 そのため、通常、このセクターにはプライマリローダーを配置します。プライマリローダーは、メインOSコードをRAMにロードし、制御を転送します。
プロセッサは最初からリアルモード(= 16ビットモード)で動作します。 これは、16ビットデータのみで機能し、セグメントメモリアドレス指定を使用し、1 MBのメモリのみをアドレス指定できることを意味します。 しかし、2番目はここでは使用しません。 以下の図は、制御をコードに転送するときのRAMの状態を示しています(図はここから取得されます )
実用的な部分の前に言う最後のことは中断です。 割り込みは、現在のコードの実行を即座に中断し、割り込みハンドラコードを実行する必要があることを示すプロセッサへの特別な信号(キーボードやマウスなどの入力デバイスから)です。 割り込みハンドラのすべてのアドレスは、メインメモリの割り込み記述子テーブル(IDT)にあります。 各割り込みには、独自の割り込みハンドラがあります。 たとえば、キーボードキーが押されると、割り込みが呼び出され、プロセッサが停止し、割り込みを受けた命令のアドレスを記憶し、そのレジスタのすべての値を(スタック上に)保存し、割り込みハンドラーの実行に進みます。 実行が終了するとすぐに、プロセッサはレジスタの値を復元し、中断された命令に戻って実行を継続します。
たとえば、画面に何かを表示するために、BIOSは0x10割り込み(16進形式)を使用し、0x16割り込みを使用してキーが押されるのを待ちます。 実際、これらはすべてここで必要な割り込みです。
また、各割り込みには、その動作の特性を決定する独自のサブ関数があります。 テキスト形式(!)で何かを表示するには、AHレジスタに値0x0eを入力する必要があります。 さらに、割り込みには独自のパラメーターがあります。 0x10は、ah(特定のサブ関数を定義)とal(印刷される文字)から値を取ります。 このように
mov ah, 0x0e mov al, 'x' int 0x10
文字「x」を表示します。 0x16は、ah(特定のサブ機能)から値を取得し、入力されたキーの値をレジスタalにロードします。 0x0関数を使用します。
実用部
ヘルパーコードから始めましょう。 2行を比較する機能と、画面に1行を表示する機能が必要です。 コメントでこれらの機能の操作をできるだけ明確に説明しようとしました。
str_compare.asm:
compare_strs_si_bx: push si ; push bx push ax comp: mov ah, [bx] ; , cmp [si], ah ; ah jne not_equal ; , cmp byte [si], 0 ; , je first_zero ; inc si ; bx si inc bx jmp comp ; first_zero: cmp byte [bx], 0 ; bx != 0, , jne not_equal ; , not_equal mov cx, 1 ; , cx = 1 pop si ; pop bx pop ax ret ; not_equal: mov cx, 0 ; , cx = 0 pop si ; pop bx pop ax ret ;
この関数は、パラメーターとしてSIおよびBXレジスターを受け入れます。 文字列が等しい場合、CXは1に設定され、それ以外の場合は0に設定されます。
レジスタAX、BX、CX、およびDXが2つのシングルバイト部分に分割されていることにも注意してください:AH、BH、CH、およびDHは上位バイト、AL、BL、CLおよびDLは下位バイトです。
最初に、bxおよびsiには、行の先頭が位置するメモリ内のあるアドレスへのポインタ(!)(つまり、メモリにアドレスを格納する)があることが理解されています。 操作[bx]はbxからポインターを取得し、このアドレスに移動してそこから値を取得します。 inc bxは、ポインターが元のアドレスの直後のアドレスを参照することを意味します。
print_string.asm:
print_string_si: push ax ; ax mov ah, 0x0e ; ah 0x0e, call print_next_char ; pop ax ; ax ret ; print_next_char: mov al, [si] ; cmp al, 0 ; si jz if_zero ; int 0x10 ; al inc si ; jmp print_next_char ; ... if_zero: ret
パラメータとして、この関数はSIレジスタを取得し、バイトごとに文字列を出力します。
それでは、メインコードに移りましょう。 最初に、すべての変数を定義しましょう(このコードはファイルの最後にあります):
; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
キャリッジリターン文字は、キャリッジを画面の左端、つまり行の先頭に移動します。
input: times 64 db 0
入力用のバッファーの下に64バイトを割り当て、それらをゼロで埋めることを意味します。
残りの変数は、一部の情報を表示するために必要です。コードをさらに下に行くと、それらがすべて必要な理由がわかります。
times 510 - ($-$$) db 0 dw 0xaa55
つまり、出力ファイル(拡張子.bin)のサイズを明示的に512バイトに設定し、最初の510バイトをゼロで埋め(もちろん、コード全体が実行される前に埋めます)、最後の2バイトを同じ「マジック」バイト55およびAAで埋めます。 $は現在の命令のアドレスを意味し、$$はコードの最初の命令のアドレスです。
実際のコードに移りましょう:
org 0x7c00 ; (1) bits 16 ; (2) jmp start ; start %include "print_string.asm" ; %include "str_compare.asm" ; ==================================================== start: mov ah, 0x00 ; (3) mov al, 0x03 int 0x10 mov sp, 0x7c00 ; (4) mov si, greetings ; call print_string_si ; mainloop
(1)。 このコマンドにより、NASMが0x7c00から始まるコードを実行していることが明確になります。 これにより、このアドレスに関連するすべてのアドレスに自動的にバイアスをかけることができるため、明示的には行いません。
(2)。 このコマンドは、16ビットモードで動作していることをNASMに指示します。
(3)。 QEMUを起動すると、必要のない多くの情報が画面に出力されます。 これを行うには、ah 0x00、al 0x03に設定し、0x10を呼び出してすべての画面をクリアします。
(4)。 レジスタをスタックに保存するには、SPスタックポインターを使用して、頂点が配置されるアドレスを指定する必要があります。 SPは、次の値が書き込まれるメモリ内の領域を指します。 スタックに値を追加します-SPはメモリを2バイト下げます(すべてのレジスタオペランドが16ビット、つまり2バイトの値であるリアルモードであるため)。 0x7c00を指定したため、スタック上の値はメモリのコードのすぐ隣に保存されます。 もう一度-スタックが大きくなります(!)。 これは、スタックにある値が多いほど、SPスタックポインターが指すメモリが少なくなることを意味します。
mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop...
メインループ。 ここでは、各反復で「>」記号を出力し、その後get_input関数を呼び出します。この関数は、キーボード割り込みの処理を実装します。
get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ;
(1)[input + bx]は、入力バッファinputの先頭のアドレスを取得してbxを追加する、つまりbx +バッファの最初の要素に到達することを意味します。
stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $
ここではすべてが簡単です。Ctrl+ Cを押すと、コンピューターはjmp $関数を無限に実行します。
backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ;
バックスペースを押したときに「>」文字を消去しないように、入力が空かどうかを確認します。 そうでない場合は、何もしません。
check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!"
ここでは、コメントからすべてが明確であると思います。
equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done
入力内容に応じて、help_desc変数のテキストまたはwrong_command変数のテキストを表示します。
; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret
実際、コード全体は次のとおりです。
prompt.asm:
org 0x7c00 bits 16 jmp start ; start %include "print_string.asm" %include "str_compare.asm" ; ==================================================== start: cli ; , ; mov ah, 0x00 ; mov al, 0x03 int 0x10 mov sp, 0x7c00 ; mov si, greetings ; call print_string_si ; mainloop mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop... get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ; stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $ backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ; check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!" equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done ; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret ; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
これをすべてコンパイルするには、次のコマンドを入力します。
nasm -f bin prompt.asm -o bootloader.bin
そして、出力でコードを含むバイナリを取得します。 次に、このファイルを使用してQEMUエミュレーターを実行します(-monitor stdioを使用すると、print $ regコマンドを使用していつでもレジスター値を表示できます)。
qemu-system-i386 bootloader.bin -monitor stdio
そして出力が得られます: