دس

دستورهای شرطی در x86-64 Assembly

4 تمرین

درباره‌ی دستورهای شرطی

RFLAGs

ثبات ویژه‌ای به اسم rflags وجود دارد. بیت‌های آن مثل پرچم برای شرط‌های مختلف عمل می‌کنند.

برخی از آن‌ها در ادامه فهرست شده‌اند:

نام نماد بیت
نقلی CF ۰
صفر ZF ۶
علامت SF ۷
سرریز OF ۱۱

برای فهرست کامل، به راهنمای اینتل مراجعه کنید.

دستورهای مقایسه

پرچم‌های rflags به‌طور مستقیم تغییر نمی‌کنند. در عوض، دستورهای مختلف بسیاری آن‌ها را تنظیم می‌کنند.

برای نمونه، وقتی نتیجه صفر باشد، بسیاری از عملیات‌های حسابی یا بیتی ZF را تنظیم می‌کنند.

یکی از رایج‌ترین دستورهایی که برای بررسی شرط‌ها به کار می‌رود cmp است. این دستور دو عملوند می‌گیرد و پرچم‌ها را به‌روزرسانی می‌کند، اما عملوندهایش را تغییر نمی‌دهد.

دستور CMP

دستور cmp عملوند دوم را از اولی کم می‌کند و پرچم‌ها را بر اساس نتیجه تنظیم می‌کند.

اگر A عملوند اول و B عملوند دوم باشد:

پرچم زمانی تنظیم می‌شود که
CF A < B (بدون علامت)
ZF A == B
SF A < B (علامت‌دار، بدون سرریز)
OF سرریز در تفریق علامت‌دار

انشعاب

به‌طور پیش‌فرض، کد در x86-64 به‌صورت ترتیبی از بالا به پایین اجرا می‌شود.

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

در زبان‌های سطح بالاتر، این کار معمولاً با انتزاع‌هایی مثل شرط‌های if...else انجام می‌شود. با این حال، چنین چیزی در x86-64 وجود ندارد.

در عوض، x86-64 دستورهایی فراهم می‌کند که اجرا را به‌طور مؤثر به نقطه‌ی دیگری از کد منتقل می‌کنند. به این کار branching می‌گویند.

Note

ما قبلاً دو دستور از این نوع دیده‌ایم: call و ret.

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

اگر ret پیدا نشود، اجرا از یک تابع به تابع بعدی ادامه می‌یابد. گاهی می‌توان از این کار برای بهینه‌سازی جریان کد استفاده کرد.

پرش غیرشرطی

دستور jmp اجرای برنامه را بدون قید و شرط به نقطه‌ی دیگری از کد منتقل می‌کند. عملوند تکی آن یک برچسب است که نشانی نقطه‌ای را دارد که اجرا از آنجا ادامه پیدا می‌کند.

برای نمونه، تابع زیر را در نظر بگیرید:

fn:
    mov rax, 5
    jmp end

    add rax, 10
end:
    ret

وقتی fn فراخوانی می‌شود، اجرا از mov rax, 5 شروع می‌شود. این کار در آن نقطه مقدار rax را برابر ۵ می‌کند.

دستور بعدی jmp end است که اجرا را به برچسب end منتقل می‌کند.

بعد از end، دستور بعدی ret است که اجرا را به تابع فراخوان بازمی‌گرداند.

توجه کنید که چون add rax, 10 بعد از jmp end و قبل از end قرار دارد، هرگز اجرا نمی‌شود. مقدار rax هنگام بازگشت fn برابر ۵ است.

پرش شرطی

خانواده‌ی دستورهای jcc اجرای برنامه را فقط در صورتی به نقطه‌ی دیگری منتقل می‌کنند که شرط خاصی برقرار باشد. در غیر این صورت، اجرا به‌صورت ترتیبی ادامه می‌یابد.

هر شرط به یک یا چند پرچم در rflags نگاشت می‌شود. بعضی از گونه‌های jcc بررسی می‌کنند که پرچمی تنظیم شده باشد؛ بعضی دیگر بررسی می‌کنند که پاک شده باشد.

cc در jcc تحت‌اللفظی نیست، بلکه به پسوند خاص مرتبط با پرچم آزمایش‌شده اشاره دارد.

پسوندهای زیادی وجود دارد و بسیاری از آن‌ها همان شرطی را آزمایش می‌کنند که پسوند دیگری آزمایش می‌کند. برخی از آن‌ها مستقیماً به یک پرچم اشاره دارند، طوری که اگر آن پرچم خاص تنظیم شده باشد، دستور به یک برچسب می‌پرد:

پسوند می‌پرد اگر
z ZF == 1
c CF == 1
s SF == 1
o OF == 1

بسیاری دیگر طوری انتخاب شده‌اند که به معنایشان در دستور cmp اشاره کنند. برای نمونه:

دستور پسوند می‌پرد اگر
cmp A, B e A == B
cmp A, B l A < B (علامت‌دار)
cmp A, B b A < B (بدون علامت)
cmp A, B g A > B (علامت‌دار)
cmp A, B a A > B (بدون علامت)

می‌توان e را بعد از l، b، g یا a اضافه کرد تا تساوی هم در شرط لحاظ شود:

cmp rcx, r8
jge two      ; this jumps to 'two' if rcx is greater than, or equal to, r8 in a signed comparison
jbe two      ; this jumps to 'two' if rcx is lesser than, or equal to, r8 in an unsigned comparison

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

برای نمونه، jnz زمانی می‌پرد که ZF تنظیم نشده باشد. به همین ترتیب، jnae زمانی می‌پرد که A بزرگ‌تر یا مساوی B نباشد (A و B به‌عنوان اعداد صحیح بدون علامت تفسیر می‌شوند).

Note

برخی پسوندها نام‌های دیگری برای همان شرط‌ها هستند. برای نمونه، jz (پسوند z، برای ZF) و je (پسوند e، برای تساوی) هر دو زمانی می‌پرند که ZF تنظیم شده باشد. دلیلش این است که در cmp، وقتی تفریق صفر شود ZF تنظیم می‌شود، که معادل برابر بودن دو عملوند است.

اما پسوندهای دیگر ترکیبی از پرچم‌ها را آزمایش می‌کنند و نمی‌توان مستقیماً آن‌ها را با یک پسوند تک‌پرچمی جایگزین کرد.

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

برچسب‌های محلی

برچسب‌ها در کل فایل منبع قابل مشاهده‌اند و محلیِ یک تابع نیستند. بنابراین استفاده‌ی دوباره از یک اسم برچسب ممکن نیست.

برای شبیه‌سازی رفتار یک برچسب محلی، NASM نشان‌گذاری خاصی برای برچسبی دارد که با یک نقطه (.) قبل از آن تعریف می‌شود. این نشان‌گذاری برچسبی را تعریف می‌کند که به‌طور ضمنی اسم برچسب پیشین بدون نقطه را در خود دارد:

section .text
fn1:
    ...
.example: ; this is fn1.example
    ...
    ret

fn2:
    ...
.example: ; this is fn2.example
    ...
    ret

هنوز می‌توان با استفاده از اسم کامل برچسب، از هر جای کد به این برچسب پرش کرد؛ برای نمونه، jmp fn1.example.

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

section .text
fn1:
    ...
.example:
    ...
    jmp .example ; this jumps to fn1.example

fn2:
    ...
.example:
    ...
    jmp .example ; this jumps to fn2.example

توجه کنید که یک برچسب بدون نقطه در داخل تابع، در عمل تابع دیگری را تعریف می‌کند:

section .text
fn1:
    ...
.example: ; this is 'fn1.example'
    ...
non_dotted:
    ...
.example: ; this is 'non_dotted.example'
    ...
    ret
ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

دستورهای شرطی را یاد بگیرید