در مقاله قبل با Interrupt Vector Table آشنا شدیم. دیدیم که در Real Mode اولین یک کیلوبایت حافظه به جدولی اختصاص دارد که برای 256 شماره Interrupt، آدرس Handler مربوط به هرکدام را نگه می‌دارد.

مثلاً وقتی برنامه‌ای می‌نویسد:

INT 10h

CPU می‌تواند از طریق IVT آدرس Handler مربوط به Interrupt شماره 10h را پیدا کند و اجرای برنامه را به آن منتقل کند.

اما هنوز چند سؤال مهم باقی مانده است.

وقتی CPU دستور INT را می‌بیند دقیقاً چه می‌کند؟

چرا INT 10h تصویر را کنترل می‌کند ولی INT 16h با Keyboard کار دارد؟

چرا INT 13h را می‌توان در یک Bootloader استفاده کرد اما INT 21h را نه؟

و چرا DOS سرویس‌های خودش را روی همان مکانیزمی بنا کرد که BIOS از آن استفاده می‌کند؟

در این مقاله این بخش از معماری PCهای کلاسیک را کمی عملی‌تر بررسی می‌کنیم.

اول یک نکته مهم: Interrupt فقط BIOS یا DOS نیست

کلمه Interrupt معنی بسیار عمومی‌تری دارد. Interrupt در اصل مکانیزمی در CPU است که اجازه می‌دهد اجرای عادی برنامه موقتاً متوقف شود و کنترل به یک Handler منتقل شود.

دلیل این انتقال می‌تواند متفاوت باشد. ممکن است یک سخت‌افزار از CPU درخواست توجه کند، یک وضعیت خاص داخل CPU اتفاق بیفتد، یا خود برنامه عمداً دستور زیر را اجرا کند:

INT n

بنابراین وقتی درباره INT 10h، INT 13h، INT 16h یا INT 21h صحبت می‌کنیم، CPU ذاتاً نمی‌داند این شماره‌ها به Video، Disk، Keyboard یا DOS مربوط هستند.

از دید CPU، اینها فقط شماره Interrupt هستند. معنی واقعی آنها را نرم‌افزاری تعیین می‌کند که Handler مربوط به آن Vector را نصب کرده است.

وقتی CPU دستور INT را اجرا می‌کند چه اتفاقی می‌افتد؟

فرض کنیم CPU در حال اجرای یک برنامه است و به این دستور می‌رسد:

INT 10h

CPU نمی‌تواند همین‌طور به Handler بپرد و بعداً فراموش کند از کجا آمده است. بعد از پایان Handler باید بتواند اجرای همان برنامه قبلی را ادامه دهد.

بنابراین قبل از انتقال کنترل، اطلاعات لازم برای برگشت را روی Stack ذخیره می‌کند. در مدل کلاسیک Real Mode، به شکل مفهومی:

Program is running
       |
       v
    INT 10h
       |
       v
Save FLAGS
Save CS
Save IP
       |
       v
Find vector 10h in IVT
       |
       v
Load new CS:IP
       |
       v
Run interrupt handler

CPU هنگام ورود به Handler، Flagهای مرتبط با Trap و Maskable Interrupt را نیز پاک می‌کند تا ورود به Handler تحت شرایط کنترل‌شده انجام شود.

IRET؛ راه برگشت از Interrupt

برای برگشت از Interrupt معمولاً از دستور:

IRET

استفاده می‌شود.

IRET اطلاعاتی را که هنگام ورود به Interrupt روی Stack ذخیره شده بودند برمی‌گرداند:

Restore IP
Restore CS
Restore FLAGS

پس چرخه به شکل ساده چنین است:

Application
    |
    | INT n
    v
Interrupt Handler
    |
    | IRET
    v
Application continues

CPU آدرس Handler را چطور پیدا می‌کند؟

در Real Mode هر Interrupt Vector چهار بایت دارد:

2 bytes  Offset
2 bytes  Segment

شماره Interrupt در چهار ضرب می‌شود تا محل Vector به دست بیاید.

مثلاً برای:

INT 10h

داریم:

10h × 4 = 40h

بنابراین Vector شماره 10h از آدرس:

0000:0040

در جدول Interruptها شروع می‌شود.

دو بایت اول Offset و دو بایت بعد Segment مربوط به Handler را مشخص می‌کنند. CPU این مقادیر را در IP و CS قرار می‌دهد و اجرای Handler آغاز می‌شود.

پس وقتی می‌گوییم INT 10h سرویس Video BIOS است، منظور این نیست که داخل CPU بخشی مخصوص Video BIOS وجود دارد. Firmware هنگام راه‌اندازی سیستم Vector مربوط به INT 10h را به Routine مناسب وصل می‌کند.

