مسیرها
/
x86-64 Assembly
x86-64 Assembly
/
تمرین‌ها
/
مدیریت موجودی
مدیریت موجودی

مدیریت موجودی

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

مقدمه

اعداد صحیح

نمایش دودویی

عدد صحیح انتزاعی است که نماینده‌ی اعداد بدون جزء اعشاری است، مانند 4، -2، 0 یا 64532.

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

اعداد بدون علامت

اگر عدد فقط بتواند نامنفی باشد، به آن عدد بدون علامت می‌گویند.

اعداد بدون علامت مستقیماً به‌صورت مجموع توان‌های دو متناظر با همه‌ی بیت‌های روشن در دنباله‌شان نمایش داده می‌شوند.

بازه‌ی اعداد صحیح نامنفی قابل نمایش در یک ثبات از 0 (هیچ بیتی روشن نیست) تا 2⁶⁴ - 1 (مجموع همه‌ی ۶۴ بیت روشن) ادامه دارد.

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

دستور movzx (حرف z به معنای صفر) عملوند مبدأ ۸ بیتی یا ۱۶ بیتی را به عملوند مقصد بزرگ‌تری گسترش صفر می‌دهد. عملوند مبدأ ۳۲ بیتی همیشه با یک mov ساده به همه‌ی ۶۴ بیت عملوند مقصد گسترش صفر داده می‌شود.

اعداد علامت‌دار

اگر عدد صحیح بتواند مقدارهای مثبت یا منفی بگیرد، به آن عدد علامت‌دار می‌گویند.

برای نمایش اعداد منفی، x86-64 از نمایش مکمل دو استفاده می‌کند.

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

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

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

دستور movsx (حرف s به معنای علامت) عملوند مبدأ ۸ بیتی یا ۱۶ بیتی را به عملوند مقصد بزرگ‌تری گسترش علامت می‌دهد. گونه‌ای از movsx به نام movsxd همین کار را از یک عملوند مبدأ ۳۲ بیتی به یک عملوند مقصد ۶۴ بیتی انجام می‌دهد.

از دستور neg می‌توان برای تغییر علامت یک عدد استفاده کرد.

Caution

در زبان اسمبلی، هیچ راهی نیست که بفهمیم دنباله‌ای از بایت نماینده‌ی یک عدد علامت‌دار است یا بدون علامت. مسئولیت معنا دادن به این بایت‌ها بر عهده‌ی برنامه‌نویس است.

استفاده از توضیحات می‌تواند کمک بزرگی در این کار باشد.

مقدارهای آنی

در مفهوم قبلی گفته شد که یک عدد ثابت، مانند 4 یا -15، می‌تواند به‌عنوان عملوند مبدأ بسیاری از دستورها استفاده شود. به این عددها مقدار آنی می‌گویند.

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

وقتی عملوند مقصد ۶۴ بیتی است، آن ۳۲ بیت برای پر کردن آن گسترش علامت داده می‌شوند. نیمه‌ی بالایی عملوند کاملاً با نسخه‌هایی از بالاترین بیت مقدار آنی پر می‌شود، پس فقط عددی در بازه‌ی یک عدد صحیح علامت‌دار ۳۲ بیتی می‌تواند این‌گونه نوشته شود:

add rax, -1          ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647  ; the largest immediate an instruction like this accepts

عددی بیرون از آن بازه نمی‌تواند به‌عنوان مقدار آنی استفاده شود. استثنای این قاعده mov است که وقتی عملوند مقصد یک ثبات باشد، می‌تواند یک مقدار آنی کامل ۶۴ بیتی بگیرد. اگر به یک مقدار آنی ۶۴ بیتی نیاز باشد، اول با mov آن را در یک ثبات بارگذاری کنید، سپس از آن ثبات استفاده کنید:

mov rax, 3435973837           ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax

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

mov rax, -1                   ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1

جمع

جمع دو عدد را می‌توان با دستور add محاسبه کرد.

دستور تک‌عملوندی inc هم هست که 1 را به مقدار عملوندش اضافه می‌کند:

inc rax ; rax = rax + 1

