نوشتن Makefile برای تسریع فرایند ساخت

احتمال خیلی زیاد، همه ما یک بار توی عمرمون این صحنه رو دیدیم :

کد   
make
make install
make clean

خب ما وقتی یه سورس کدی رو دریافت میکنیم معمولا همراهشم یه Makefile داریم، سازنده اون برنامه، یا همون دوست عزیزی که سورس کد رو برامون نوشته، میتونست بگه کسی که دانلود کرد دیگه خودش بشینه یکی یکی کد ها رو کامپایل کنه و آخرش لینک کنه و این حرفا. ولی خب چون همه توانایی، وقت یا دانش لازم رو ندارن، معمولا یه Makefile نوشته میشه که این کارها درونش صورت بگیره! توی این پست، قراره ما یاد بگیریم که چطور Makefile بنویسیم. اول ببینیم make چیه؟ همونطور که میدونید make به معنای ساختنه. و نرم افزار make هم ساخته شده که بسازه. طبق تعریفی که توی ویکیپدیا و صفحه رسمیش هست، میگن که «نرم افزاریه که فایل های قابل اجرا از یک سری سورس تولید میکنه». البته کارهای دیگری هم میشه باهاش کرد! مثلا همون سورس ها و … رو باهاش منتقل کنیم به جایی که میخوایم و از این قبیل کارا. خب برای نوشتن یک Makefile نیاز به یک ادیتور متنی، نرم افزار make (دقت کنید این نرم افزار جزئی از استاندارد POSIX به حساب میاد و معمولا روی همه یونیکس-لایک ها نصبه)، یک محیط یونیکس لایک (اگر ویندوزی هستید Cygwin یا یک یونیکس لایک مجازی) نیاز داریم. همین؟ درسته همین. حالا با make قراره چی کار کنیم؟ اول بیایم یه Makefile ساده بنویسیم.

سناریوی کلی ما استفاده از نرم افزار echo هست که اونم در یونیکس لایک ها موجوده. ما میخوایم با استفاده از make و echo پیام معروف Hello, World رو نشون بدیم :

کد   
all:
	echo "Hello, World"

خب اینجا دو تا توضیح داریم :

یکی این که all چیه؟ این all یه سوییچ عمومی هست که توی همه Makefile ها موجوده. وقتی که شما صرفا تایپ کنید make هرچی که به این سوییچ گفته شده اجرا میشه، حالا میتونه روی فایل خاصی تغییر اعمال کنه یا نه، مثل ما صرفا یکی از برنامه ها رو فراخوانی کنه. و این که اون فاصله زیاد برای چیه؟ هر وقت نوشتید all (یا هر سوییچ دیگه ای) ، کافیه بیاید خط بعدی و Tab رو بزنید (دقت کنید مطلقا از Space استفاده نکنید چون با هم فرق دارن!) و دستور مورد نظرتون رو وارد کنید!. الان میتونید این تکه کد رو در یک فایل به اسم Makefile ذخیره کنید و بعدش تایپ کنید make و نتیجه رو ببینید! خواهید دید اول دستوری که بهش دادیم و سپس نتیجش رو نشون میده.

بسیار خوب! بیایم یک سناریوی دیگه پیاده کنیم. فرض کنیم شما اومدید :

یک برنامه به زبان C نوشتید، و الان میخواید اون رو کامپایل کنید، در یک دایرکتوری خاص به اسم path (که زیرشاخه دایرکتوری Root هست) قرار بدید، و سپس با یک دستور فایل باینری رو از دایرکتوری سورس حذف کنید. 

قطعا به صورت دستی هم میشه انجام داد، ولی خب منطقی ترش اینه که بیایم و از Makefile استفاده کنیم! فرض میکنیم اسم سورس شما src.c هست و در دایرکتوری ای قرار داره! خب میک فایل ما به این شکل میشه :

کد   
all: src.c
	gcc -o src src.c
 
install: src
	cp -v src /path
 
clean: src
	rm -v src

خب برای این که اینجا، ما سوییچ های all و install و clean رو روی فایلهای مورد نظرمون اعمال کنیم، اسم فایل های مورد نظر رو جلوی سوییچ ها قرار دادیم. توی all میاد از src.c یک فایل باینری به اسم src میسازه! توی install هم اون باینری رو کپی میکنه توی path (البته چون path زیرشاخه دایرکتوری ریشه ماست، باید make رو با sudo اجرا کنید و فکر کنم ترکیب sudo و make و install شدیدا براتون آشناس 😀 )، توی clean هم اومدیم اون فایل باینری رو پاک کردیم! به همین سادگی!

توی این پست سعی کردم تا حد امکان، ساده توضیح بدم چرا که واقعا ساده تر از این نمیشد make رو توضیح داد. البته میشه Makefile های بسیار پیچیده تری هم نوشت که همه می بینیم هستن! مثلا Makefile های FreeBSD هر کدوم غولی به حساب میان. ولی برای پروژه های ساده، خوندن و دونستن همین پست هم کافیه.

