مسیرها
/
x86-64 Assembly
x86-64 Assembly
/
تمرین‌ها
/
صفحه‌آرایی مجله
صفحه‌آرایی مجله

صفحه‌آرایی مجله

تمرین یادگیری

مقدمه

ماکرو

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

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

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

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

ماکروی تک‌خطی

ساده‌ترین تعریف، با %define به یک مقدار یا یک عبارت کوتاه اسم می‌دهد:

%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)

هر ظهور بعدی WIDTH پیش از اسمبل با 8 جایگزین می‌شود. جایگزینی متنی است، پس اسم در هر جایی که ظاهر شود جایگزین می‌شود، چه در یک عملوند و چه درون تعریف دیگر.

Note

برای ثابت‌های عددی، equ را به‌جای %define ترجیح دهید.

ماکروی تک‌خطی که با %define اعلام می‌شود تنها هنگام استفاده گسترش می‌یابد و می‌تواند دوباره مقداردهی شود یا تعریفش لغو شود. از سوی دیگر، ثابت equ در همان نقطه‌ی تعریف ارزیابی می‌شود و هرگز تغییر نمی‌کند.

%define می‌تواند پارامترهای دارای اسم هم بگیرد و مانند یک تابع درون‌خطی کوچک عمل کند:

%define double(x) ((x) + (x))

    mov eax, double(WIDTH)   ; assembled as mov eax, ((8) + (8))
Caution

هم کل بدنه و هم هر پارامتر را در پرانتز بپیچید. با %define square(x) x*x، فراخوانی square(1+1) به 1+1*1+1 گسترش می‌یابد که برابر با 3 است. اگر به‌شکل %define square(x) ((x)*(x)) نوشته شود، همان فراخوانی به ((1+1)*(1+1)) گسترش می‌یابد که برابر با 4 است.

اسم‌هایی که با %define تعریف می‌شوند به بزرگی و کوچکی حروف حساس‌اند. گونه‌ی %idefine اسم را نسبت به بزرگی و کوچکی حروف نامحسوس می‌کند، اگر چنین چیزی خواسته شود.

ماکروهای تک‌خطی برای معنادار کردن بخش‌های مختلف کد مفیدند. اگر با دقت به کار روند، خوانایی را بهبود می‌دهند و احتمال غلط‌های تایپی را کاهش می‌دهند. برای نمونه، ثباتی که درون یک تابع هدف مشخصی دارد را می‌توان بر پایه‌ی همان هدف تعریف کرد:

%define COUNTER rcx
    ...
.loop:
    ...
    dec COUNTER
    jnz .loop
    ...

ماکروی چندخطی

یک دنباله‌ی کامل از کد، با هر تعداد خط، با %macro ثبت می‌شود و با %endmacro بسته می‌شود. عددی که پس از اسم می‌آید نشان می‌دهد ماکرو چند آرگومان می‌گیرد، که می‌تواند صفر باشد. هنگام ارسال آرگومان‌ها، آن‌ها با کاما (,) از هم جدا می‌شوند:

%macro example_macro 0
...
%endmacro

آرگومان‌ها در ماکروهای چندخطی اسمی ندارند. به آن‌ها با %1، %2 و به همین ترتیب، به همان ترتیبی که ارسال می‌شوند ارجاع داده می‌شود:

; Multiplies two values, returning the result in the first argument
%macro mult_two 2        ; %1 = accumulator, %2 = value
    imul %1, %2
%endmacro

    mult_two eax, ecx    ; multiplies eax and ecx and returns the result into eax
    mult_two r8, r9      ; multiplies r8 and r9 and returns the result into r8

توجه کنید که آرگومان‌ها باید نحوه‌ی نگارش معتبری برای عملیات انجام‌شده در بدنه‌ی ماکرو داشته باشند. در اینجا هم mult_two eax, ecx و هم mult_two r8, r9 اسمبل می‌شوند، اما mult_two eax, cl اسمبل نمی‌شود، چون imul eax, cl تعریف نشده است.

برچسب‌ها درون یک ماکرو

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