پس شماره‌های BIOS از کجا آمده‌اند؟

در معماری PC، تعدادی از Interrupt Vectorها به‌صورت قراردادی برای سرویس‌های مشخص BIOS استفاده شدند.

INT 10h   Video services

INT 13h   Disk services

INT 16h   Keyboard services

این قرارداد باعث می‌شد نرم‌افزار بدون دانستن جزئیات دقیق سخت‌افزار، از Firmware کمک بگیرد.

INT 10h؛ صحبت کردن با Video BIOS

یکی از معروف‌ترین BIOS Interruptها:

INT 10h

است.

اما خود INT 10h فقط می‌گوید وارد سرویس Video BIOS شو. Video BIOS چندین Function دارد و برای مشخص کردن Function معمولاً مقادیر خاصی در Registerها قرار می‌دهیم.

مثلاً Function:

AH = 0Eh

برای نمایش Character در حالت Teletype استفاده می‌شود.

mov ah, 0Eh
mov al, 'A'
mov bh, 0

int 10h

معنی آن تقریباً این است:

AH = 0Eh      -> Teletype output
AL = 'A'      -> Character to display

INT 10h       -> Call Video BIOS

Handler مربوط به Video BIOS مقدار AH را بررسی می‌کند و متوجه می‌شود Function شماره 0Eh درخواست شده است.

نکته مهم این است که خود دستور INT 10h نمی‌گوید چه کاری با Video انجام شود. Registerها Function و پارامترهای آن را مشخص می‌کنند.

یک Interrupt می‌تواند ده‌ها Function داشته باشد

Interrupt Number
       +
Function Number
       +
Parameters in Registers

این سه مورد یک درخواست کامل را می‌سازند. از دید برنامه‌نویس، چیزی شبیه یک API Call بسیار Low-Level است.

Registerهای CPU خودشان نقش ورودی و خروجی را بازی می‌کنند.

INT 16h؛ Keyboard بدون درگیری مستقیم با Controller

اگر برنامه بخواهد منتظر فشرده شدن یک کلید بماند، می‌تواند از BIOS Keyboard Services استفاده کند.

یکی از Functionهای ساده:

INT 16h
AH = 00h

است.

xor ah, ah
int 16h

اگر هیچ کلیدی آماده نباشد، این Function منتظر می‌ماند. وقتی کاربر کلیدی فشار دهد، BIOS اطلاعات آن را برمی‌گرداند.

AL = ASCII character
AH = BIOS scan code

بنابراین برنامه برای یک کار ساده لازم نیست مستقیماً Keyboard Controller را مدیریت کند.

یک تفاوت مهم: INT 09h با INT 16h یکی نیست

Keyboard سخت‌افزاری نیز می‌تواند باعث Interrupt شود. این رویداد سخت‌افزاری با فراخوانی نرم‌افزاری INT 16h یکی نیست.

Keyboard Hardware
       |
       v
Hardware Interrupt
       |
       v
BIOS keyboard handler
       |
       v
Keyboard buffer

بعداً برنامه می‌تواند با:

INT 16h

از BIOS بخواهد اطلاعات موجود در Buffer را دریافت کند.

INT 13h؛ یکی از مهم‌ترین Interruptها برای Boot

برای داستانی که از مقاله اول دنبال کرده‌ایم، شاید مهم‌ترین BIOS Interrupt:

INT 13h

باشد.

INT 13h مجموعه‌ای از سرویس‌های مربوط به Disk را فراهم می‌کند. مثلاً Function کلاسیک:

AH = 02h

برای خواندن Sectorهای Disk استفاده می‌شود.

یک نمونه مفهومی:

xor ax, ax
mov es, ax
mov bx, 7E00h

mov ah, 02h
mov al, 01h

mov ch, 00h
mov cl, 02h
mov dh, 00h

; DL = drive number

int 13h
jc disk_error

درخواست بالا تقریباً می‌گوید:

Read 1 sector
from disk
and place it at ES:BX

اگر عملیات با خطا روبه‌رو شود، BIOS می‌تواند وضعیت خطا را از طریق Registerها و Carry Flag در اختیار برنامه قرار دهد.

چرا INT 13h برای Bootloader مهم است؟

فرض کنیم BIOS اولین Boot Sector را از Disk خوانده و آن را در حافظه اجرا کرده است. Boot Sector فقط 512 بایت فضا دارد و واضح است که یک سیستم‌عامل کامل داخل آن جا نمی‌شود.

پس Boot Code باید اطلاعات بیشتری از Disk بخواند. اما هنوز DOS، Windows یا Linux اجرا نشده‌اند و Driver معمول سیستم‌عامل نیز در اختیار Bootloader نیست.