امیدوارم که این پست به دردتون خورده باشه، موفق باشید :)

FacebookTwitterGoogle+Share

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

بهرحال، هر شروعی یک پایانی داره و اینجاس که ما به پایان خودمون نزدیک میشیم! پایان قشنگ و دوست داشتنی، وقتی می بینیم دست رنجمون در طول چند هفته (بسته به مهارت برنامه نویسی و حوصله و زمانی که صرف این کار کردید) ، تبدیل شده به یه چیز «تقریبا» قابل استفاده، دوست داشتنی تر هم میشه! بسیار خب کد اسمبلی که نوشتیم و کامل شده به این شکله :

کد   
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
 
 mov si, buffer
 mov di, cmd_reboot
 call strcmp
 jc .reboot
 
 mov si, buffer
 mov di, cmd_about
 call strcmp
 jc .about
 
 mov si, buffer
 mov di, cmd_help
 call strcmp
 jc .help
 
 mov si, badcommand
 call print_string
 jmp mainloop
 
 .reboot:
  int 0x19
  jmp mainloop
 
 .about:
 mov si, msg_about
 call print_string
 jmp mainloop
 
 .help:
 mov si, msg_help
 call print_string
 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
badcommand db 'Bad command entered', 0x0d, 0x0a, 0
cmd_reboot db 'reboot',0
cmd_about db 'about', 0
msg_about db 'This is a 16 bit operating system, works in real mode and written in assembly language', 0x0d, 0x0a, 0
cmd_help db 'help', 0
msg_help db 'Commands are : about, help and reboot', 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
 
strcmp:
 .loop:
  mov al, [si]
  mov bl, [di]
  cmp al, bl
  jne .notequal
 
  cmp al, 0
  je .done
 
  inc di
  inc si
 
  jmp .loop
 
  .notequal:
   clc
   ret
  .done:
    stc
    ret
 
times 510-($-$$) db 0
dw 0xaa55

 

همونطور که می بینید چن قسمت جدید شروع شده ، مثلا تابع strcmp رو ساختیم، چن تا دستور و پیام تعریف کردیم و زیر تابع های reboot و about و help رو هم تعریف کردیم. خب اول یه توضیح کوچیک : دستورات باید حتما معرفی بشن. چرا؟ چون تابع strcmp ورودی کاربر رو با یه سری اطلاعات از پیش تعیین شده میاد مقایسه میکنه و در صورت برابری با مقدار وارد شده، اون دستوری که در برنامه تعریف شده رو اجرا میکنه. پس بخشی که چندین db پشت هم تعریف شده رو جدی بگیرید! بسیار خوب، یکی از توابع خوب و زیبامون تابع strcmp هست. این تابع چیه؟ ما یه دستوری در اسمبلی داریم به اسم cmp که کارش مقایسه است. حالا برای این که ببینیم دو تا رشته با هم برابرن اومدیم strcmp نوشتیم. در واقع مخفف string compare هست. خب این تابع در کد ما به این شکله :

کد   
strcmp:
 .loop:
  mov al, [si]
  mov bl, [di]
  cmp al, bl
  jne .notequal
 
  cmp al, 0
  je .done
 
  inc di
  inc si
 
  jmp .loop
 
  .notequal:
   clc
   ret
  .done:
    stc
    ret

این تابع ، میاد اول یک بایت از si رو میریزه توی ۸ بیت پایینی AX . بعدش یک بایت از di رو میریزه توی هشت بیت پایینی BX . بعد با cmp مقایسه‌ش میکنه، در صورتی که برابر نبودن، میره سراغ notequal که توضیح داده میشه، و اگر برابر باشن، هشت بیت پایین AX چک میشه و اگر خالی بود میره done. در غیر این صورت یک کرکتر میره جلو (با دستور inc یک خونه به جلو حرکت میکنه). بعد میرسیم به بخش دوست داشتنی notequal ، توی این بخش اول Carry Flag که یک بیت از رجیستر Flag در پردازنده ماست، خالی میشه و تابع تموم میشه. در قسمت done هم این بیت پر میشه و تابع تموم میشه! دیدید؟ بسیار ساده تر از چیزی بود که فکرش رو میکردید! حالا بریم کارکرد دستورات رو بررسی کنیم :

کد   
mov si, buffer
 mov di, cmd_about
 call strcmp
 jc .about

خب این بخش برای همه دستورات یکیه، چی کار میکنه؟ خط اول buffer (رشته ای که کاربر وارد کرده) و خط دوم دستور (دستوری که توی اون بخش توسط db تعریف کردیم) وارد رجیستر های SI و BI میشن. بعد تابع strcmp میاد این دو رشته رو با هم مقایسه میکنه. اگر شرط درست باشه، به زیرتابع مورد نظر میریم. در غیر این صورت، زیرتابع badcommand که در قسمت سه و نیم معرفی شد، فراخوانی میشه. حالا بیایم ساختار about رو بررسی کنیم (توجه کنید about ساختارش با help یکیه پس یکیشون کافیه که بررسی شه) :