; clobbers rcx
%macro clear_n 2         ; %1 = base address, %2 = count of dwords to clear
    xor ecx, ecx
%%loop:
    mov dword [%1 + rcx*4], 0
    inc ecx
    cmp ecx, %2          ; %2 is compared with ecx: it is an immediate or a 32-bit operand
    jb %%loop
%endmacro

    clear_n rdi, 4       ; gets its own %%loop
    clear_n rsi, 8       ; a different %%loop, no clash

تکرار در زمان اسمبل

می‌توان یک بلوک کد یک‌خطی را با times تکرار کرد. این به‌ویژه هنگام اعلام مقادیر تکراری در حافظه مفید است:

section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1

برای تکرار کد چندخطی، می‌توانیم به‌جایش از %rep استفاده کنیم:

%rep 2          ; number of repetitions: 2
    add rcx, 32
    mov dword [rsi + rcx], 0
%endrep

توجه کنید که تعداد تکرارها درست پس از %rep مشخص می‌شود و یک %endrep برای بستن بلوک تکراری وجود دارد.

استفاده از یک شمارنده‌ی زمان اسمبل درون بلوک %rep رایج است، همان‌طور که یک حلقه‌ی زمان اجرا از یک شمارنده استفاده می‌کند. در حالی که یک ثابت عدد صحیح زمان اسمبل را می‌توان با equ اعلام کرد، برای متغیرهای عدد صحیح زمان اسمبل به‌جایش از %assign استفاده می‌کنیم. برخلاف %define، %assign بی‌درنگ به یک عدد ارزیابی می‌شود و می‌تواند دوباره مقداردهی شود:

    xor eax, eax
%assign i 0
%rep 4
    add eax, i           ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep

پس از اجرای پیش‌پردازنده، حلقه از میان می‌رود و چهار دستور add باقی می‌ماند که مقادیر فوری 0، 1، 2 و 3 در آن‌ها جای گرفته‌اند. این بازکردن حلقه است که توسط اسمبلر انجام می‌شود: نه ثبات شمارنده‌ای و نه انشعابی، فقط کد خطی.

اسمبل شرطی

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

%if ... %elif ... %else ... %endif یک شاخه را بر پایه‌ی یک آزمون عددی انتخاب می‌کنند:

%if WIDTH > 4
    mov eax, 2
%elif WIDTH > 2
    mov eax, 1
%else
    mov eax, 0
%endif

تقسیم به فایل

ماکروها اغلب در فایل مخصوص خودشان نگه داشته می‌شوند و با %include به هر منبعی که به آن‌ها نیاز دارد وارد می‌شوند:

%include "patterns.mac"

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

اگر ممکن است فایل ماکرو به‌تنهایی اسمبل شود، باید پسوند فایل .asm داشته باشد. توجه کنید که ماکروها هیچ پیوندی ندارند. حتی اگر فایل ماکرو اسمبل شود، ماکروهایش فقط برای فایل دیگری که آن را %include می‌کند قابل مشاهده است.

در غیر این صورت، فایلی که فقط از طریق %include وارد می‌شود می‌تواند هر پسوند فایلی داشته باشد. استفاده از .asm یا .mac یک قرارداد رایج است.

دستورالعمل‌ها

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

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

چهار وظیفه دارید.

Caution

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

1. نام‌گذاری ثابت‌های قالب

قالب مجله چند عدد ثابت است و بقیهٔ صفحه‌آرایی تنها با نام به آن‌ها ارجاع می‌دهد. باید به هرکدام در زمان اسمبل نامی خوانا بدهید:

  • COLUMN_COUNT برابر با 4 است
  • COLUMN_STRIDE برابر با 18 است
  • SPACE برابر با 0x20 است، بایت خالی
  • RULE برابر با 0x3D است، بایت =
  • RULE_WIDTH برابر با 4 است
  • PLAIN برابر با 0x2E است، بایت .
  • RULED برابر با 0x2D است، بایت -
  • BOXED برابر با 0x23 است، بایت #

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

out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order

