چگونه یک سیستم عامل توسعه دهیم؟ — به زبان ساده

۲۸۳۳ بازدید
آخرین به‌روزرسانی: ۲۷ شهریور ۱۴۰۲
زمان مطالعه: ۱۳ دقیقه
چگونه یک سیستم عامل توسعه دهیم؟ — به زبان ساده

اگر تاکنون با رایانه کار کرده باشید، احتمالاً در مورد طرز کار سیستم عامل در سطوح پایین کنجکاو شده‌اید و یا حتی ممکن است خواسته باشید در مورد این که چگونه می‌توان یک سیستم عامل توسعه داد سؤالاتی به ذهنتان رسیده باشد. این که گفته شود توسعه کرنل کار دشواری است، به هیچ وجه حق مطلب را ادا نمی‌کند. در واقع این کار اوج برنامه‌نویسی محسوب می‌شود. در این راهنما ابزارهای پایه‌ای مورد نیاز برای این منظور را معرفی کردیم و یک سیستم عامل ساده را در 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 آغاز می‌کنیم که دستورهای زیر را در خود دارد:

1MBALIGN  equ  1
2MEMINFO  equ  2 
3FLAGS    equ  MBALIGN | MEMINFO
4MAGIC    equ  0x1BADB002   
5CHECKSUM equ -(MAGIC + FLAGS)
6
7section .multiboot
8align 4
9	dd MAGIC
10	dd FLAGS
11	dd CHECKSUM
12section .bss
13align 16
14stack_bottom:
15resb 16384
16stack_top:
17
18section .text
19global _start:function (_start.end - _start)
20_start:
21	mov esp, stack_top
22	extern kernel_main
23	call kernel_main
24	cli
25.hang:	hlt
26	jmp .hang
27.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 بسازید و کد زیر را در آن بنویسید:

1#include <stdbool.h>
2#include <stddef.h>
3#include <stdint.h>
4
5void kernel_main(){
6
7}

این کد هنوز هیچ کاری انجام نمی‌دهد و صرفاً کمی بعدتر که اقدام به لینک کردن می‌کنیم، استفاده خواهد شد. دقت کنید که در این تابع void kernel_main، تابعی است که در boot.asm اعلان و فراخوانی می‌شود و از این رو نقطه مدخل کرنل ما محسوب می‌شود.

سپس یک فایل به نام linker.ld ایجاد می‌کنیم و کد زیر را به آن می‌افزاییم.

1ENTRY(_start)
2SECTIONS
3{
4	. = 1M;
5	.text BLOCK(4K) : ALIGN(4K)
6	{
7		*(.multiboot)
8		*(.text)
9	}
10 	.rodata BLOCK(4K) : ALIGN(4K)
11	{
12		*(.rodata)
13	}
14 	.data BLOCK(4K) : ALIGN(4K)
15	{
16		*(.data)
17	}
18 	.bss BLOCK(4K) : ALIGN(4K)
19	{
20		*(COMMON)
21		*(.bss)
22	}
23}

