پالت رنگ

پالت رنگ

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

مقدمه

حافظه

معمولاً سیستمی‌عامل حافظه را برای یک برنامه در یک چیدمان کلی نگاشت می‌کند:

آدرس ناحیه‌ی حافظه
بالا پشته
...
هیپ
قطعه‌ی خواندنی-نوشتنی
قطعه‌ی کد/فقط‌خواندنی
پایین رزروشده

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

توابعی که تا اینجا تعریف کرده‌ایم، همگی در بخش .text بوده‌اند. این بخش داده‌های اجراییِ فقط‌خواندنی را نگه می‌دارد. از بخش‌های دیگر برای تعریف متغیرهای داده استفاده می‌شود که ممکن است فقط‌خواندنی یا خواندنی-نوشتنی باشند، اما اجرایی نیستند.

بخش .data

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

در NASM (اسمبلر Netwide، همان اسمبلری که این ترک از آن استفاده می‌کند)، یک متغیر مقداردهی‌شده یک اسم، یک دستور که اندازه‌ی داده را مشخص می‌کند و فهرستی از مقادیر جداشده با کاما دارد. هر یک از این‌ها با یک فاصله از بقیه جدا می‌شود و ممکن است بعد از برچسب، به صورت اختیاری، یک : بیاید.

دستورهای اصلی و اندازه‌های داده‌ی مربوط به آن‌ها عبارت‌اند از:

دستور اندازه
db ۱ بایت
dw ۲ بایت
dd ۴ بایت
dq ۸ بایت

برای نمونه، این کد یک متغیر یک‌بایتی به اسم space با مقدار 10 تعریف می‌کند:

section .data
    space db 10

متغیرهایی که در section .data تعریف می‌شوند تغییرپذیرند، یعنی خواندنی-نوشتنی هستند. این متغیرها مدت ذخیره‌سازی ایستا دارند، که یعنی در کل زمان اجرای برنامه وجود دارند.

بخش .rodata

بخش .rodata مشابه section .data است. هر دو بخش شامل داده‌های مقداردهی‌شده‌اند که به یک شکل تعریف می‌شوند و مدت ذخیره‌سازی یکسانی دارند.

تفاوت اصلی این دو این است که داده‌های section .rodata تغییرناپذیرند، یعنی فقط‌خواندنی.

Note

ثابت‌هایی که با equ تعریف می‌شوند با ثابت‌هایی که در section .rodata تعریف می‌شوند تفاوت دارند.

ثابتی که با equ تعریف می‌شود فضایی در حافظه اشغال نمی‌کند و اسمبلر مستقیماً مقدارش را جای آن می‌گذارد. در واقع این ثابت یک جانگهدار برای همان مقدار است.

در مقابل، ثابت‌هایی که در section .rodata تعریف می‌شوند واقعاً در حافظه ذخیره می‌شوند و آدرس دارند.

دسترسی به داده

برچسب‌ها و غیرمستقیم‌سازی

داده‌ی تعریف‌شده باید اسمی داشته باشد که به آن نسبت داده شود. به این اسم «برچسب» می‌گویند.

برچسب نمادی است که آدرس مشخصِ داده در حافظه را رمزگذاری می‌کند. آدرس‌ها در x86-64 مقادیر ۶۴‌بیتی‌اند.

در NASM، اگر بخواهید با برچسبِ داده مستقیماً به آن دسترسی پیدا کنید، آنچه به دست می‌آورید حافظه‌ی تخصیص‌یافته نیست، بلکه آدرس آن است:

section .data
    example dq 27 ; this declares a 8-byte variable initialized with 27

section .text
fn:
    mov rax, example ; this stores the address of the declared variable in rax, not its contents
    ...

برای دسترسی به محتوای یک آدرس حافظه، باید آن را واسازی کنید. به این کار «غیرمستقیم‌سازی» می‌گویند.

در NASM، این کار با [] انجام می‌شود:

section .data
    example dq -27 ; this declares a 8-byte variable initialized with -27

section .text
fn:
    mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
    ...

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

مهم‌ترین پیشوندها و اندازه‌هایشان در یک برنامه‌ی معمول x86-64 عبارت‌اند از:

پیشوند اندازه
byte ۱ بایت
word ۲ بایت
dword ۴ بایت
qword ۸ بایت

همان بارگذاری را می‌توان با ذکر صریح اندازه نوشت:

    mov rax, qword [example] ; same dereference, size stated explicitly

کار خوبی است که هنگام واسازی حافظه همیشه از یک پیشوند استفاده کنید.

نوشتن در حافظه

نوشتن در حافظه هم به همان شکل و با واسازی یک آدرس انجام می‌شود:

section .data
    example1 db 10            ; example1 is a 1-byte memory location initialized with value 10
    example2 dq -456          ; example2 is a 8-byte memory location initialized with value -456
    example3 dd 54            ; example3 is a 4-byte memory location initialized with value 54

section .text
fn:
    mov byte [example1], 20   ; example1 now has value 20
    mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
    mov dword [example3], eax ; example3 now has value equal to the contents in eax

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

section .data
    example4 dw 4
    example5 dq -8
    example6 dd 15

section .text
fn:
    add word [example4], 5     ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
    imul rax, qword [example5] ; rax = rax * (-8)
    ; this is not possible -> sub dword [example6], dword [example6]
دستورالعمل LEA

هرچند می‌توان از mov برای ذخیره‌ی آدرس یک متغیر در یک ثبات استفاده کرد، دستورالعمل ویژه‌ای برای همین کار وجود دارد: lea.

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