2. نوشتن ماکروی fill_run

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

نخست، نامی خوانا یعنی COUNTER برای ثبات rcx تعریف کنید. سپس ماکروی fill_run را تعریف کنید که باید یک آرایه را با N نسخه از یک بایت پر کند. این ماکرو سه آرگومان به این ترتیب می‌گیرد:

  1. نشانی مقصد، که در یک ثبات همه‌کارهٔ ۶۴ بیتی پاس داده می‌شود.
  2. تعداد بایت‌هایی که باید نوشته شود (N)، که در یک ثبات همه‌کارهٔ ۶۴ بیتی یا به‌صورت یک مقدار فوری پاس داده می‌شود.
  3. بایتی که باید نوشته شود، که به‌صورت یک مقدار فوری پاس داده می‌شود.
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE

ماکروی fill_run می‌تواند COUNTER را آزادانه خراب کند، اما هیچ ثبات دیگری را نه.

توجه داشته باشید که تعداد بایت‌ها ممکن است در یک ثبات همه‌کارهٔ ۶۴ بیتی، مانند rax یا rsi، یا به‌صورت یک مقدار فوری، مانند 10 یا 4 پاس داده شود. می‌توانید فرض کنید که این تعداد هرگز منفی نیست.

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

length = 8
page   = {32, 32, 32, 32, 32, 32, 32, 32}

width  = 8
page   = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces

3. نوشتن ماکروی lay_columns

شبکه برای این قالب ثابت است، پس شروع ستون‌ها را می‌توان در زمان اسمبل محاسبه کرد.

ماکروی lay_columns را تعریف کنید که COLUMN_COUNT آفست می‌نویسد، که هرکدام یک عدد صحیح ۶۴ بیتی است. این ماکرو دو آرگومان به این ترتیب می‌گیرد:

  1. نشانی مقصد، که در یک ثبات همه‌کارهٔ ۶۴ بیتی پاس داده می‌شود
  2. حاشیهٔ چپ، که در یک ثبات همه‌کارهٔ ۶۴ بیتی پاس داده می‌شود
lay_columns rsi, rax

تعداد آفست‌هایی که باید در نشانی مقصد نوشته شوند ثابت و برابر با COLUMN_COUNT است.

هر آفست یک مقدار ۶۴ بیتی است که با offset[i] = margin + i * COLUMN_STRIDE محاسبه می‌شود. در این رابطه i اندیس آفست است و از 0 شروع می‌شود؛ margin هم آرگومان دوم است.

این ماکرو می‌تواند دو آرگومان ثباتی خود را خراب کند، اما هیچ ثبات دیگری را نه. این ماکرو یک بار فراخوانی می‌شود.

margin = 3
out    = {3, 21, 39, 57}

4. نوشتن ماکروی set_header

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

ماکروی set_header را تعریف کنید که دو آرگومان به این ترتیب می‌گیرد:

  1. نشانی مقصد.
  2. یک شناسهٔ سبک که به‌صورت مقدار فوری پاس داده می‌شود، که در آن 0 یعنی ساده، 1 یعنی خط‌دار و 2 یعنی کادردار.

نشانی مقصد یک عبارت نشانی حافظهٔ مؤثر است. این عبارت یک ثبات پایه می‌گیرد که ممکن است با یک ثبات اندیس که در 1، 2، 4 یا 8 ضرب شده است جمع شود، به‌همراه یک مقدار فوری:

set_header rdx+8*rcx+2, 1

بایت سرصفحه (PLAIN، RULED یا BOXED) را به‌جای یک انشعاب زمان اجرا، با اسمبل شرطی (%if / %elif / %else) انتخاب کنید.

این ماکرو نباید هیچ ثباتی را خراب کند. این ماکرو سه بار در همان تابع فراخوانی می‌شود.

pitch = 4
out   = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
x86-64 Assembly Exercism

آماده‌اید صفحه‌آرایی مجله را شروع کنید؟

در Exercism ثبت‌نام کنید تا x86-64 Assembly را همراه با 22 مفهوم130 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.