(ENTRY(_start به تعریف start_ به عنوان نماد مدخل اقدام می‌کند که می‌توان آن را کاملاً گویا دانست. خطی که به صورت ; 1M = . است، اعلام می‌کند که بخش‌های مختلف باید در اندازه‌های 1 مگابایتی تنظیم شوند. سپس آن را در هدر multiboot. قرار می‌دهیم، به طوری که bootloader فرمت فایل را تشخیص دهد. در ادامه بخش text. قرار دارد. سپس داده‌های فقط-خواندنی، داده‌های خواندنی-نوشتنی و ناحیه‌هایی برای پشته در آن قرار می‌دهیم و همچنین ناحیه‌هایی برای بخش‌های دیگر که ممکن است پشته بخواهد ایجاد کند، در نظر می‌گیریم.

اکنون که همه فایل‌های مورد نیاز ایجاد شده‌اند، می‌توانیم یک ایمیج CDROM قابل بوت را کامپایل کرده و بسازیم. سیستم خود را می‌توانید با دستورهای زیر کامپایل کرده و بسازید.

nasm -felf32 boot.asm -o boot.o
i686-elf-gcc -c kernel.c -o kernel.o -std=gnu99 -ffreestanding \
-O2 -Wall -Wextra
i686-elf-gcc -T linker.ld -o basilica.bin -ffreestanding -O2 \
-nostdlib boot.o kernel.o –lgcc

در این مرحله، می‌توانیم با اجرای دستور زیر بررسی کنیم که آیا همه چیز را به درستی پیکربندی کرده‌ایم یا نه.

grub-file --is-x86-multiboot basilica.bin
echo $?

اگر دستور فوق مقدار 0 بازگرداند، همه چیز درست بوده است. اگر مقدار 1 بازگرداند، باید اطمینان حاصل کنیم که همه مراحل به درستی طی شده‌اند. اینک می‌توانیم با ایجاد یک فایل پیکربندی به نام grub.cfg یک ایمیج CDROM ISO از فایل‌های باینری خود بسازیم:

menuentry "basilica" {
multiboot /boot/basilica.bin
}

سپس یک ساختار پوشه ایجاد کرده و فایل‌های مورد نیاز را در آن کپی کرده و یک ایمیج ISO ایجاد می‌کنیم:

mkdir -p isodir/boot/grub
cp basilica.bin isodir/boot/basilica.bin
cp grub.cfg isodir/boot/grub/grub.cfg
grub-mkrescue -o basilica.iso isodir

اگر فکر می‌کنید دستورهای فوق برای لینک کردن کامل، کامپایل و ساخت یک ایمیج از سیستم عامل زیاد هستند، می‌توانیم پیشنهاد کنیم با ایجاد یک makefile امور فوق را کمی ساده‌تر بسازید. بدین منظور یک فایل به نام makefile با محتوای زیر ایجاد کنید:

1CC = i686-elf-gcc
2
3main: kernel.c linker.ld boot.asm
4	nasm -felf32 boot.asm -o boot.o
5	$(CC) -c kernel.c -o kernel.o -std=gnu99 -ffreestanding -Wall -Wextra
6	$(CC) -T linker.ld -o basilica.bin -ffreestanding -nostdlib boot.o kernel.o -lgcc
7	cp basilica.bin isodir/boot/basilica.bin
8	grub-mkrescue -o basilica.iso isodir
9
10clean:
11rm ./*.o ./*.bin ./*.iso ./isodir/boot/*.bin

اکنون می‌توانید کل پروژه خود را با استفاده از دستور make به صورت یک ایمیج ISO کامپایل و بیلد کنید و همه فایل‌های iso، bin. و o. را با وارد کردن دستور make clean پاک کنید. ما از این به بعد برای ایجاد سرعت بیشتر از این روش استفاده خواهیم کرد. اینک می‌توانیم برنامه خود را با دستور qemu اجرا کنیم.

qemu-system-i386 -cdrom basilica.iso

بدین ترتیب منوی چند بوتی Grub نمایش می‌یابد و سپس بعد از پیشروی، با یک صفحه خالی سیاه مواجه می‌شویم.

Grub BootLoader
Grub BootLoader
سیستم عامل
سیستم عامل ما در حال حاضر کار زیادی انجام نمی‌دهد!

واداشتن کرنل به انجام یک کار

اینک که همه چیز کار می‌کند، می‌توانیم شروع به تعریف کردن برخی کارها برای کرنل بکنیم.

خستین کاری که می‌توانیم برای آن تعریف کنیم نمایش چیزی روی صفحه است. این کار از طریق نوشتن اطلاعاتی روی حافظه ویدئویی برای نمایش‌های رنگی که در آدرس 0xb8000 قرار دارد میسر خواهد بود. بدین ترتیب از حالت 80x25 استفاده خواهیم کرد. در این حالت برای هر کاراکتر که روی صفحه نمایش می‌یابد، حافظه حالت متنی دو بایت فضا اشغال می‌کند که یکی برای کد Ascii و دیگری برای «بایت خصوصیت» (Attribute Byte) است که شامل اطلاعات رنگ پیش‌زمینه و پس‌زمینه متن است. در بایت خصوصیت، رنگ پس‌زمینه در چهار بیت نخست و رنگ پس‌زمینه در چهار بیت آخر قرار دارند.

0000 0000 00000000
BG FG ASCII

با دانستن این موارد می‌توانیم شروع به تعریف برخی ساختارها بکنیم و تابع‌هایی برای قرار دادن کاراکترها روی صفحه بنویسیم.

1static const size_t VGA_WIDTH = 80;
2static const size_t VGA_HEIGHT = 25;
3
4uint16_t* terminal_buffer;
5
6enum vga_colour {
7    VGA_COLOUR_BLACK,
8    VGA_COLOUR_BLUE,
9    VGA_COLOUR_GREEN,
10    VGA_COLOUR_CYAN,
11    VGA_COLOUR_RED,
12    VGA_COLOUR_MAGENTA,
13    VGA_COLOUR_BROWN,
14    VGA_COLOUR_LIGHT_GREY,
15    VGA_COLOUR_DARK_GREY,
16    VGA_COLOUR_LIGHT_BLUE,
17    VGA_COLOUR_LIGHT_GREEN,
18    VGA_COLOUR_LIGHT_CYAN,
19    VGA_COLOUR_LIGHT_RED,
20    VGA_COLOUR_LIGHT_MAGENTA,
21    VGA_COLOUR_LIGHT_BROWN,
22    VGA_COLOUR_WHITE,
23};
24
25static inline uint8_t vga_entry_colour(enum vga_colour foreground, enum vga_colour background){
26    return foreground | (background << 4);
27}
28
29static inline uint16_t vga_entry(unsigned char c, uint8_t colour){
30    return (uint16_t) c | ((uint16_t) colour << 8);
31}
32
33void terminal_putcharat(char c, uint16_t colour, size_t x, size_t y){
34    const size_t index = y * VGA_WIDTH + x;
35    terminal_buffer[index] = vga_entry(c, colour);
36}

اکنون قصد داریم یک رنگ پیش‌فرض برای ترمینال خود تنظیم کنیم. به این منظور از مقدار VGA_COLOUR_WHITE برای پیش‌زمینه و از مقدار VGA_COLOUR_BLUE برای پس‌زمینه استفاده می‌کنیم. همچنین روشی برای حفظ رد سطر و ستونی که در آن قرار داریم، تعریف می‌کنیم تا بتوانیم هر بار چندین کاراکتر در یک خط قرار دهیم. پس از انجام این کار می‌توانیم یک تابع ()terminal_initialize اضافه کنیم تا برخی مقادیر پیش‌فرض را اضافه کند و رنگ پس‌زمینه را به چیزی که کمتر کسل‌کننده است تغییر دهیم. همچنین چند کاراکتر در مرکز صفحه نمایش می‌دهیم تا ببینیم آیا این تابع کار می‌کند یا نه.

1size_t terminal_row;
2size_t terminal_column;
3uint8_t terminal_colour;
4
5void terminal_initialize(){
6    terminal_row = 0;
7    terminal_column = 0;
8    terminal_colour = vga_entry_colour(VGA_COLOUR_WHITE, VGA_COLOUR_BLUE);
9    terminal_buffer = (uint16_t *) 0xB8000;
10    for(size_t y = 0; y < VGA_HEIGHT; y++){
11        for(size_t x = 0; x < VGA_WIDTH; x++){
12            const size_t index = y * VGA_WIDTH + x;
13            terminal_buffer[index] = vga_entry(' ', terminal_colour);
14        }
15    }
16}
17
18void kernel_main() {
19    terminal_initialize();
20    terminal_putcharat('A', terminal_colour, 31, 10);
21    terminal_putcharat('B', terminal_colour, 32, 10);
22    terminal_putcharat('C', terminal_colour, 33, 10);
23}

اینک اگر سیستم عامل خود را بیلد و اجرا کنید چیزی مانند تصویر زیر را نمایش خواهد داد.

سیستم عامل

با این که این یک نتیجه عالی است؛ اما بهتر است به جای این که در مورد هر کاراکتر مجبور باشیم رنگ پیش و پس‌زمینه را تعیین کنیم، در هر مرحله صرفاً کاراکترها را وارد کنیم. با نوشتن چند تابع دیگر، عملیات حفظ رد کاراکترها و همچنین رنگ نوشته را به صورت خودکار انجام می‌دهیم تا بتوانیم رشته‌های مختلف را در ترمینال نمایش دهیم.

1void terminal_putchar(char c){
2    terminal_putcharat(c, terminal_colour, terminal_column, terminal_row);
3    if (++terminal_column == VGA_WIDTH) {
4        terminal_column = 0;
5        if (++terminal_row == VGA_HEIGHT)
6            terminal_row = 0;
7    }
8}
9 
10void terminal_write(const char* data, size_t size){
11    for (size_t i = 0; i < size; i++)
12        terminal_putchar(data[i]);
13}
14void terminal_writestring(const char* data){
15    terminal_write(data, strlen(data));
16}
17
18void kernel_main() {
19    terminal_initialize();
20    terminal_writestring("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n");
21}

دقت کنید که در این حالت برنامه کامپایل نمی‌شود و خطایی به صورت 'undefined reference to 'strlen نشان می‌دهد. دلیل این امر آن است که ما به کتابخانه استاندارد یا هر کتابخانه واقعاً غیر کامپایلر دیگر برای این موضوع دسترسی نداریم. در این مرحله، باید یک فایل به نام stdlib.h ایجاد کنید و آن را در پروژه خود بگنجانید. پس از این کار می‌توانید تابع را درون آن قرار دهید و سپس سیستم عامل را بیلد و اجرا کنید.

1size_t strlen(const char* str){
2    size_t len = 0;
3    while (str[len])
4        len++;
5    return len;
6}

اینک ما با خروجی زیر مواجه می‌شویم:

سیستم عامل

با این که تصویر فوق شبیه صفحه مرگ آبی ویندوز به نظر می‌رسد که موجب وحشت ما می‌شود؛ اما نتیجه مناسبی است، چون نشان می‌دهد که رفتن رشته‌ها به سر خط به درستی صورت گرفته است. احتمالاً متوجه آن کاراکتر با شکل عجیب در انتهای رشته شده‌اید، دلیل این امر آن است که کرنل ما در حال حاضر روشی برای بررسی کاراکترهای Newline ندارد و از این رو آن را نیز مانند دیگر کاراکترها در صفحه نمایش می‌دهد.

کاراکترهای Newline را می‌توان با بازنویسی ()terminal_putchar جهت تغییر برخورد سیستم با 'n/' به صورت VGA_WIDTH و سپس ادامه فرایند نوشتن از بافر نمایش، به سادگی مدیریت کرد. بدین منظور ()terminal_putchar را باید به صورت زیر بازسازی کنید:

1void terminal_putchar(char c){
2    if(terminal_column == VGA_WIDTH || c == '\n'){
3        terminal_column = 0;
4        if(terminal_row == VGA_HEIGHT-1){
5            terminal_row = 0;
6        } else {
7            terminal_row++;
8        }
9    }
10    if(c == '\n') return;
11    terminal_putcharat(c, terminal_colour, terminal_column++, terminal_row);
12}

نکته دیگری که ممکن است متوجه شوید، این است که وقتی متن به انتهای صفحه می‌رسد، دوباره به ابتدای صفحه بازمی‌گردد و از آنجا ادامه متن را نمایش می‌دهد. برای حل این وضعیت باید قابلیت اسکرول شدن ترمینال را پیاده‌سازی کنیم. به این منظور باید خط آخر ترمینال را پاک کنید و همه خطوط بالایی را یک خط به بالاتر انتقال دهیم. شاید در ابتدا وسوسه شوید که از دستور memmove استفاده کنید؛ اما به خاطر داشته باشید که ما هیچ کتابخانه استانداردی نداریم. با تعریف حلقه‌ای روی همه کاراکترها و تعیین VGA_WIDTH در ابتدای کاراکترها، می‌توانیم تابعی برای اسکرول کل صفحه به سمت بالا پیاده‌سازی کنیم. بدین ترتیب ()terminal_putchar و ()terminal_scroll_up را می‌توانیم به صورت زیر بنویسیم:

1void terminal_putchar(char c){
2    if(terminal_column == VGA_WIDTH || c == '\n'){
3        terminal_column = 0;
4        if(terminal_row == VGA_HEIGHT-1){
5            terminal_scroll_up();
6        } else {
7            terminal_row++;
8        }
9    }
10    if(c == '\n') return;
11    terminal_putcharat(c, terminal_colour, terminal_column++, terminal_row);
12}
13
14void terminal_scroll_up(){
15    int i;
16    for(i = 0; i < (VGA_WIDTH*VGA_HEIGHT-80); i++)
17        terminal_buffer[i] = terminal_buffer[i+80];
18    for(i = 0; i < VGA_WIDTH; i++)
19        terminal_buffer[(VGA_HEIGHT - 1) * VGA_WIDTH + i] = vga_entry(' ', terminal_colour);
20}

اکنون متن هنگام رسیدن به انتهای صفحه می‌تواند به سمت بالا اسکرول کند. با این وجود، صفحه ما کاملاً تاریک به نظر می‌رسد. دلیل آن این است که صفحه کاملاً استاتیک است. این وضعیت را با افزودن ()delay به کتابخانه استاندارد خودمان حب می‌کنیم. از آنجا که ما هنوز به هیچ نوع تایمر CPU دسترسی نداریم، ساده‌ترین (و در عین حال دم دست‌ترین) روش برای پیاده‌سازی این کارکرد ورود به یک حلقه for برای یک مجموعه از تکرارها است. نوشتن این کار نسبتاً ساده است.

1void delay(int t){   
2    volatile int i,j;
3    for(i=0;i<t;i++)
4        for(j=0;j<250000;j++)
5            __asm__("NOP");
6}

کلیدواژه volatile و همچنین استفاده از  ;("asm__("NOP __ به منظور جلوگیری از بهینه‌سازی‌ها از سوی کامپایلر صورت می‌گیرد تا بدین ترتیب از اجرای حلقه جلوگیری نکنند. ما می‌توانیم هر دو تابع ()delay و ()and terminal_scroll_up را با اجرای دستور زیر تست کنیم:

for(;;){
   delay(100);
   terminal_writestring("test ");
}

سیستم عامل

همچنین در ادامه چند تابع نمایش دیگر اضافه می‌کنیم تا کارهای خود را ساده‌تر کنیم. اولین کار این است که تابع ()terminal_writestring_colour را اضافه می‌کنیم تا رشته‌های کاملی را در یک رنگ خاص نمایش دهیم. همچنین یک تابع ()terminal_writeint برای نمایش اعداد صحیح اضافه می‌کنیم. پیاده‌سازی هر دو این تابع‌ها کار آسانی است.

1void terminal_writestring_colour(const char* data, enum vga_colour fg, enum vga_colour bg){
2    uint8_t oldcolour = terminal_colour;
3    terminal_colour = vga_entry_color(fg, bg);
4    terminal_writestring(data);
5    terminal_colour = oldcolour;
6}
7
8void terminal_writeint(unsigned long n){
9    if(n/10)
10        terminal_writeint(n/10);
11    terminal_putchar((n % 10) + '0');
12}

گسترش کتابخانه استاندارد و تعامل با CPU

ما تا به اینجا پیشرفت نسبتاً خوبی در سیستم عامل خود داشته‌ایم؛ اما نکته‌ای که شاید متوجه شده باشید این است که صفحه سیستم عامل ما هر بار که سیستم را روشن می‌کنیم، به روش مشابهی عمل می‌کند. دلیل این مسئله آن است که ما هم اینک هیچ روشی برای تعامل با اجزای داخلی یا خارجی سیستم و یا عوامل تصادفی نداریم. به همین دلیل یک تابع مفید برای افزودن به کتابخانه استاندارد خودمان تابعی به نام ()rand است که متغیرهای تصادفی تولید می‌کند. در اغلب استانداردهای زبان C تابع ()rand به طور معمول از یک «تولیدکننده همسان خطی» (linear congruential generator) یا به اختصار LGG استفاده می‌کند که نوشتن آن کاملاً ساده است.

یک پیاده‌سازی ساده آن به صورت زیر است:

1#define RAND_MAX 32767
2unsigned long next = 1;
3
4int rand(){
5    next = next * 1103515245 + 12345;
6    return (unsigned int)(next/65536) % RAND_MAX+1; 
7}
8
9void srand(unsigned int seed){
10    next = seed;
11}

این تابع کار می‌کند؛ اما خیلی زود متوجه می‌شوید که این تابع صرفاً مشکل ما را کمی جا به جا می‌کند، چون ما هیچ روشی برای ارائه بذرهای نیمه تصادفی به تابع ()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 بنویسیم.

1global _timestamp_edx
2global _timestamp_eax
3
4_timestamp_edx:
5    rdtsc
6    mov eax, edx
7    ret
8
9_timestamp_eax:
10    rdtsc
11    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_ بذرافشانی شده‌اند را در آن نمایش دهیم.

1void kernel_main() {
2    terminal_initialize();
3    terminal_writestring("\n\n\n\n                            ");
4    terminal_writestring_colour("Welcome to Basilica OS\n", VGA_COLOUR_WHITE, VGA_COLOUR_LIGHT_BLUE);
5    terminal_writestring("\n\n\n\n\n\n\n\n\n                               ");
6    terminal_writestring_colour("Rand():", VGA_COLOUR_BLACK, VGA_COLOUR_WHITE);
7    terminal_putchar(' ');
8
9    delay(200);
10    srand(_timestamp_eax());
11    for(;;){
12        terminal_writeint(rand());
13        terminal_writestring("        ");
14        terminal_column = 39;
15        terminal_row = 14;
16        delay(100);
17    }
18}

سیستم عامل

اینک ما موفق شده‌ایم که کرنل مقدماتی را توسعه داده و پیاده‌سازی کنیم و افزودن امکانات دیگر صرفاً به زمان بیشتری نیاز دارد. شما با استفاده ابزارهای معرفی شده در این راهنما می‌توانید یک سیستم با امکانات کامل توسعه دهید. کد منبع کامل این سیستم عامل را می‌توانید در این آدرس (+) ملاحظه کنید.

سخن پایانی

ابزار عمده دیگری که باید پیاده‌سازی کنیم، پشتیبانی از کیبورد است. مستندات 650 صفحه‌ای کیبوردهای USB باعث می‌شود که از پیاده‌سازی آن صرف‌نظر کنیم و به جای آن سعی کنیم کیبوردهای PS/2 را که پیاده‌سازی بسیار ساده‌تری دارند را بررسی کنیم. برای کسب اطلاعات بیشتر در این مورد می‌توانید به این آدرس (+) مراجعه کنید.

اگر قصد دارید جلوتر بروید و سیستم عامل پیشرفته‌تری بسازید، بهتر است کمی دانش خود را در مورد نظریه سیستم عامل افزایش دهید. به این منظور توصیه می‌کنیم از «سری مقاله‌های آموزش مباحث سیستم عامل» استفاده کنید.

دقت کنید که سیستم عامل به طور معمول به صورت «کرنل + ابزارها + اپلیکیشن‌ها» تعریف می‌شود. بر اساس این تعریف سیستمی که ما در این مقاله پیاده‌سازی کرده‌ایم در واقع به مفهوم کرنل نزدیک‌تر از سیستم عامل است. اما شما می‌توانید با تصور کردن ()strlen به صورت یک ابزار و همچنین با تصور ()rand به عنوان یک اپلیکیشن آن را یک سیستم عامل نیز بنامید!

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۳۳ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
lduck11007
۲۴ دیدگاه برای «چگونه یک سیستم عامل توسعه دهیم؟ — به زبان ساده»

استاد در اینده با تلاش خییییییلی زیاد با هسته متفاوت می توان سیستم عاملی بهتر از macOS و Windows ساخت

سلام سیستم عامل ios با چه زبانی نوشته شده

و میشه روی گوشی های ایفون قدیمی مثل iphone 4 ی سیستم عامل نوشت؟

و اینکه چه زبان هایی مخصوص ساخت سیستم عامل هستن؟

با سلام و احترام؛

صمیمانه از همراهی شما با مجله فرادرس و ارائه بازخورد سپاس‌گزاریم.

سیستم عامل iOS با زبان‌های C++‎ ،C و Objective C نوشته شده است.

امکان نصب سیستم عاملی به غیر از iOS روی گوشی‌های iPhone وجود ندارد.

زبان C رایج‌ترین و پراستفاده‌ترین زبان برنامه نویسی برای توسعه و ساخت سیستم عامل به حساب می‌آید و همچنین از زبان‌های جاوا و پایتون هم می‌توان برای این منظور استفاده کرد.

برای یادگیری زبان C می‌توانید از دوره آموزشی زیر استفاده کنید:

  • آموزش برنامه نویسی C
  • برای شما آرزوی سلامتی و موفقیت داریم.
    ‌‌

    اگه دنبال خلاقیت و ساخت ی سیستم عامل شخصی یا ی پلتفورم هستین واسه خودتون باید اینو بدونین سیستم همیشه لازم نیست یچیزی شبیه لینوکس یا ویندوز بسازین حتی تلگرامم از ی سری دیدگاه ها میتونی یجور سیستم عامل باشه یعنی پلتفرومی ک روش روبات رو بعنوان نرم افزار میدونن اگه قصد ساخت ی ایده خاص یا خلاق دارین فلسفی و مفهومی فکر کنید شما میتونین با پایتون ی کروس پلتفرم یا سیستم عاملی بسازین که روی هر سیستم عاملی بشه اجرا کرد

    لازم نیس ک حتما با C بشینین با سیستم های سخت افزار ور برین

    بقول استیو جابز
    “ثینک دیفرنت”

    زبان برنامه نویسی , فرمورک , پلتفرم و سکو تعاریف متفاوتی هستند
    سیستم عامل علاوه بر اجرا و مدیریت نرم افزار باید مدیریت حافظه و منابع سیستم , مالتی تسکینگ و خیلی کارهای دیگه باید انجام بده در نتیجه فقط با زبان های برنامه نویسی کامپایلری که کدهای اجرایی یکپارچه و عمیق تولید میکنند میشه به سیستم عامل, فریمویر یا هر میان افزاری دست پیدا کرد دست پیدا کرد

    سلام خسته نباشید
    آقای لطفی اگر میشود لطفا به سوال بنده پاسخ بدین ???
    برای ساخت سیستم عامل‌های قدرتمند مثل ویندوز، چه پیشنیازهایی موردنیاز میباشد؟ ممنون میشم پاسخ بدین.

    سلام و وقت بخیر؛
    همان طور که در انتهای مقاله اشاره شده است، یک سیستم عامل از سه بخش کرنل، ابزارها و اپلیکیشن‌ها تشکیل یافته است. قدرت یک سستم عامل در وهله نخست به هدف آن وابسته است و این که تا چه حد و با چه کیفیتی می‌تواند هدف تعریف‌شده را برآورده سازد. از این رو بسته به ماهیت سیستم عامل ممکن است تعابیر مختلفی از سیستم عامل قدرتمند وجود داشته باشد. با این حال برای این که یک سیستم عامل قدرتمند داشته باشیم، می‌بایست کرنل سریعی طراحی کرده و ابزارها و اپلیکیشن‌های جامعی برای آن تدارک دید.

    سلام علیکم
    اگه بخوایم اینو روی ماشین مجازی نصب کنیم باید چیکار کنیم؟

    سلام علیکم
    میشه فایل iso اش رو هم بزارید
    ممنون میشم

    سلام و خسته نباشید.
    ممنون از آموزش تون.
    من چند(میلیون!)سوال داشتم.
    ۱. میگم این سیستم عاملی که شما طراحی کردید بر روی کنسول اجرا میشه دیگه؟ چطور میشه اون رو از حالت کنسول خارج کرد؟
    ۲.میشه بعد از اینکه کد ها رو اجرا کردم، کامپیوتر(دستگاه) به حالت اول یعنی ویندوز برگرده؟
    ۳.چطور میتونم اون فایلی رو که روی این لپتاپ کد شده رو لپتاپ دیگه ای اجرا کنم؟(شما اول متن توضیح دادین ولی من نفهمیدم‌.)
    ممنون میشم جواب بدید.ببخشید اگه سوالام زیادن.

    سلام دوست عزیز
    من یک گوشی داشتم میساختم که قابلیت ارسال ایمیل وضربان قلب و ارسال پیامک و تماس داشت

    یک لحظه به فکرم رسید بهتر است یک سیستم عامل برای این کا طراحی کنم

    حالا میخام بدوم اگر سیستم عامل رو با هسته لینوکس بنویسم میتونیم روش برنامه های آند رویدی بزنیم

    و میخلستم بدونم چطوری اصلا به هسته لینوکس دسترسی داشته باشم
    و اینکه چطور از هسته لینوک به گوشی انتقال بدم

    اصلا من هیچ اطلاعاتی ندارم
    ممنون میشم راهنمایی کنید

    میتونید از پلتفرم های ابتدایی مثل Raspberry pi الهام بگیرید این مینی کامپیوتر های تک بردی متن باز هستند خصوصا Beaglebone Black که حتی شماتیک آن بصورت PDF وجود دارد توجه کنید که اگر تمام موارد را انجام میدهید منطقی نیست چون نیاز به یک تیم کامل است زمانی که چنین فرکانس ئردازنده ای نیاز دارید باید در طراحی PCB نهایت استاندارد های لازم و تغذیه , کامپوننت ها و سایر موارد را درنظر بگیرید ئیشنهاد من این هست که ابتدا برروی سفارشی کردن توضیع هایی مثل raspbian تمرکز کنید اگر نیاز به معماری هایی مثل x86 اینتل هم داشته اید مانند من ان مقاله یک سرنخ است

    بنظرم راحتترین راه برای شما مطالعه raspberry pi است این مینی کامپیوتر های تک بردی مبنی بر معماری arm هستند همچنین متن باز هستند درنتیجه میتونید اطلاعات خوبی بدست بیارید میتونید Beaglebone Black رو هم مطالعه کنید که حتی شماتیک مدار اون هم بصورت pdf وجود داره توجه داشته باشد که اگر نمیخواهید از بورد آماده استفاده کنید منطقی نیست و باید یک تیم چندنفر از برنامه نویسان و مهندسین الکترونیک باشد وقتی به چنین فرکانس کاری از پردازنده نیاز دارید یعنی طراحی PCB نیاز به استاندارهای بالا دارد تغذیه و سایر کامپوننت ها هم مهم است ….

    خود اندروید یجور لینوکسه و میتونید سورس اندروید رو بگیرین و تغییر بدین این طوری راحت طره بنظر من

    یه سوال دیگه ، (سوالای من تمومی نداره ) بعد از اینکه سیستم عاملمون رو توسعه دادیم چطور میتونیم یه سری امکانات با زبانی مثل پایتون یا هر زبان دیگه بهش اضافه کنیم؟؟

    سلام ، ببخشید فقط قسمت بوت لودر از اسمبلی استفاده شد ؟ و یه سوال دیگم هم اینه که توی فرادرس اموزش های مرتبط با این مطلب هست (یعنی مثلا در اموزش اسمبلی این چیز های مربوط به سیستم عامل یا کار با بایوس یا مثلا در اموزش سی اموزش کرافیک هم هست؟؟)
    خیلی دنبال جواب این سوال هستم لطفا به من کم علم رو راهنمایی کنید

    سلام.من کرنل را امتحان کردم ولی IDE ارور داد.
    میشه کد کاملش را بهم بدید.
    هرکی تونسته با این روش سیستم عامل بسازه بگه.

    سلام ، ببخشید من یه سوال دیگه ای هم دارم . سوالم اینه که اگر ما بخوایم یه سیستم عامل با زبان اسمبلی که شبیه ام اس داس باشه و بعد به اون گرافیک اضافه کنیم وبعد از اون هم بخوایم مثلا سورس موزیلا فایر فاکس رو بهش اضافه کنیم باید چیکار کنیم.بعدشم من میخام سیستم عاملم بر پایه Basilica نباشه باید چیکار کنیم ؟

    باسلام.ااپا با زبان اسمبلی و c++هم می توان سیستم عامل نوشت؟

    سلام دوست عزیز؛
    یک سیستم عامل از سطوح و بخش‌های مختلفی تشکیل یافته است. در مورد توسعه کرنل به طور عمده از زبان‌های سطح پایین استفاده می‌شود. اما در مورد بخش‌های دیگر می‌توان از زبان‌های سطح بالا نیز استفاده کرد.
    با تشکر از توجه شما.

    سلام ببخشید اگه بخوای یک سیستم عامل کامل همرا با گرافیک کرنل و… طراحی کنی باید چه زبان های یاد داشته باشی

    اگه بخوای از هسته لینوکس استفاده کنی ولی یکم تغیر بدی و گرافیک براش یزنی باید چه ربان هایی یاد داشت

    سلام دوست عزیز؛
    همان طور که در مطلب هم اشاره شده برای توسعه سیستم عامل به طور عمده از زبان‌های سطح پایین مانند اسمبلی یا نزدیک به سطح پایین مانند C استفاده می‌شود. 96.6% کد کرنل لینوکس به زبان C نوشته شده است. در مورد گرافیک هم سیستم‌های عامل موجود از قبیل لینوکس و ویندوز به طور عمده از کتابخانه‌هایی مبتنی بر زبان C بهره می‌گیرند.

    ?یه ایده نو دارم برای سیتم عامل
    همش کاملا فقط دنبال یکیم پشتیبانی کنه
    فکر میکنی عملیش کنم بهتره یا بفروشمش؟

    سلام
    ایده‌های نو در غالب موارد موفق خواهند بود و از این جهت به شما تبریک می‌گوییم. در عین حال پیشنهاد می‌کنیم، در این خصوص با فرد خبره‌ای که تجربه نسبتا بالایی در حوزه مارکتینگ نرم‌افزار دارد مشاوره کنید.
    با آرزوی موفقیت.

    نظر شما چیست؟

    نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *