مب

مبانی در x86-64 Assembly

1 تمرین

درباره‌ی مبانی

کامنت‌ها

مسیر x86-64 در Exercism از NASM (اسمبلر Netwide) به عنوان اسمبلر خود استفاده می‌کند. در NASM، کامنت‌ها با یک نقطه‌ویرگول (;) شروع می‌شوند.

کامنت‌ها را می‌توان هر جایی از برنامه قرار داد و اسمبلر هر چیزی را که بعد از یک ; بیاید نادیده می‌گیرد.

ثابت‌ها

می‌توان یک ثابت زمان اسمبل را در NASM با equ تعریف کرد. برای مثال، این کد ثابتی به اسم UNIVERSE با مقدار 42 تعریف می‌کند:

UNIVERSE equ 42

ثابت‌ها یک بار، هنگام تعریف، ارزیابی می‌شوند و بعداً نمی‌توان آن‌ها را دوباره تعریف کرد.

رجیسترهای همه‌کاره (GPRs)

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

برخی از این رجیسترها برای انواع مختلفی از محاسبات به کار می‌روند و رجیسترهای همه‌کاره (GPRs) نامیده می‌شوند. برخی دیگر کاربردهای ویژه یا اختصاصی دارند.

در x86-64، ۱۶ رجیستر همه‌کاره‌ی ۶۴ بیتی (GPR) وجود دارد که می‌توان به‌صورت ۳۲ بیتی، ۱۶ بیتی یا ۸ بیتی هم به آن‌ها دسترسی داشت. رجیسترهای همه‌کاره در ادامه توصیف شده‌اند؛ در آن n در rn از ۸ تا ۱۵ است: r8، r9، r10، r11، r12، r13، r14 و r15.

۶۴ بیتی ۳۲ بیتی ۱۶ بیتی ۸ بیتی
rax eax ax ah/al
rbx ebx bx bh/bl
rcx ecx cx ch/cl
rdx edx dx dh/dl
rsi esi si sil
rdi edi di dil
rbp ebp bp bpl
rsp esp sp spl
rn rnd rnw rnb

هنگام استفاده از کمتر از ۶۴ بیت، بیت‌های دسترسی‌یافته معمولاً از بخش پایینی رجیستر هستند. استثنای این قاعده ah، bh، ch و dh هستند که به ۸ بیت بالایی از بخش ۱۶ بیتی رجیستر دسترسی می‌دهند.

تصویری از نحوه‌ی دسترسی به بیت‌های رجیستر rax:

+--------+---------------------------------------+
| 64-bit |                  rax                  |
+--------+-------------------+-------------------+
| 32-bit |                   |        eax        |
+--------+-------------------+---------+---------+
| 16-bit |                             |    ax   |
+--------+-----------------------------+----+----+
| 8-bit  |                             | ah | al |
+--------+-----------------------------+----+----+

برخی از این رجیسترها باید در طول فراخوانی توابع حفظ شوند: rbp، rsp، rbx، r12، r13، r14 و r15. حفظ نکردن آن‌ها می‌تواند به خطا یا به رفتار تعریف‌نشده منجر شود.

بقیه حفظ نمی‌شوند و می‌توان آزادانه از آن‌ها استفاده کرد: rax، rcx، rdx، rdi، rsi، r8، r9، r10 و r11.

Note

رجیسترها معمولاً با دستور push روی پشته حفظ می‌شوند. سپس پیش از پایان تابع با دستور pop بازگردانده می‌شوند.

دستورها

دستورها تکه‌هایی از محاسبات هستند که یک پردازنده می‌تواند انجام دهد. آن‌ها معمولاً این شکل را دارند:

name destination, source

پس اسم دستور اول می‌آید، سپس حداقل یک فاصله، بعد از آن «عملوند» مقصد، یک کاما (,) و در پایان عملوند مبدأ. عملوند مبدأ معمولاً توسط یک دستور تغییر نمی‌کند؛ فقط عملوند مقصد تغییر می‌کند.

هر دو عملوند باید اندازه‌ی یکسانی داشته باشند، مگر اینکه خلافش ذکر شده باشد. برای مثال، اگر عملوند مبدأ ۱۶ بیت داشته باشد، عملوند مقصد هم باید ۱۶ بیت داشته باشد.

برای مثال، برای ذخیره‌ی یک مقدار در یک رجیستر می‌توانیم از دستور mov استفاده کنیم:

mov rax, rdx  ; rax = rdx
              ; mov is the name, rax is the destination operand and rdx is the source operand

قطعه‌کد بالا محتوای همه‌ی ۶۴ بیت rdx، یعنی عملوند مبدأ، را در همه‌ی ۶۴ بیت rax، که عملوند مقصد این دستور است، کپی می‌کند.

در بیشتر دستورها می‌توان از عددهای ثابت صحیح به عنوان عملوند مبدأ (اما نه به عنوان عملوند مقصد) استفاده کرد:

mov rcx, 42 ; rcx = 42
            ; rcx is the destination operand and 42 is the source operand

توجه کنید که نوشتن در یک رجیستر ۳۲ بیتی بیت‌های بالایی را هم پاک می‌کند، بنابراین mov eax, 42 معادل mov rax, 42 است. این موضوع برای رجیسترهای ۱۶ بیتی و ۸ بیتی درست نیست.

برای عملیات حسابی جمع، تفریق و ضرب می‌توانیم از دستورهای add، sub و imul استفاده کنیم:

add rax, rsi ; rax = rax + rsi
imul rax, rdi ; rax = rax * rdi
sub rax, r8 ; rax = rax - r8

توابع

دستورها در قالب توابع سازمان‌دهی می‌شوند. همه‌ی توابع در بخش section .text فایل منبع قرار می‌گیرند.

یک اعلان تابع از این موارد تشکیل می‌شود:

۱. یک «برچسب» با اسم تابع که بعد از آن یک : می‌آید. ۲. دستورهایی که تابع را تعریف می‌کنند. ۳. دستور بازگشت، ret.

قواعد مشخصی برای نحوه‌ی پاس دادن آرگومان‌ها به تابع و بازگرداندن مقدار از آن وجود دارد. دو قرارداد اصلی فراخوانی، System V AMD64 ABI و Microsoft x64 هستند.

این مسیر از قرارداد فراخوانی System V AMD64 ABI استفاده می‌کند و شش آرگومان صحیح اول در رجیسترها به تابع پاس داده می‌شوند. ترتیب پاس دادن آن‌ها این است: rdi، rsi، rdx، rcx، r8 و r9.

یک مقدار صحیح در رجیستر rax از تابع بازگردانده می‌شود.

برای فراخوانی یک تابع از دستور call استفاده می‌کنیم. یک تابع را می‌توان هر جایی از همان فایل منبع فراخوانی کرد. برای اینکه یک تابع برای فایل‌های منبع دیگر قابل مشاهده باشد، باید از رهنمود global استفاده کرد.

برای مثال، این کد تابعی به اسم sum تعریف می‌کند:

section .text ; functions are placed here

global sum ; sum is visible to other source files

sum:
    ; first argument is passed in rdi
    ; second argument is passed in rsi
    ; return value is placed in rax

    mov rax, rdi ; rax is now equal to rdi
    add rax, rsi ; rax = rax + rsi

    ret ; function return

و این کد تابع sum ما را با آرگومان‌های ۳ و ۵ فراخوانی می‌کند:

mov rdi, 3  ; First argument in rdi
mov rsi, 5  ; Second argument in rsi
call sum

; The rax register now contains the value 8 (3 + 5), after sum returns
ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

مبانی را یاد بگیرید