یکی از راه‌های کلاسیک Bootloader برای خواندن Disk استفاده از:

BIOS INT 13h

است. Bootloader می‌تواند از این سرویس برای خواندن Stage بعدی خودش یا بخش‌های لازم از Disk استفاده کند.

حالا به INT 21h می‌رسیم

اگر با Assembly در DOS کار کرده باشید احتمالاً معروف‌ترین Interrupt برایتان:

INT 21h

است.

اما یک تفاوت اساسی وجود دارد: INT 21h یک BIOS Service نیست. این Vector توسط DOS برای ارائه بسیاری از سرویس‌های سیستم‌عامل استفاده می‌شود.

برای اینکه INT 21h به معنایی که برنامه‌های DOS انتظار دارند کار کند، ابتدا باید DOS Load و Initialize شده باشد.

یک برنامه DOS ساده

یکی از Functionهای معروف INT 21h:

AH = 09h

است.

این Function انتظار دارد DS:DX به Stringای اشاره کند که با Character $ تمام می‌شود.

org 100h

mov dx, message
mov ah, 09h
int 21h

mov ax, 4C00h
int 21h

message db 'Hello from DOS!$'

بخش اول از DOS می‌خواهد String را نمایش دهد و Function 4Ch برای پایان دادن به Program و بازگشت به DOS استفاده می‌شود.

چرا همان کار را با INT 10h هم می‌توانیم انجام دهیم؟

برای نمایش Character می‌توان از BIOS یعنی INT 10h استفاده کرد، اما DOS خودش هم سرویس‌های Output دارد.

تفاوت مهم در سطح Abstraction است. BIOS بیشتر به Hardware و Firmware نزدیک است. DOS مفاهیم سطح بالاتری مثل File، Directory، Process، Standard Input و Standard Output را می‌شناسد.

مثلاً BIOS مفهوم یک فایل مانند C:\TEST.TXT را نمی‌شناسد، ولی DOS آن را می‌شناسد.

BIOS و DOS در یک تصویر

+--------------------------+
|       Application        |
+--------------------------+
            |
            | INT 21h
            v
+--------------------------+
|           DOS            |
+--------------------------+
            |
            | sometimes BIOS services
            v
+--------------------------+
|       BIOS / Firmware    |
+--------------------------+
            |
            v
+--------------------------+
|        Hardware          |
+--------------------------+

این شکل یک مدل آموزشی است و نباید نتیجه بگیریم تمام عملیات DOS الزاماً از BIOS عبور می‌کنند. DOS و Driverها بسته به نوع عملیات می‌توانند مسیرهای متفاوتی داشته باشند.

چرا Bootloader نمی‌تواند INT 21h استفاده کند؟

در لحظه‌ای که Boot Sector تازه اجرا شده:

BIOS          موجود است
DOS           هنوز وجود ندارد

بنابراین:

INT 13h

می‌تواند به BIOS Disk Service برسد.

اما:

INT 21h

نمی‌تواند از سرویس‌های DOS استفاده کند، چون DOS هنوز Load نشده و Handlerهای مربوط به سرویس‌هایش را نصب نکرده است.

Before DOS:

BIOS Services     YES
DOS Services      NO

بعد از Load شدن DOS:

BIOS Services     YES
DOS Services      YES

یک Vector متعلق به CPU نیست؛ قابل تغییر است

وقتی می‌گوییم INT 21h = DOS نباید تصور کنیم CPU این ارتباط را Hard-Code کرده است. CPU فقط Vector شماره 21h را می‌شناسد.

اینکه آن Vector به چه Handlerی اشاره کند در حافظه مشخص می‌شود. بنابراین نرم‌افزار می‌تواند Interrupt Vector را تغییر دهد.

به این کار معمولاً Interrupt Hooking گفته می‌شود.

Interrupt Hooking چیست؟

Before:

INT n
  |
  v
Original Handler

بعد از Hook:

INT n
  |
  v
Our Handler
  |
  v
Original Handler

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

TSRها و Interruptها

برنامه‌های Terminate and Stay Resident یا TSR نمونه خوبی هستند.

یک TSR اجرا می‌شود، بخشی از خودش را در حافظه نگه می‌دارد و ظاهراً کنترل را به DOS برمی‌گرداند. اما چون Handler خودش را روی یک Interrupt نصب کرده، بعدها دوباره می‌تواند فعال شود.

بنابراین برنامه‌ای که ظاهراً Terminate شده است، بخشی از آن همچنان Resident in memory باقی می‌ماند.

آیا باید مستقیماً IVT را تغییر می‌دادیم؟

از نظر سخت‌افزاری می‌توان به حافظه IVT دسترسی داشت و Vector را تغییر داد. اما زیر DOS روش تمیزتری هم وجود داشت.

DOS خودش Functionهایی برای گرفتن و تنظیم Interrupt Vector ارائه می‌کرد.

INT 21h
AH = 35h

برای دریافت آدرس Handler فعلی یک Interrupt استفاده می‌شد.

و:

INT 21h
AH = 25h

برای تنظیم Vector جدید.

Hardware Interrupt و Software Interrupt را قاطی نکنیم

تا اینجا بیشتر درباره دستور INT n صحبت کردیم. این یک Software-generated Interrupt است؛ خود برنامه عمداً آن را اجرا می‌کند.

اما Hardware نیز می‌تواند از CPU درخواست Interrupt کند.

Hardware Event
     |
     v
Interrupt Request
     |
     v
CPU
     |
     v
Interrupt Handler

در مقابل:

Program
     |
     | INT n
     v
CPU
     |
     v
Interrupt Handler

در هر دو حالت ایده اصلی شبیه است: اجرای عادی موقتاً متوقف می‌شود و Handler مناسب اجرا می‌شود. اما منبع درخواست متفاوت است.

چرا BIOS از همین مکانیزم برای API استفاده کرد؟

CPU از قبل مکانیزمی داشت که می‌توانست Handler را پیدا کند، Context لازم برای برگشت را ذخیره کند، کنترل را منتقل کند و دوباره برگردد.

Firmware می‌توانست از همین مکانیزم به‌عنوان رابط سرویس‌های خودش استفاده کند. به‌جای اینکه هر برنامه آدرس واقعی Routineهای BIOS را بداند، یک شماره استاندارد Interrupt را فراخوانی می‌کرد.

Video     -> INT 10h
Disk      -> INT 13h
Keyboard  -> INT 16h

این باعث می‌شد برنامه به آدرس فیزیکی دقیق Routine داخل ROM وابسته نباشد.

DOS هم همان ایده را ادامه داد

وقتی DOS اجرا شد، همان مکانیزم آماده در اختیارش بود. بنابراین DOS هم مجموعه‌ای از Interruptها را به سرویس‌های سیستم‌عامل اختصاص داد.

معروف‌ترین آنها:

INT 21h

شد.

از دید معماری، یک مکانیزم ساده CPU یعنی Interrupt تبدیل شد به یکی از پایه‌های ارتباط میان Hardware، Firmware، Operating System و Application.

از Reset Vector تا INT 21h

حالا اگر سه مقاله این مجموعه را کنار هم قرار دهیم، تصویر کامل‌تری داریم.

Power On
   |
   v
CPU Reset
   |
   v
Reset Vector
   |
   v
BIOS / Firmware
   |
   v
Hardware Initialization
   |
   v
BIOS Interrupt Services
   |
   v
Boot Sector / Bootloader
   |
   v
DOS
   |
   v
INT 21h Services
   |
   v
Application

چرا این داستان برای سیستم‌های مدرن مهم است؟

ممکن است همه اینها بسیار قدیمی به نظر برسند. امروز برنامه Android یا Windows معمولاً با INT 10h متن روی صفحه چاپ نمی‌کند.

سیستم‌های مدرن Protected Mode، Long Mode، Driverها، System Callها، UEFI و معماری‌های متفاوتی دارند. اما ایده بنیادی همچنان مهم است.

سیستم از چند مرحله تشکیل شده است. هر مرحله بخشی از Hardware را آماده می‌کند و کنترل را به مرحله بعد می‌دهد. هر لایه نیز Interfaceهایی در اختیار لایه بالاتر قرار می‌دهد.

قدم بعدی: همان داستان، این بار روی Android

حالا وقت آن است که از PC کلاسیک فاصله بگیریم.

در PC قدیمی مسیر را تقریباً به این شکل دنبال کردیم:

Reset
  |
  v
BIOS
  |
  v
Boot Sector
  |
  v
Bootloader
  |
  v
Operating System

اما وقتی یک دستگاه Android را روشن می‌کنیم چه اتفاقی می‌افتد؟

اولین کد روی SoC از کجا اجرا می‌شود؟ Boot ROM چیست؟ Bootloader چرا چند Stage دارد؟ Verified Boot کجای زنجیره وارد می‌شود؟ و چه کسی در نهایت Linux Kernel را وارد حافظه می‌کند؟

در مرحله بعد همین داستان «اولین دستور تا سیستم‌عامل» را از دنیای PC به دنیای SoC و Android منتقل می‌کنیم.

مقاله قبلی مقاله بعدی: شروع Boot در Android بازگشت به مقالات صفحه اصلی