در دو مقاله قبلی سعی کردیم یک سیستم عامل ساده بسازیم که توی مد حقیقی ۱۶ بیت کار میکنه و دو تا رشته هم نشونمون میده. بسیار خوب، اگر با سیستم عامل هایی مثل DOS کار کرده باشید قطعا با محیط متنی آشنایید. اگر هم مثل من لینوکسی باشید احتمالا بخش بزرگی از کارهاتون رو توی محیط ترمینال انجام میدید. حتی در ویندوز هم بعضی وقتا نیاز شدیدی به CMD یا همون Command Prompt پیدا میکنیم. پس نیاز داریم که یک محیطی بسازیم که دستورات رو از کاربر بگیره و بهشون پاسخ بده. ما در این قسمت، میایم و دستورات رو از کاربر میگیریم. در قسمت های بعدی دستورات مورد نظرمون رو هم اضافه میکنیم.
بعد از نوشتن prompt و قابلیت خوندن ورودی از کاربر، در واقع به این شکل میشه کدمون:
org 0x7c00
bits 16
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
mov si, welcome
call print_string
mov si, about
call print_string
mov si, newline
call print_string
mainloop:
mov si, prompt
call print_string
mov di, buffer
call get_string
mov si, buffer
cmp byte [si], 0
je mainloop
jmp mainloop
welcome db 'Welcome to my OS', 0x0d, 0x0a, 0
about db 'Written in 16-bit real mode', 0x0d, 0x0a, 0
buffer times 64 db 0
prompt db '>> ', 0
newline db ' ', 0x0d, 0x0a, 0
print_string:
lodsb
or al, al
jz .done
mov ah, 0x0e
int 0x10
jmp print_string
.done:
ret
get_string:
xor cl, cl
.loop:
mov ah, 0
int 0x16
cmp al, 0x08
je .backspace
cmp al, 0x0D
je .done
cmp cl, 0x3F
je .loop
mov ah, 0x0e
int 0x10
stosb
inc cl
jmp .loop
.backspace:
cmp cl, 0
je .loop
dec di
mov byte [di], 0
dec cl
mov ah, 0x0e
mov al, 0x08
int 0x10
mov al, ' '
int 0x10
mov al, 0x08
int 0x10
jmp .loop
.done:
mov al,0
stosb
mov ah, 0x0e
mov al, 0x0d
int 0x10
mov al, 0x0a
int 0x10
ret
times 510-($-$$) db 0
dw 0xaa55
تفاوتش با قبلی چیه؟ درسته یک تابع بلند و بالای get_string اضافه شده، متغیر buffer هم تعریف شده. یک حلقه اصلی هم به اسم mainloop تعریف کردیم. بیایم ببینیم این همه بخش، چه کارایی میکنن؟
بین بخش های اضافه شده، mainloop بخش ساده تریه. پس اول میریم اون رو بررسی کنیم :
mainloop:
mov si, prompt
call print_string
mov di, buffer
call get_string
mov si, buffer
cmp byte [si], 0
je mainloop
jmp mainloop
در بخش اول، یک اسم برای حلقه اصلی گذاشتیم. سپس متغیر prompt رو توی Source Index فرستادیم و تابع print_string رو صدا زدیم. مثل نوشتن اون چیزایی که قبلا نمایش دادیم (پیام خوشامد گویی و این حرفا!). بعدش هم buffer که الان ۶۴ تا صفره رو داخل Destination Index قرار دادیم و تابع get_string رو صدا زدیم. این تابع، در واقع داره یه چیزی رو از ورودی میخونه (چطوری؟ الان بررسیش میکنیم!) . بعد بافر رو به si میفرستیم و بایت به بایت رو با صفر مقایسه میکنیم. اگر همه بایت ها صفر بود(یعنی دستور خالی بود) ، بر میگردیم سر خونه اول. در آخر هم یک پرش میکنیم به حلقه اصلی. چه دستوری وارد شده باشه، چه نشده باشه! حالا بیایم ببینیم تابع get_string مون چطوری کار میکنه؟
get_string:
xor cl, cl
.loop:
mov ah, 0
int 0x16
cmp al, 0x08
je .backspace
cmp al, 0x0D
je .done
cmp cl, 0x3F
je .loop
mov ah, 0x0e
int 0x10
stosb
inc cl
jmp .loop
.backspace:
cmp cl, 0
je .loop
dec di
mov byte [di], 0
dec cl
mov ah, 0x0e
mov al, 0x08
int 0x10
mov al, ' '
int 0x10
mov al, 0x08
int 0x10
jmp .loop
.done:
mov al,0
stosb
mov ah, 0x0e
mov al, 0x0d
int 0x10
mov al, 0x0a
int 0x10
ret
خب اولین کاری که کردیم انجام یه xor روی cl بوده. در واقع هشت بیت پایینی CX رو با خودش XOR کردیم. این یک کلک مرسوم هست برای تولید عدد صفر، مواقعی که نمیخوایم رجیسترهامون صفر شن. بعدش یک حلقه تعریف کردیم. توی این حلقه، اومدیم چه کردیم؟ عدد صفر رو توی AH قرار دادیم سپس از وقفه ۱۶ هگزادسیمال استفاده کردیم. این وقفه منتظر میمونه تا یک کلید فشرده شه. بعدش عدد هگزادسیمال 08 رو وارد AL کردیم. این چی کار میکنه؟ این چک میکنه کلید backspace فشرده شده یا نه. سپس در صورت برابری، میره به زیرتابع backspace که تعریف کردیم. بعدش هم با قرار دادن 0D در AL چک میشه که آیا Enter فشرده شده یا نه؟ و اگر درست باشه، میره به زیرتابع done که باز هم تعریف شده و مشاهده میکنید. بعد اومدیم محتوای CL رو با 3F مقایسه کردیم ، این دستور چک میکنه آیا ۶۳ کرکتر وارد شده؟ و سپس فقط به Enter و Back Space اجازه میده که کار انجام بدن. در واقع نمیتونید کلید دیگری رو فشار بدید. بعدش هم 0E رو داخل AH گذاشتیم و از همون وقفه معروف بایوس استفاده کردیم. این پروسه به شما اجازه میده هر وقت کلیدی رو فشردید، بدون درنگ کرکتر مورد نظرتون رو روی صفحه مشاهده کنید. حالا نوبتی هم باشه، نوبت زیرتابع هایی هست که داریم. اولیش، backspace هست که به این شکله :
.backspace:
cmp cl, 0
je .loop
dec di
mov byte [di], 0
dec cl
mov ah, 0x0e
mov al, 0x08
int 0x10
mov al, ' '
int 0x10
mov al, 0x08
int 0x10
jmp .loop
این زیرتابع، اول میاد CL رو با صفر مقایسه میکنه. اگر برابر بود میره دوباره توی حلقه اصلی زیرتابع که تعریف کردیم. اگر نبود DI رو یک واحد کم میکنه، بعد کرکتر رو پاک میکنه و همچنین یکی از CL کم میکنه، چرا؟ چون طول رشته ما توی CL ذخیره شده. بعد میایم عدد 0x0e رو وارد AH میکنیم (این تابع مربوط به چاپ کرکتر ها و رشته ها توی وقفه 0x10 بایوس هست) ، بعد 08 رو وارد AL میکنیم که پروسه فشرده شدن backspace رو کنترل میکنه بعد هم وقفه 0x10 رو اجرا میکنیم. بعدش میایم یک کرکتر خالی میریزیم توی AL و بعد باز وقفه اجرا میشه و بعدش هم کرکتر خالی رو پاک میکنیم. در آخر هم به Loop برمیگردیم. حالا بریم ببینیم done اینجا چیکار میکنه؟
.done:
mov al,0
stosb
mov ah, 0x0e
mov al, 0x0d
int 0x10
mov al, 0x0a
int 0x10
ret
حقیقتا این تابع برای اینه که وقتی یک رشته وارد کردیم و enter رو زدیم عمل کنه حالا ببینیم چطور کار میکنه؟ اول عدد صفر رو میذاریم داخل AL که نقش NULL terminator رو داره. بعدش دستور stosb رو میزنیم که رشته رو ذخیره میکنه. بعدش هم 0E رو وارد AH میکنیم (همون تابع معروف 😀 ) و بعدش 0D رو در AL میذاریم (اتمام رشته) و کارمون رو با وقفه تموم میکنیم. بعدش با قرار دادن 0x0a و اجرای وقفه معروف، میریم خط بعدی. ret هم که تابعمون رو تموم میکنه!
حالا میتونید با خیال راحت با سیستم عامل ۱۶ بیتی خودتون بازی کنید! برای این که ببینید چه اتفاقاتی پشت پرده داره میفته، میتونید ویدئوی من رو ببینید که سیستم عامل رو اسمبل کردم و در ویرچوال باکس اجراش کردم 🙂
توی ویدئو می بینید که با ورود نوشته ها، دستوری اجرا نمیشه. ولی در آینده چند تا دستور ساده برای سیستم عامل سادمون خواهیم نوشت.
موفق باشید 🙂