جمع دو عدد صحیح برای اعداد بدون علامت و علامت‌دار یکسان عمل می‌کند.

تفریق

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

دستور تک‌عملوندی dec هم هست که 1 را از مقدار عملوندش کم می‌کند:

dec rax ; rax = rax - 1

تفریق دو عدد صحیح نیز برای اعداد بدون علامت و علامت‌دار یکسان عمل می‌کند.

ضرب

در x86-64 دو دستور متفاوت برای ضرب دو عدد وجود دارد. به‌طور کلی، ضرب بدون علامت از دستور mul و ضرب علامت‌دار از imul استفاده می‌کند.

دستور mul شکل تک‌عملوندی زیر را دارد که در آن src عملوند مبدأ است:

mul src

دستور imul می‌تواند شکل تک‌عملوندی، دو عملوندی یا سه‌عملوندی داشته باشد:

imul src
imul dest, src
imul dest, src1, src2
ضرب تک‌عملوندی

در شکل تک‌عملوندی، دو ثبات به‌طور ضمنی برای انجام ضرب استفاده می‌شوند: rax و rdx. اگر ضرب شامل دو عدد ۶۴ بیتی باشد، ۶۴ بیت پایینی نتیجه در rax و ۶۴ بیت بالایی در rdx قرار می‌گیرد.

معمولاً به این rdx:rax می‌گویند تا نشان دهد هر دو ثبات با هم به کار می‌روند:

mul rcx ; rax = lower 64 bits of rax * rcx
        ; rdx = upper 64 bits of rax * rcx

برای اندازه‌های دیگر عملوند نیز همین اتفاق می‌افتد. برای نمونه، اگر دو عدد ۳۲ بیتی ضرب شوند، از eax و edx استفاده می‌شود.

استثنا، ضرب بین دو بایت است.

در این حالت، به‌جای dl:al از ax استفاده می‌شود. بخش پایینی ax یعنی al ۸ بیت پایینی حاصل‌ضرب را می‌گیرد و بخش بالایی یعنی ah ۸ بیت بالایی را.

Caution

ثبات‌هایی که به‌طور ضمنی در ضرب استفاده می‌شوند، مانند rax و rdx، همیشه بازنویسی می‌شوند. اگر مقدارهای این ثبات‌ها بعداً لازم باشند، باید پیش از عملیات ذخیره شوند.

ضرب دو عملوندی

شکل دو عملوندی imul یک عملوند مقصد صریح دارد و از نحوه‌ی نگارش معمول پیروی می‌کند. از rdx استفاده نمی‌شود. در عوض، نتیجه بریده می‌شود تا در عملوند مقصد جا بگیرد.

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
ضرب سه‌عملوندی

شکل سه‌عملوندی imul دو عملوند مبدأ دارد که دومی همیشه یک مقدار آنی (یک عدد ثابت) است. هر دو عملوند مبدأ در هم ضرب می‌شوند و نتیجه بریده و در عملوند مقصد قرار می‌گیرد:

imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100

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

مدیریت سرریز

ضرب دو عملوندی و سه‌عملوندی هر دو نتیجه را بر می‌دارند تا در اندازه‌ی عملوند مقصد جا بگیرد. ضرب تک‌عملوندی تمام بازه را حفظ می‌کند، اما معمولاً بین دو ثبات rdx و rax تقسیم می‌شود.

پس گاهی مفید است عملوندها را پیش از ضرب گسترده کنیم تا تمام حاصل‌ضرب در یک ثبات واحد جا بگیرد. عملوند بدون علامت گسترش صفر و عملوند علامت‌دار گسترش علامت داده می‌شود:

movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx       ; the 32-bit product fits in eax, and edx is cleared

تقسیم

همان‌ند ضرب، دو دستور نیز برای تقسیم دو عدد وجود دارد. تقسیم بدون علامت از دستور div و تقسیم علامت‌دار از idiv استفاده می‌کند.

هر دو دستور فقط با یک عملوند کار می‌کنند:

div src
idiv src

