در این تمرین، نحوهی نگارش و مدل اجرای یک زبان کوچک را تعریف میکنید که از زبان اسمبلی x86-64 الهام گرفته شده است.
هدف، رسمیسازی این است که کد در یک محیط محدود چگونه نوشته، تجزیه و اجرا میشود.
کد اسمبلی شما درون یک ساختار ویژه نوشته میشود:
let program := assemble!(
-- assembly code here
)
این ساختار باید یک برنامه تولید کند که تابعی را شبیهسازی میکند؛ تابعی که کد اسمبلی را اجرا میکند و مقداری برمیگرداند.
سپس این برنامه با نحوهی نگارش زیر فراخوانی میشود، که در آن a، b، c و غیره آرگومان هستند:
let result := program(a, b, c, ...)
همهی آرگومانها و مقدار بازگشتی، اعداد صحیح علامتدار ۶۴ بیتی هستند. حداکثر ۶ آرگومان مجاز است.
مقدار بازگشتی و همهی آرگومانها در ثباتها منتقل میشوند. زبان شما باید از ثباتهای زیر پشتیبانی کند:
| ثبات | نقش |
|---|---|
rdi |
آرگومان اول |
rsi |
آرگومان دوم |
rdx |
آرگومان سوم |
rcx |
آرگومان چهارم |
r8 |
آرگومان پنجم |
r9 |
آرگومان ششم |
rax |
مقدار بازگشتی |
ثباتها با مقدار پیشفرض 0 شروع میکنند، مگر اینکه برای انتقال آرگومانها به تابع استفاده شوند.
برای مثال، در برنامهی زیر، همهی ثباتها مقدار 0 دارند، بهجز rdi که با 10 مقداردهی اولیه میشود و rsi که با -20 مقداردهی اولیه میشود:
program(10, -20)
مقدار بازگشتی برنامه همیشه در rax ذخیره میشود.
اسم ثباتها به بزرگی و کوچکی حروف حساس نیست.
این یعنی rax، RAX و rAx همگی به یک ثبات اشاره میکنند.
هر خط از کد اسمبلی میتواند به یکی از دو شکل زیر باشد:
جریان اجرای برنامه، مگر آنکه دستوری آن را تغییر دهد، بهصورت خطی پیش میرود. این یعنی هر خط پیش از خط بعدی بهطور کامل اجرا میشود.
برچسبها نحوهی نگارش زیر را دارند:
<label>:
آنها مقدار هیچ ثباتی را تغییر نمیدهند و هیچ تأثیری بر برنامه ندارند جز اینکه مکانهای مشخصی از کد را نشانهگذاری میکنند تا دستورها بتوانند از آنها استفاده کنند.
اسم برچسبها به بزرگی و کوچکی حروف حساس است.
این یعنی Start، start و sTart همگی برچسبهای متفاوتی هستند.
بیشتر دستورها نحوهی نگارش زیر را دارند:
<opcode> <destination>, <source>
opcode عملیاتی را نشان میدهد که اجرا میشود و از مقادیر عملوندهای destination و source استفاده میکند.
نتیجهی عملیات در عملوند destination ذخیره میشود.
برای مثال، دستور add مقادیر عملوندهای destination و source را جمع میکند و این نتیجه را در عملوند destination ذخیره میکند.
عملوند destination همیشه یک ثبات است، در حالی که عملوند source میتواند یک ثبات یا یک عدد ثابت باشد.
این فهرستِ دستورهایی است که برنامهی شما باید از آنها پشتیبانی کند:
| کد عملیات | عملیات انجامشده |
|---|---|
mov |
destination := source |
add |
destination := destination + source |
sub |
destination := destination - source |
mul |
destination := destination * source |
div |
destination := destination / source |
xor |
destination := destination ^^^ source |
and |
destination := destination &&& source |
or |
destination := destination ||| source |
shl |
destination := destination <<< source |
shr |
destination := destination >>> source |
علاوه بر اینها، برنامهی شما باید از دستور دو عملوندی cmp پشتیبانی کند.
این دستور هیچ ثباتی را تغییر نمیدهد، بلکه عملوندهای destination و source را مقایسه میکند و حالت داخلی برنامه را روی یکی از سه گزینه تنظیم میکند:
destination > source
destination == source
destination < source
نحوهی پیگیری داخلی این حالت به خودتان بستگی دارد.
برخی دستورها جریان برنامه را تغییر میدهند و اجرا را به نقطهی دیگری از کد منتقل میکنند. به آنها دستورهای پرش میگویند.
همهی آنها فقط یک عملوند میگیرند که برچسبی است و مقصد پرش، یعنی نقطهای از کد که اجرا در آن ادامه مییابد، را نشان میدهد:
<opcode> <label>
دستور jmp همیشه پرش به برچسب مقصد را انجام میدهد.
سایر دستورهای پرش تنها زمانی پرش میکنند که حالت داخلی برنامه روی مقدار مشخصی تنظیم شده باشد.
این یعنی معمولاً پیش از آنها یک دستور cmp میآید.
اینها دستورهای پرشی هستند که برنامهی شما باید از آنها پشتیبانی کند:
| دستور | عملیات انجامشده |
|---|---|
jmp |
پرش بیقیدوشرط. پرش همیشه انجام میشود |
je |
اگر حالت داخلی برابر باشد، پرش میکند |
jl |
اگر حالت داخلی کوچکتر باشد، پرش میکند |
jg |
اگر حالت داخلی بزرگتر باشد، پرش میکند |
کدهای عملیات دستورها، چه دو عملوندی و چه یک عملوندی، به بزرگی و کوچکی حروف حساس نیستند.
این یعنی add، ADD و aDd همگی به یک دستور اشاره میکنند.