اگر تاکنون با رایانه کار کرده باشید، احتمالاً در مورد طرز کار سیستم عامل در سطوح پایین کنجکاو شدهاید و یا حتی ممکن است خواسته باشید در مورد این که چگونه میتوان یک سیستم عامل توسعه داد سؤالاتی به ذهنتان رسیده باشد. این که گفته شود توسعه کرنل کار دشواری است، به هیچ وجه حق مطلب را ادا نمیکند. در واقع این کار اوج برنامهنویسی محسوب میشود. در این راهنما ابزارهای پایهای مورد نیاز برای این منظور را معرفی کردیم و یک سیستم عامل ساده را در C و x86 Assembly پیادهسازی خواهیم کرد.
این تصویری از صفحه سیستم عامل Basilica است که در این نوشته قصد داریم آن را ایجاد کنیم.
سیستمی که قصد داریم توسعه دهیم، به افتخار Terry Davis، توسعهدهنده تازه درگذشته TempleOS (+) به صورت Basilica OS نامگذاری شده است. این سیستم بسیار ساده خواهد بود و به عنوان یک مقدمه برای توسعه سیستم عامل محسوب میشود. از این رو قصد نداریم هیچ موضوع مرتبط با نظریه سیستم عامل مانند قالبهای اجرایی، ارتباط سریال و غیره را مطرح کنیم. ما پشتیبانی از کیبورد را در سیستم خود نخواهیم داشت؛ با این حال، یک سیستم عامل اولیه میسازیم. شروع به پیادهسازی یک کتابخانه استاندارد میکنیم. ما فرضی میکنیم شما از سیستم عامل Ubuntu 18.04 استفاده میکنید، اگر چه استفاده از WSL روی ویندوز 10 نیز ممکن است.
راهاندازی یک کامپایلر متقابل (Cross Compiler)
نخستین کاری که برای توسعه یک سیستم عامل نیاز داریم، راهاندازی یک کامپایلر متقابل است. منظور از کامپایلر متقابل، کامپایلری است که کد را برای سیستمی به جز سیستم میزبان خود کامپایل میکند. در این مورد ما میخواهیم کد را برای سیستم i686-elf کامپایل کنیم. شما لازم نیست موارد زیادی را در مورد ELF بدانید؛ اما اگر قصد دارید بدانید که فایلهای ELF چگونه کار میکنند، میتوانید این مقاله (+) را مطالعه کنید. اگر روی یک سیستم لینوکس مشغول توسعه هستید، احتمالاً هم اینک یک کامپایلر با قابلیت کامپایل برای یک ELF را به صورت 32 بیت روی سیستم خود دارید؛ اما ممکن است این کامپایلر کار نکند، چون فایلهای اجرایی که تولید میکند برای لینوکس است که با هدف ما ناسازگار هستند.
برای راه اندازی کامپایلر متقابل کار خود را از نصب وابستگیها آغاز میکنیم.
sudo apt-get install build-essential bison flex libgmp3-dev libmpc-dev libmpfr-dev texinfo libcloog-isl-dev libisl-dev qemu grub-common xorriso nasm grub-pc-bin |
اکنون برای این که فرایند نصب کمی آسانتر باشد، چند مقدار را تعریف میکنیم، یک دایرکتوری در مسیر src/~ برای نصب کامپایلر متقابل خود ایجاد میکنیم و فایلهای باینری جدیداً اسمبل شده را به مسیر سیستم اضافه میکنیم تا بتوانیم binutils را در زمان ساخته شدن تشخیص دهیم.
export PREFIX="$HOME/opt/cross" export TARGET=i686-elf export PATH="$PREFIX/bin:$PATH" mkdir~/src |
اکنون آخرین نسخه از binutils را در دایرکتوری جدید src/build-binutils/~ دانلود میکنیم. این کار از طریق GNU ftp mirror (+) یا از طریق wget میسر است. ما از روش wget استفاده میکنیم. مهم نیست که در نهایت کدام نسخه را دریافت میکنید؛ اما باید مطمئن شوید که بر اساس نسخه نصب شده، دستورات صحیحی را در خط فرمان وارد میکنید.
cd~/src wget https://ftp.gnu.org/gnu/binutils/binutils-2.31.1.tar.xz tar-xf binutils-2.31.1.tar.xz rm binutils-2.31.1.tar.xz mkdir build-binutils cd build-binutils/ ../binutils-2.31.1/configure--target=$TARGET--prefix="$PREFIX"\ --with-sysroot--disable-nls--disable-werror make make install |
زمانی که این کار پایان یافت، GCC را از GNU ftp mirror (+) و یا با استفاده از wget دانلود کنید. فرایند ساخت GCC ممکن است کمی طولانی باشد و باید صبور باشید.
cd~/src wget https://ftp.gnu.org/gnu/gcc/gcc-8.2.0/gcc-8.2.0.tar.xz tar-xf gcc-8.2.0.tar.xz rm gcc-8.2.0.tar.xz mkdir build-gcc cd build-gcc ../gcc-8.2.0/configure--target=$TARGET--prefix="$PREFIX"\ --disable-nls--enable-languages=c,c++--without-headers make all-gcc make all-target-libgcc make install-gcc make install-target-libgcc |
پس از این که این موارد نصب شدند، خط زیر را به فایل bashrc./~ اضافه کنید.
export PATH=$HOME/opt/cross/bin:$PATH |
اینک میتوانیم شروع به ساخت سیستم عامل خود بکنیم.
بوت کردن و لینک کردن
نخستین کاری که در این مرحله انجام میدهیم، نوشتن مقداری کد اسمبلی است که شیوه اجرای سیستم بعد از بوت شدن را مدیریت میکند. ایجاد یک برنامه که یک سیستم عامل را پس از بوت شدن بارگذاری کند (Bootloader) کاملاً دشوار است، بنابراین ما از GRUB Bootloader استفاده خواهیم کرد که سیستم عامل را بارگذاری کرده و سپس کنترل رایانه به برنامه ما میدهد.
کار خود را با ایجاد فایل boot.asm آغاز میکنیم که دستورهای زیر را در خود دارد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | MBALIGN equ 1 MEMINFO equ 2 FLAGS equ MBALIGN|MEMINFO MAGIC equ 0x1BADB002 CHECKSUMequ-(MAGIC+FLAGS) section.multiboot align4 ddMAGIC ddFLAGS ddCHECKSUM section.bss align16 stack_bottom: resb16384 stack_top: section.text global_start:function(_start.end-_start) _start: movesp,stack_top externkernel_main callkernel_main cli .hang:hlt jmp.hang .end: |
5 خط نخست به تعریف چند متغیر سراسری میپردازند که شامل Magic Values هستند. این متغیرها از سوی Bootloader جستجو میشوند، به طوری که کرنل ما به صورت سازگار با وضعیت چندبوتی (multiboot) شناسایی میشود.
خطوط 7 تا 11 هدر چند بوتی را که شامل Magic Value و چند فلگ است تعریف میکنند. همچنین یک checksum صورت میگیرد تا تأیید کنیم که واقعاً یک هدر چند بوتی است. عبارت section.multiboo تضمین میکند که این مقادیر در 8 کیلوبایت نخست فایل کرنل قرار میگیرند. align موجب میشود 4 فایل در محدوده 32 بیتی قرار گیرد.
خطوط 13 تا 16 یک پشته کوچک میسازد. align در ادامه 16 پشته را به صورت 16 بیتی تعریف میکند که استانداردی برای پشتهها در x86 محسوب میشود. stack_bottom: یک نماد برای انتهای پشته ایجاد میکند. resb 16384 مقدار 16 کیلوبایت فضا برای پشته ذخیره میکند و stack_top: یک نماد برای ابتدای پشته ایجاد میکند.
در خطوط 19 تا 24 به شیوه کارکرد برنامه در زمان فراخوانی شدن پرداختهایم. در اسکریپت لینک کننده که در ادامه خواهیم ساخت، یک متغیر start_ به عنوان نقطه ورود (entry point) سیستم تعریف میکنیم و از این رو bootloader پس از این که کرنل بارگذاری شد، به این نقطه پرش میکند. mov esp, stack_top با انتقال stack_top به ثبات اشارهگر پشته، پشتهای را که قبلاً تعریف کردیم راهاندازی میکند. پس از آن یک تابع خارجی به نام kernel_main را فراخوانی میکنیم که در ادامه آن را تعریف خواهیم کرد.
اکنون شروع به پیادهسازی کرنل میکنیم. ابتدا یک فایل به نام kernel.c بسازید و کد زیر را در آن بنویسید:
#include <stdbool.h> #include <stddef.h> #include <stdint.h> voidkernel_main(){ } |
این کد هنوز هیچ کاری انجام نمیدهد و صرفاً کمی بعدتر که اقدام به لینک کردن میکنیم، استفاده خواهد شد. دقت کنید که در این تابع void kernel_main، تابعی است که در boot.asm اعلان و فراخوانی میشود و از این رو نقطه مدخل کرنل ما محسوب میشود.
سپس یک فایل به نام linker.ld ایجاد میکنیم و کد زیر را به آن میافزاییم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | ENTRY(_start) SECTIONS { .=1M; .text BLOCK(4K):ALIGN(4K) { *(.multiboot) *(.text) } .rodata BLOCK(4K):ALIGN(4K) { *(.rodata) } .data BLOCK(4K):ALIGN(4K) { *(.data) } .bss BLOCK(4K):ALIGN(4K) { *(COMMON) *(.bss) } } |
(ENTRY(_start به تعریف start_ به عنوان نماد مدخل اقدام میکند که میتوان آن را کاملاً گویا دانست. خطی که به صورت ; 1M = . است، اعلام میکند که بخشهای مختلف باید در اندازههای 1 مگابایتی تنظیم شوند. سپس آن را در هدر multiboot. قرار میدهیم، به طوری که bootloader فرمت فایل را تشخیص دهد. در ادامه بخش text. قرار دارد. سپس دادههای فقط-خواندنی، دادههای خواندنی-نوشتنی و ناحیههایی برای پشته در آن قرار میدهیم و همچنین ناحیههایی برای بخشهای دیگر که ممکن است پشته بخواهد ایجاد کند، در نظر میگیریم.
اکنون که همه فایلهای مورد نیاز ایجاد شدهاند، میتوانیم یک ایمیج CDROM قابل بوت را کامپایل کرده و بسازیم. سیستم خود را میتوانید با دستورهای زیر کامپایل کرده و بسازید.
nasm-felf32 boot.asm-oboot.o i686-elf-gcc-ckernel.c-okernel.o-std=gnu99-ffreestanding\ -O2-Wall-Wextra i686-elf-gcc-Tlinker.ld-obasilica.bin-ffreestanding-O2\ -nostdlib boot.okernel.o–lgcc |
در این مرحله، میتوانیم با اجرای دستور زیر بررسی کنیم که آیا همه چیز را به درستی پیکربندی کردهایم یا نه.
grub-file--is-x86-multiboot basilica.bin echo$? |
اگر دستور فوق مقدار 0 بازگرداند، همه چیز درست بوده است. اگر مقدار 1 بازگرداند، باید اطمینان حاصل کنیم که همه مراحل به درستی طی شدهاند. اینک میتوانیم با ایجاد یک فایل پیکربندی به نام grub.cfg یک ایمیج CDROM ISO از فایلهای باینری خود بسازیم:
menuentry"basilica"{ multiboot/boot/basilica.bin } |
سپس یک ساختار پوشه ایجاد کرده و فایلهای مورد نیاز را در آن کپی کرده و یک ایمیج ISO ایجاد میکنیم:
mkdir-pisodir/boot/grub cp basilica.bin isodir/boot/basilica.bin cp grub.cfg isodir/boot/grub/grub.cfg grub-mkrescue-obasilica.iso isodir |
اگر فکر میکنید دستورهای فوق برای لینک کردن کامل، کامپایل و ساخت یک ایمیج از سیستم عامل زیاد هستند، میتوانیم پیشنهاد کنیم با ایجاد یک makefile امور فوق را کمی سادهتر بسازید. بدین منظور یک فایل به نام makefile با محتوای زیر ایجاد کنید:
CC=i686-elf-gcc main:kernel.clinker.ld boot.asm nasm-felf32 boot.asm-oboot.o $(CC)-ckernel.c-okernel.o-std=gnu99-ffreestanding-Wall-Wextra $(CC)-Tlinker.ld-obasilica.bin-ffreestanding-nostdlib boot.okernel.o-lgcc cp basilica.bin isodir/boot/basilica.bin grub-mkrescue-obasilica.iso isodir clean: rm./*.o./*.bin./*.iso./isodir/boot/*.bin |
اکنون میتوانید کل پروژه خود را با استفاده از دستور make به صورت یک ایمیج ISO کامپایل و بیلد کنید و همه فایلهای iso، bin. و o. را با وارد کردن دستور make clean پاک کنید. ما از این به بعد برای ایجاد سرعت بیشتر از این روش استفاده خواهیم کرد. اینک میتوانیم برنامه خود را با دستور qemu اجرا کنیم.
qemu-system-i386-cdrom basilica.iso |
بدین ترتیب منوی چند بوتی Grub نمایش مییابد و سپس بعد از پیشروی، با یک صفحه خالی سیاه مواجه میشویم.
Grub BootLoaderسیستم عامل ما در حال حاضر کار زیادی انجام نمیدهد!
واداشتن کرنل به انجام یک کار
اینک که همه چیز کار میکند، میتوانیم شروع به تعریف کردن برخی کارها برای کرنل بکنیم. نخستین کاری که میتوانیم برای آن تعریف کنیم نمایش چیزی روی صفحه است. این کار از طریق نوشتن اطلاعاتی روی حافظه ویدئویی برای نمایشهای رنگی که در آدرس 0xb8000 قرار دارد میسر خواهد بود. بدین ترتیب از حالت 80×25 استفاده خواهیم کرد. در این حالت برای هر کاراکتر که روی صفحه نمایش مییابد، حافظه حالت متنی دو بایت فضا اشغال میکند که یکی برای کد Ascii و دیگری برای «بایت خصوصیت» (Attribute Byte) است که شامل اطلاعات رنگ پیشزمینه و پسزمینه متن است. در بایت خصوصیت، رنگ پسزمینه در چهار بیت نخست و رنگ پسزمینه در چهار بیت آخر قرار دارند.
0000000000000000 BG FG ASCII |
با دانستن این موارد میتوانیم شروع به تعریف برخی ساختارها بکنیم و تابعهایی برای قرار دادن کاراکترها روی صفحه بنویسیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | staticconstsize_t VGA_WIDTH=80; staticconstsize_t VGA_HEIGHT=25; uint16_t*terminal_buffer; enumvga_colour{ VGA_COLOUR_BLACK, VGA_COLOUR_BLUE, VGA_COLOUR_GREEN, VGA_COLOUR_CYAN, VGA_COLOUR_RED, VGA_COLOUR_MAGENTA, VGA_COLOUR_BROWN, VGA_COLOUR_LIGHT_GREY, VGA_COLOUR_DARK_GREY, VGA_COLOUR_LIGHT_BLUE, VGA_COLOUR_LIGHT_GREEN, VGA_COLOUR_LIGHT_CYAN, VGA_COLOUR_LIGHT_RED, VGA_COLOUR_LIGHT_MAGENTA, VGA_COLOUR_LIGHT_BROWN, VGA_COLOUR_WHITE, }; staticinlineuint8_t vga_entry_colour(enumvga_colour foreground,enumvga_colour background){ returnforeground|(background<<4); } staticinlineuint16_t vga_entry(unsignedcharc,uint8_t colour){ return(uint16_t)c|((uint16_t)colour<<8); } voidterminal_putcharat(charc,uint16_t colour,size_tx,size_ty){ constsize_t index=y*VGA_WIDTH+x; terminal_buffer[index]=vga_entry(c,colour); } |
اکنون قصد داریم یک رنگ پیشفرض برای ترمینال خود تنظیم کنیم. به این منظور از مقدار VGA_COLOUR_WHITE برای پیشزمینه و از مقدار VGA_COLOUR_BLUE برای پسزمینه استفاده میکنیم. همچنین روشی برای حفظ رد سطر و ستونی که در آن قرار داریم، تعریف میکنیم تا بتوانیم هر بار چندین کاراکتر در یک خط قرار دهیم. پس از انجام این کار میتوانیم یک تابع ()terminal_initialize اضافه کنیم تا برخی مقادیر پیشفرض را اضافه کند و رنگ پسزمینه را به چیزی که کمتر کسلکننده است تغییر دهیم. همچنین چند کاراکتر در مرکز صفحه نمایش میدهیم تا ببینیم آیا این تابع کار میکند یا نه.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | size_t terminal_row; size_t terminal_column; uint8_t terminal_colour; voidterminal_initialize(){ terminal_row=0; terminal_column=0; terminal_colour=vga_entry_colour(VGA_COLOUR_WHITE,VGA_COLOUR_BLUE); terminal_buffer=(uint16_t*)0xB8000; for(size_ty=0;y<VGA_HEIGHT;y++){ for(size_tx=0;x<VGA_WIDTH;x++){ constsize_t index=y*VGA_WIDTH+x; terminal_buffer[index]=vga_entry(' ',terminal_colour); } } } voidkernel_main(){ terminal_initialize(); terminal_putcharat('A',terminal_colour,31,10); terminal_putcharat('B',terminal_colour,32,10); terminal_putcharat('C',terminal_colour,33,10); } |
اینک اگر سیستم عامل خود را بیلد و اجرا کنید چیزی مانند تصویر زیر را نمایش خواهد داد.
با این که این یک نتیجه عالی است؛ اما بهتر است به جای این که در مورد هر کاراکتر مجبور باشیم رنگ پیش و پسزمینه را تعیین کنیم، در هر مرحله صرفاً کاراکترها را وارد کنیم. با نوشتن چند تابع دیگر، عملیات حفظ رد کاراکترها و همچنین رنگ نوشته را به صورت خودکار انجام میدهیم تا بتوانیم رشتههای مختلف را در ترمینال نمایش دهیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | voidterminal_putchar(charc){ terminal_putcharat(c,terminal_colour,terminal_column,terminal_row); if(++terminal_column==VGA_WIDTH){ terminal_column=0; if(++terminal_row==VGA_HEIGHT) terminal_row=0; } } voidterminal_write(constchar*data,size_t size){ for(size_ti=0;i<size;i++) terminal_putchar(data[i]); } voidterminal_writestring(constchar*data){ terminal_write(data,strlen(data)); } voidkernel_main(){ terminal_initialize(); terminal_writestring("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"); } |
دقت کنید که در این حالت برنامه کامپایل نمیشود و خطایی به صورت ‘undefined reference to ‘strlen نشان میدهد. دلیل این امر آن است که ما به کتابخانه استاندارد یا هر کتابخانه واقعاً غیر کامپایلر دیگر برای این موضوع دسترسی نداریم. در این مرحله، باید یک فایل به نام stdlib.h ایجاد کنید و آن را در پروژه خود بگنجانید. پس از این کار میتوانید تابع را درون آن قرار دهید و سپس سیستم عامل را بیلد و اجرا کنید.
size_t strlen(constchar*str){ size_t len=0; while(str[len]) len++; returnlen; } |
اینک ما با خروجی زیر مواجه میشویم:
با این که تصویر فوق شبیه صفحه مرگ آبی ویندوز به نظر میرسد که موجب وحشت ما میشود؛ اما نتیجه مناسبی است، چون نشان میدهد که رفتن رشتهها به سر خط به درستی صورت گرفته است. احتمالاً متوجه آن کاراکتر با شکل عجیب در انتهای رشته شدهاید، دلیل این امر آن است که کرنل ما در حال حاضر روشی برای بررسی کاراکترهای Newline ندارد و از این رو آن را نیز مانند دیگر کاراکترها در صفحه نمایش میدهد.
کاراکترهای Newline را میتوان با بازنویسی ()terminal_putchar جهت تغییر برخورد سیستم با ‘n/’ به صورت VGA_WIDTH و سپس ادامه فرایند نوشتن از بافر نمایش، به سادگی مدیریت کرد. بدین منظور ()terminal_putchar را باید به صورت زیر بازسازی کنید:
void terminal_putchar(charc){ if(terminal_column==VGA_WIDTH||c=='\n'){ terminal_column=0; if(terminal_row==VGA_HEIGHT-1){ terminal_row=0; }else{ terminal_row++; } } if(c=='\n')return; terminal_putcharat(c,terminal_colour,terminal_column++,terminal_row); } |
نکته دیگری که ممکن است متوجه شوید، این است که وقتی متن به انتهای صفحه میرسد، دوباره به ابتدای صفحه بازمیگردد و از آنجا ادامه متن را نمایش میدهد. برای حل این وضعیت باید قابلیت اسکرول شدن ترمینال را پیادهسازی کنیم. به این منظور باید خط آخر ترمینال را پاک کنید و همه خطوط بالایی را یک خط به بالاتر انتقال دهیم. شاید در ابتدا وسوسه شوید که از دستور memmove استفاده کنید؛ اما به خاطر داشته باشید که ما هیچ کتابخانه استانداردی نداریم. با تعریف حلقهای روی همه کاراکترها و تعیین VGA_WIDTH در ابتدای کاراکترها، میتوانیم تابعی برای اسکرول کل صفحه به سمت بالا پیادهسازی کنیم. بدین ترتیب ()terminal_putchar و ()terminal_scroll_up را میتوانیم به صورت زیر بنویسیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | voidterminal_putchar(charc){ if(terminal_column==VGA_WIDTH||c=='\n'){ terminal_column=0; if(terminal_row==VGA_HEIGHT-1){ terminal_scroll_up(); }else{ terminal_row++; } } if(c=='\n')return; terminal_putcharat(c,terminal_colour,terminal_column++,terminal_row); } voidterminal_scroll_up(){ inti; for(i=0;i<(VGA_WIDTH*VGA_HEIGHT-80);i++) terminal_buffer[i]=terminal_buffer[i+80]; for(i=0;i<VGA_WIDTH;i++) terminal_buffer[(VGA_HEIGHT-1)*VGA_WIDTH+i]=vga_entry(' ',terminal_colour); } |
اکنون متن هنگام رسیدن به انتهای صفحه میتواند به سمت بالا اسکرول کند. با این وجود، صفحه ما کاملاً تاریک به نظر میرسد. دلیل آن این است که صفحه کاملاً استاتیک است. این وضعیت را با افزودن ()delay به کتابخانه استاندارد خودمان حب میکنیم. از آنجا که ما هنوز به هیچ نوع تایمر CPU دسترسی نداریم، سادهترین (و در عین حال دم دستترین) روش برای پیادهسازی این کارکرد ورود به یک حلقه for برای یک مجموعه از تکرارها است. نوشتن این کار نسبتاً ساده است.
voiddelay(intt){ volatile inti,j; for(i=0;i<t;i++) for(j=0;j<250000;j++) __asm__("NOP"); } |
کلیدواژه volatile و همچنین استفاده از ;(“asm__(“NOP __ به منظور جلوگیری از بهینهسازیها از سوی کامپایلر صورت میگیرد تا بدین ترتیب از اجرای حلقه جلوگیری نکنند. ما میتوانیم هر دو تابع ()delay و ()and terminal_scroll_up را با اجرای دستور زیر تست کنیم:
for(;;){ delay(100); terminal_writestring("test "); } |
همچنین در ادامه چند تابع نمایش دیگر اضافه میکنیم تا کارهای خود را سادهتر کنیم. اولین کار این است که تابع ()terminal_writestring_colour را اضافه میکنیم تا رشتههای کاملی را در یک رنگ خاص نمایش دهیم. همچنین یک تابع ()terminal_writeint برای نمایش اعداد صحیح اضافه میکنیم. پیادهسازی هر دو این تابعها کار آسانی است.
voidterminal_writestring_colour(constchar*data,enumvga_colour fg,enumvga_colour bg){ uint8_t oldcolour=terminal_colour; terminal_colour=vga_entry_color(fg,bg); terminal_writestring(data); terminal_colour=oldcolour; } voidterminal_writeint(unsignedlongn){ if(n/10) terminal_writeint(n/10); terminal_putchar((n%10)+'0'); } |
گسترش کتابخانه استاندارد و تعامل با CPU
ما تا به اینجا پیشرفت نسبتاً خوبی در سیستم عامل خود داشتهایم؛ اما نکتهای که شاید متوجه شده باشید این است که صفحه سیستم عامل ما هر بار که سیستم را روشن میکنیم، به روش مشابهی عمل میکند. دلیل این مسئله آن است که ما هم اینک هیچ روشی برای تعامل با اجزای داخلی یا خارجی سیستم و یا عوامل تصادفی نداریم. به همین دلیل یک تابع مفید برای افزودن به کتابخانه استاندارد خودمان تابعی به نام ()rand است که متغیرهای تصادفی تولید میکند. در اغلب استانداردهای زبان C تابع ()rand به طور معمول از یک «تولیدکننده همسان خطی» (linear congruential generator) یا به اختصار LGG استفاده میکند که نوشتن آن کاملاً ساده است. یک پیادهسازی ساده آن به صورت زیر است:
#define RAND_MAX 32767 unsignedlongnext=1; intrand(){ next=next*1103515245+12345; return(unsignedint)(next/65536)%RAND_MAX+1; } voidsrand(unsignedintseed){ next=seed; } |
این تابع کار میکند؛ اما خیلی زود متوجه میشوید که این تابع صرفاً مشکل ما را کمی جا به جا میکند، چون ما هیچ روشی برای ارائه بذرهای نیمه تصادفی به تابع ()srand نداریم و از این رو تابع ()rand هر بار که سیستم بوت میشود، همان مقادیر را با همان ترتیبها ایجاد میکند. برای اصلاح این وضعیت باید یک منبع آنتروپی ایجاد کنیم. زمانی که سیستم عامل ما در ادامه پیشرفتهتر شد، میتوانیم از حرکت ماوس یا تعاملهای کاربر به عنوان یک منبع آنتروپی استفاده کنیم؛ اما فعلاً مسیر سادهتری را به صورت استفاده از شمارنده Time-Stamp در CPU برای تعیین بذر مقادیر تصادفی خود استفاده میکنیم.
هیچ روش توکاری برای خواندن شمارنده Time-Stamp در زبان C وجود ندارد و از این رو باید از اسمبلی استفاده کنیم. این کار از طریق استفاده از اسمبلی درون خطی ممکن است؛ اما روش آسانتر تغییر دادن boot.asm موجود برای افزودن این کارکرد است.
برای انجام این کار از یک RDTSC به صورت x86 Mnemonic استفاده میکنیم که یک مقدار 64 بیتی را به EDX:EAX میخواند. یعنی بیتهای مرتبه بالا (high-order) در EDX و بیتهای مرتبه پایین (low-order) در EAX بارگذاری میشوند. از آنجا که ثبات تجمیع کننده (accumulator register) برای EAX تنها قادر به ذخیرهسازی مقادیر 32 بیتی است، باید دو تابع برای بازگشت دادن مقادیر ذخیره شده در هر دو ثبات EDX و EAX بنویسیم.
global_timestamp_edx global_timestamp_eax _timestamp_edx: rdtsc mov eax,edx ret _timestamp_eax: rdtsc ret |
ابتدا timestamp_edx_ و timestamp_eax_ را به عنوان متغیرهای سراسری اعلان میکنیم تا بتوانیم آن را از kernel.c فراخوانی کنیم. تابع اول مقدار EDX:EAX را برابر با مقدار شمارنده time stamp تعیین میکند و مقدار ذخیره شده در EDX را به EAX انتقال داده و اجرا را به kernel.c بازمیگرداند. تابع دوم درست پس از فراخوانی RDTSC بازگشت میدهد، چون مقدار EAX اینک آن چیزی است ما میخواهیم.
ما اکنون میتوانیم هر تابع را با گنجاندن اعلانهای تابع زیر در برنامه kernel.c خود فراخوانی کنیم:
extern uint32_t _timestamp_edx(); extern uint32_t _timestamp_eax(); |
اکنون به عنوان تست نهایی تلاش میکنیم که صفحه ساده ایجاد کرده و مقادیر تصادفی تولید شده که با استفاده از ()timestamp_eax_ بذرافشانی شدهاند را در آن نمایش دهیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | voidkernel_main(){ terminal_initialize(); terminal_writestring("\n\n\n\n "); terminal_writestring_colour("Welcome to Basilica OS\n",VGA_COLOUR_WHITE,VGA_COLOUR_LIGHT_BLUE); terminal_writestring("\n\n\n\n\n\n\n\n\n "); terminal_writestring_colour("Rand():",VGA_COLOUR_BLACK,VGA_COLOUR_WHITE); terminal_putchar(' '); delay(200); srand(_timestamp_eax()); for(;;){ terminal_writeint(rand()); terminal_writestring(" "); terminal_column=39; terminal_row=14; delay(100); } } |
اینک ما موفق شدهایم که کرنل مقدماتی را توسعه داده و پیادهسازی کنیم و افزودن امکانات دیگر صرفاً به زمان بیشتری نیاز دارد. شما با استفاده ابزارهای معرفی شده در این راهنما میتوانید یک سیستم با امکانات کامل توسعه دهید. کد منبع کامل این سیستم عامل را میتوانید در این آدرس (+) ملاحظه کنید.
سخن پایانی
ابزار عمده دیگری که باید پیادهسازی کنیم، پشتیبانی از کیبورد است. مستندات 650 صفحهای کیبوردهای USB باعث میشود که از پیادهسازی آن صرفنظر کنیم و به جای آن سعی کنیم کیبوردهای PS/2 را که پیادهسازی بسیار سادهتری دارند را بررسی کنیم. برای کسب اطلاعات بیشتر در این مورد میتوانید به این آدرس (+) مراجعه کنید.
اگر قصد دارید جلوتر بروید و سیستم عامل پیشرفتهتری بسازید، بهتر است کمی دانش خود را در مورد نظریه سیستم عامل افزایش دهید. به این منظور توصیه میکنیم از «سری مقالههای آموزش مباحث سیستم عامل» استفاده کنید.
دقت کنید که سیستم عامل به طور معمول به صورت «کرنل + ابزارها + اپلیکیشنها» تعریف میشود. بر اساس این تعریف سیستمی که ما در این مقاله پیادهسازی کردهایم در واقع به مفهوم کرنل نزدیکتر از سیستم عامل است. اما شما میتوانید با تصور کردن ()strlen به صورت یک ابزار و همچنین با تصور ()rand به عنوان یک اپلیکیشن آن را یک سیستم عامل نیز بنامید!
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای دروس مهندسی کامپیوتر
- آموزش سیستم های عامل
- مجموعه آموزشهای علوم کامپیوتر
- آموزش آشنایی با سیستم عامل میکروتیک MikroTik
- آموزش مباحث سیستم عامل — مجموعه مقالات جامع وبلاگ فرادرس
- آموزش سیستم عامل لینوکس (Linux) – مقدماتی
==
«میثم لطفی» دانشآموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری علاقهمندیهایش در رشتههای برنامهنویسی، کپیرایتینگ و محتوای چندرسانهای، در زمینه نگارش مقالاتی با محوریت نرمافزار نیز با مجله فرادرس همکاری دارد.
بر اساس رای 24 نفر
آیا این مطلب برای شما مفید بود؟