تقسیم ۱۶ بیتی، ۳۲ بیتی و ۶۴ بیتی به‌ترتیب از dx:ax، edx:eax و rdx:rax به‌عنوان مقسوم استفاده می‌کنند. در این حالت‌ها، هر دو ثبات با هم یک مقدار ۲N بیتی می‌سازند که N اندازه‌ی عملیات (۱۶ بیتی، ۳۲ بیتی یا ۶۴ بیتی) است. این مقدار سپس بر عملوند مبدأ تقسیم می‌شود. خارج‌قسمت در ax، eax یا rax و باقی‌مانده در dx، edx یا rdx نوشته می‌شود، بسته به اندازه‌ی عملیات.

تقسیم بین بایت‌ها ویژه است: به‌جای dl:al از ax استفاده می‌شود. ۸ بیت پایینی ax یعنی al خارج‌قسمت عملیات را می‌گیرد و ۸ بیت بالایی یعنی ah باقی‌مانده را.

توجه کنید که همه‌ی بیت‌های مقسوم باید پیش از تقسیم به‌درستی تنظیم شده باشند. هر بیت روشنی در rdx (یا در ah برای تقسیم ۸ بیتی) در مقدار در حال تقسیم سهیم است.

در تقسیم بدون علامت، وقتی مقدار در حال تقسیم در نیمه‌ی پایینی جا می‌گیرد، نیمه‌ی بالایی باید صفر شود. هر دستوری که آن بیت‌ها را صفر کند کافی است. برای نمونه، mov edx, 0 بیت‌های بالایی را در تقسیم ۳۲ بیتی صفر می‌کند.

در تقسیم علامت‌دار، در عوض باید مقدار گسترش علامت داده شود. دستورهایی هستند که این فرایند را خودکار می‌کنند: cbw، cwd، cdq و cqo. اولی بیت‌های ah را بر اساس علامت al تنظیم می‌کند. بقیه به‌ترتیب گسترش علامت را از ax به dx، از eax به edx و از rax به rdx انجام می‌دهند.

Caution

ثبات‌هایی که به‌طور ضمنی در تقسیم استفاده می‌شوند، مانند rax و rdx، همیشه بازنویسی می‌شوند. اگر مقدارهای این ثبات‌ها بعداً لازم باشند، باید پیش از تقسیم ذخیره شوند.

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

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

چهار کار پیش رو دارید که همه به مدیریت حمل‌ونقل مربوط می‌شوند.

Note

این‌ها دستورهایی هستند که در این مفهوم به آن‌ها اشاره شده است:

دستور توضیح
add a, b a = a + b
inc a a = a + 1
sub a, b a = a - b
dec a a = a - 1
imul a rdx:rax = a * rax (signed)
imul a, b a = a * b (signed, truncated)
imul a, b, c a = b * c (signed, truncated)
mul a rdx:rax = a * rax (unsigned)
div a rax = quotient, rdx = remainder of rdx:rax / a (unsigned)
idiv a rax = quotient, rdx = remainder of rdx:rax / a (signed)
movzx a, b a = b, adding 0 to the extra bits
movsx a, b a = b, adding 1 to the extra bits if b < 0 or 0 otherwise
Note

به یاد داشته باشید که می‌توانید با تغییر اسم عملوند، به همان ثبات با اندازه‌های مختلف دسترسی داشته باشید. برای نمونه: rax (۶۴ بیتی)، eax (۳۲ بیتی)، ax (۱۶ بیتی)، al (۸ بیتی).

برای دیدن جدول کامل می‌توانید به مفهوم قبلی مراجعه کنید.

1. وزن هر جعبه را به دست آورید

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

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

تابعی به اسم get_box_weight تعریف کنید که وزن کل یک جعبه را بر حسب g برمی‌گرداند. این تابع به‌ترتیب، پارامترهای زیر را می‌گیرد:

  • تعداد اقلام محصول اول در جعبه
  • وزن هر قلم از محصول اول، بر حسب g
  • تعداد اقلام محصول دوم در جعبه
  • وزن هر قلم از محصول دوم، بر حسب g

در نظر بگیرید که وزن یک جعبه‌ی خالی ۵۰۰ گرم است. ثابتی به اسم WEIGHT_OF_EMPTY_BOX در بالای فایل راه‌حل تعریف شده است.