کد   
.about:
 mov si, msg_about
 call print_string
 jmp mainloop

خب تا حدود زیادی مثل badcommand عمل میکنه (تقریبا همونه با این تفاوت که یه شرط لازم داره برای اجرا شدن) ، اول رشته مورد نظرمون میره توی SI ، بعد print_string صدا زده میشه، و بعد میریم به حلقه اصلی و منتظر میمونیم تا کاربر دستور بعدی رو بده.

این زیرتابع ها، مشابهن. فقط تابع reboot یکم ممکنه اذیتتون کنه، ولی خب اون رو هم اینجا توضیح میدیم :

کد   
.reboot:
  int 0x19
  jmp mainloop

خب طبیعیه که jmp کارش پرش به حلقه اصلیه و برای این میذاریمش که اگر reboot رو کاربر نزنه ، چی بشه؟ هیچ اتفاقی نیفته و سیستم عامل کوچولومون همچنان منتظر باشه کاربر بهش دستور بده. اما وقفه ۱۹ هگزادسیمال یا 0x19 (که بعضی جاها ممکنه 19h هم نوشته شه) ، چیه؟ این وقفه یکی از وقفه های بایوسه که کارش ریبوت کردن (راه اندازی مجدد) سیستم عاملمونه.

تبریکات فراوان! شما الان یک سیستم عامل زیبا و کوچولو نوشتید، که سه تا دستور داره و اگر دستوری غیر از اینا بهش بدید، بهتون ارور میده. در واقع شما کاری رو پیش بردید که توسعه دهنده ها سالیان پیش انجام میدادن تا کامپیوترها بیشتر و بیشتر کاربرپسند بشن! و از این جهت شدیدا باید خوشحال باشید، چرا؟ چون کارهایی رو دوباره کردید که احتمال خیلی زیاد توی شرکتهای بزرگ کامپیوتری انجام میشده! و همچنین تشکرات فراوان بابت این که مطالب من رو خوندید و براش وقت گذاشتید. بزودی سری جدیدی از این مقالات شروع میشه که سیستم عامل کوچولومون رو میبریم توی مود حفاظت شده ( Protected Mode ) که یک مود ۳۲ بیتی هست و احتمال خیلی زیاد از بوت لودر GRUB برای بوت کردنش استفاده خواهیم کرد!

موفق باشید :)

FacebookTwitterGoogle+Share

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

این قسمت، حقیقتا ارزش این که به قسمت چهارم تبدیل بشه رو نداره. برای همین تصمیم گرفتم اسمش رو بذارم سه و نیم! توی قسمت قبلی prompt نوشتیم که دستورات رو از کاربر میخوند اما هیچ جوابی نمیداد. توی این قسمت قصدم اینه که صرفا یک ارور رو روی صفحه نمایش بدم. به این شکل :

error

بسیار خوب! در قسمتی که چندین db داشتیم برای تعریف دو پیام اول و بعد شکل prompt و بعدش بافر، این رو وارد کنید :

کد   
badcommand db 'Bad command entered', 0x0d, 0x0a, 0

و بعد قبل از اتمام mainloop یعنی قبل از آخرین پرشمون، این کد رو اضافه کنید :

کد   
mov si, badcommand
call print_string

خب بعد از اسمبل کردن، خواهید دید که هر کامندی (مثل فاصله، کرکتر های الکی و …) که وارد کنید این ارور به شما نمایش داده میشه! و دلیلش اینه که هنوز دستوری به سیستم عاملمون اضافه نشده. در قسمت های بعدی دستورات مورد نیازمون رو هم به سیستم عامل کوچولومون اضافه میکنیم!

ضمیمه : ساخت فایل ISO قابل بوت از سیستم عامل

از اونجایی که میدونستم این سوال ممکنه پیش بیاد که چطور باید این فایل رو به ISO تبدیل کنیم و بوتش کنیم، در این بخش توضیح میدم. اولا که img و حتی bin قابل بوت شدن هستن (اگر مجازی سازتون میذاره که floppy controller اضافه کنید) ولی خب اگر دلتون میخواد ISO درست کنید ، اول فایل bin که اسمبلر داده بهتون رو به img تبدیل کنید :

کد   
dd if=my16bitos.bin of=my16bitos.img

بعد یه فولدر به اسم cdiso بسازید و فایل img رو ببرید داخلش و بعد این دستور رو بزنید (برنامه genisoimage رو نصب کنید حتما!):

کد   
mkisofs -o my16bitos.iso -b my16bitos.img -no-emul-boot cdiso/

بعدش میتونید فایل ISO رو بدید به یه مجازی ساز مثل VirtualBox یا QEMU یا VMWare .

موفق باشید!

FacebookTwitterGoogle+Share