lea rax, [example] ; this stores the address of 'example' in rax

استفاده از lea برای محاسبه و ذخیره‌ی آدرس‌های حافظه در ثبات‌ها طبیعی‌تر است.

آدرس‌دهی نسبی

هنگام دسترسی به مکان‌های حافظه، رفتار پیش‌فرض در NASM تولید «آدرس‌های مطلق» است، یعنی آدرس‌های حافظه‌ی ثابت.

به دلایل امنیتی، فایل‌های اجرایی اغلب به صورت PIE (فایل اجرایی مستقل از موقعیت) ساخته می‌شوند، که در آن‌ها ناحیه‌های حافظه در مکان‌های تصادفی قرار می‌گیرند. در یک PIE، آدرس نهایی یک متغیر در زمان پیوند معلوم نیست. بنابراین کد، آدرس‌ها را به صورت انحرافی از مقدار ثبات ویژه‌ای به اسم rip محاسبه می‌کند، ثباتی که به دستورالعمل بعدی برای اجرا اشاره می‌کند.

به این کار معمولاً «آدرس‌دهی نسبی نسبت به RIP» می‌گویند.

در NASM می‌توانید با عملگر rel دسترسی نسبی نسبت به RIP را درخواست کنید:

mov rax, qword [rel variable]

آدرس‌دهی نسبی را می‌توان با قرار دادن default rel در ابتدای یک فایل مبدأ، به حالت پیش‌فرض آن فایل هم تبدیل کرد.

همه‌ی تمرین‌های این ترک به صورت PIE کامپایل و پیوند می‌شوند، پس برای تولید آدرس‌های نسبی باید از rel استفاده کرد.

قابلیت مشاهده

برچسب‌هایی (توابع و داده‌ها) که در هر بخشی (مثلاً .text، .data، .rodata) تعریف می‌شوند، در همان فایل مبدأ قابل مشاهده‌اند. اگر به صورت global تعریف شوند، برای فایل‌های مبدأ دیگر هم قابل مشاهده‌اند.

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

default rel

section .data

global number1 ; 'number1' is a variable visible to other source files
number1 db 200

extern number2 ; 'number2' is a variable visible to the current source file, but defined in another

section .text

extern sum ; sum is a function visible to the current source file, but defined in another

fn:
    mov dil, byte [number1]
    mov sil, byte [number2]
    call sum
    ...

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

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

او برای این آزمایش‌ها از شما کمک خواست.

Note

در این تمرین، هر رنگ با یک عدد ۳۲بیتی (۴بایتی) نمایش داده می‌شود که مقدار RGB آن را رمزگذاری می‌کند.

یک مقدار RGB از ۳ کانال تشکیل شده است، قرمز، سبز و آبی، که هرکدام ۸ بیت (۱ بایت) را اشغال می‌کنند. بایت چهارم معمولاً به کانال آلفا اختصاص دارد، اما در این تمرین مقدارش خالی (0) خواهد بود.

1. گرفتن مقدار RGB برای یک رنگ

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

تابعی به اسم get_color_value تعریف کنید که مقدار ۳۲بیتی یک رنگ را برمی‌گرداند. این تابع یک نشانی معتبر برای این رنگ در جدول رنگ‌ها را به عنوان پارامتر می‌گیرد.

get_color_value(black)
// => 0

راهنما: ۳۲ بیت معادل ۴ بایت است.

2. افزودن رنگ پایه

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

تابعی به اسم add_base_color تعریف کنید که مقدار ۳۲بیتی یک رنگ را در متغیر base_color ذخیره می‌کند تا بعداً بتوان از آن استفاده کرد. این تابع مقدار بازگشتی ندارد و نشانی رنگ در جدول رنگ‌ها را به عنوان پارامتر می‌گیرد.

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

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

به‌طور پیش‌فرض، در شروع برنامه، base_color باید با مقدار ۳۲بیتی سفید که 0xFFFFFF00 است مقداردهی اولیه شود.

راهنما: NASM اعدادی را می‌پذیرد که با 0x در ابتدا به‌صورت مبنای شانزده تعریف شده باشند، مانند 0xFFFFFF00.

3. تعریف ثابت‌ها برای رنگ‌های اصلی

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

  • RED، با مقدار 0xFF000000.
  • GREEN، با مقدار 0x00FF0000.
  • BLUE، با مقدار 0x0000FF00.

برای هر یک از این رنگ‌ها یک ثابت تعریف کنید. این ثابت‌ها باید از فایل‌های منبع دیگر قابل دسترس باشند.

4. ترکیب رنگ‌ها

رنگ‌ها باید بر اساس combining_function که در یک فایل منبع دیگر تعریف شده است ترکیب شوند. این تابع مقادیر ۳۲بیتی base_color و یک رنگ ثانویه را که باید با آن ترکیب شود به عنوان پارامتر می‌گیرد. این تابع مقدار ۳۲بیتی رنگ ترکیب‌شده را برمی‌گرداند.

تابعی به اسم make_color_combination تعریف کنید که دو رنگ را ترکیب می‌کند و نتیجه را در حافظه ذخیره می‌کند. این تابع مقدار بازگشتی ندارد و پارامترهای زیر را به این ترتیب می‌گیرد:

  • نشانی‌ای که مقدار ۳۲بیتی رنگ ترکیب‌شده باید در آن ذخیره شود.
  • نشانی یک رنگ ثانویه در جدول رنگ‌ها، برای ترکیب با رنگ اصلی.
Caution

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

ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
x86-64 Assembly Exercism

آماده‌اید پالت رنگ را شروع کنید؟

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