نوشتن یک سیستم عامل ساده – قسمت سوم

در دو مقاله قبلی سعی کردیم یک سیستم عامل ساده بسازیم که توی مد حقیقی ۱۶ بیت کار میکنه و دو تا رشته هم نشونمون میده. بسیار خوب، اگر با سیستم عامل هایی مثل 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 هم که تابعمون رو تموم میکنه!

حالا میتونید با خیال راحت با سیستم عامل ۱۶ بیتی خودتون بازی کنید! برای این که ببینید چه اتفاقاتی پشت پرده داره میفته، میتونید ویدئوی من رو ببینید که سیستم عامل رو اسمبل کردم و در ویرچوال باکس اجراش کردم 🙂

توی ویدئو می بینید که با ورود نوشته ها، دستوری اجرا نمیشه. ولی در آینده چند تا دستور ساده برای سیستم عامل سادمون خواهیم نوشت.

موفق باشید 🙂

Share

استفاده از اسمبلر MASM روی لینوکس

اگر شما هم هم رشته من باشید و زبان ماشین برداشته باشید، به احتمال بسیار بسیار بالا استاد این درس، کدهایی بهتون میده که با MASM اسمبل میشن، و طبیعتا شما ناراحت خواهید شد که چرا این اسمبلر نسخه لینوکسی نداره و … 😀 . و جالبه بدونید راهی که توی این پست به شما ارائه میکنم، نه تنها برای لینوکس و سیستم عاملهای غیر از ویندوز، بلکه روی ویندوز ۶۴ بیتی هم باید اجرا بشه.

اولین چیزی که نیاز دارید، این هست که یک ایمولاتور برای شبیه سازی محیط سیستم عامل قدیمی DOS تهیه کنید. برای ویندوز و لینوکس و BSD ها و … ، نرم افزاری ارائه شده به اسم DOSBox که محیط داس رو شبیه سازی میکنه. توی اوبونتو/دبین با این دستور نصب میشه :

کد   
sudo apt-get install dosbox

سپس، شما نیاز به دانلود اسمبلر MASM دارید که میتونید از اینجا دانلودش کنید.

پس از دانلود MASM و خارج کردن اون از حالت فشرده، حالا نیازمند اجرای داس باکس و سوار کردن پوشه MASM هستید. برای این کار ، داس باکس رو اجرا کنید، و دستورات زیر رو درون شل داس تایپ کنید :

کد   
mount f ~/masm
f:

دستور mount پوشه ای که آدرسش را به عنوان ورودی دریافت کرده را درون یک درایو مجازی به نام F سوار میکند و با دستور بعدی، به آن درایو مجازی میرویم. (توجه کنید که پوشه MASM در پوشه خانگی قرار داده شده است. چنانچه در آدرس دیگری قرار داده اید باید مسیر را عوض کنید)

حالا میتوانید با اجرای MASM.EXE یا ML.EXE ، کدی که به زبان اسمبلی نوشته اید را اسمبل کنید.

موفق باشید 🙂

 

Share