مثال:

get_box_weight(30, 40, 50, 20);
// => 2700

همه‌ی آرگومان‌ها اعداد صحیح نامنفی ۱۶ بیتی هستند و مقدار بازگشتی یک عدد صحیح نامنفی ۳۲ بیتی است.

2. محاسبه کنید چند جعبه در کامیون جا می‌شود

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

تابعی به اسم max_number_of_boxes تعریف کنید که مشخص می‌کند چند جعبه با ارتفاع معین را می‌توان به‌صورت عمودی (یکی روی دیگری) در کامیون روی هم چید.

این تابع ارتفاع جعبه را بر حسب cm به عنوان پارامتر می‌گیرد. در نظر بگیرید که ارتفاع داخلی کامیون ۳۰۰ سانتی‌متر است. ثابتی به اسم TRUCK_HEIGHT در بالای فایل راه‌حل تعریف شده است.

مثال:

max_number_of_boxes(30);
// => 10

آرگومان و مقدار بازگشتی اعداد صحیح نامنفی ۸ بیتی هستند. ارتفاع جعبه همیشه حداقل 2 است، بنابراین نتیجه در ۸ بیت جا می‌شود.

3. بررسی کنید که همه‌ی محصولات در نظر گرفته شده‌اند

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

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

  • تعداد اقلام هنوز حساب‌نشده برای یک محصول
  • تعداد اقلام آن محصول در یک جعبه

مثال:

items_to_be_moved(76532, 120);
// => 76412

آرگومان‌ها اعداد صحیح نامنفی ۳۲ بیتی هستند. مقدار بازگشتی یک عدد صحیح ۳۲ بیتی است. اگر در این فرایند خطایی رخ دهد، ممکن است نتیجه عددی منفی باشد.

4. دریافت دستمزد

دستمزد شما بر اساس تعداد جعبه‌های جابه‌جاشده و تعداد سفرهای لازم با کامیون محاسبه می‌شود. برای هر جعبه ۵ دلار و برای هر سفر ۲۲۰ دلار دریافت می‌کنید. ثابت‌های PAY_PER_BOX و PAY_PER_TRUCK_TRIP در بالای فایل راه‌حل تعریف شده‌اند.

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

یعنی مبلغ خالصی که باید دریافت کنید، یا باید بپردازید، چنین است:

net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value

این دستمزد، یا بدهی، به‌طور مساوی بین شما و تعدادی کارگر که استخدام کرده‌اید تقسیم می‌شود. هر پول یا بدهی باقی‌مانده مال خودتان است. برای نمونه، اگر مبلغ خالص پول 100 باشد و بین 6 نفر (شما و 5 کارگر) تقسیم شود، سهم شما 20 می‌شود (100/(5 + 1) = 16 به‌علاوه‌ی 4 باقی‌مانده).

تابعی به اسم calculate_payment تعریف کنید که مشخص می‌کند در پایان چقدر باید دریافت کنید یا بپردازید. این تابع به‌ترتیب، پارامترهای زیر را می‌گیرد:

  • مبلغی که از قبل دریافت کرده‌اید، به‌صورت یک عدد صحیح نامنفی ۶۴ بیتی
  • تعداد کل جعبه‌های جابه‌جاشده، به‌صورت یک عدد صحیح نامنفی ۳۲ بیتی
  • تعداد سفرهای انجام‌شده با کامیون، به‌صورت یک عدد صحیح نامنفی ۳۲ بیتی
  • تعداد اقلام شکسته یا گم‌شده، به‌صورت یک عدد صحیح نامنفی ۳۲ بیتی
  • ارزش هر قلم از دست‌رفته، به‌صورت یک عدد صحیح نامنفی ۶۴ بیتی
  • تعداد کارگرانی که دستمزد یا بدهی با شما تقسیم می‌شود، به‌صورت یک عدد صحیح مثبت ۸ بیتی

مثال:

calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029

مقدار بازگشتی یک عدد صحیح ۶۴ بیتی است.

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

آماده‌اید مدیریت موجودی را شروع کنید؟

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