امیر محمدی
امیر محمدی
خواندن ۴ دقیقه·۸ ماه پیش

حل مساله اول اویلر با اسمبلی و gdb

مدتی قبل با «مرحله ۳» آشنا شدم که ترجمه‌ای فارسی از Project Euler بود. بهانه خوبی به نظرم اومد تا دوباره نگاهی به سوالاتش بندازم.

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

section .text section .data section .bss

در حال حاضر نیازی به دیتا ندارم پس می‌شه از .data و .bss چشم‌پوشی کرد. فایل بالا رو می‌تونم به این شکل اسمبل کنم:

$ nasm -felf64 problem1.asm -o problem1.o

نگاهی به فایل خروجی بندازیم:

$ file problem1.o problem1.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

برای اجرای برنامه لازمه فایل بالا لینک بشه. از gnu linker استفاده می‌کنم:

$ ld -m elf_x86_64 problem1.o -o problem1 ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000

لینکرها برای پیدا کردن آدرس instructionها و اجراشون از لیبل‌های مشخص کمک می‌گیرند (یا بطور پیش‌فرض از آدرسی خاص شروع به اجرا می‌کنند). پس به این شکل:

section .text global _start _start: nop

یا بطور مشابه می‌تونستیم با gcc هم خروجی اسمبلر رو لینک کنیم. gcc بطور پیش‌فرض دستورات رو از لیبل main شروع به خوندن می‌کنه. پس به این شکل هم می‌تونستیم فایل زیر رو داشته باشیم:

section .text global main main: nop

یک Makefile ساده می‌نویسم تا فرایند اسمبل و لینک کردن رو راحت‌تر کنم:

problem1: problem1.o ld -m elf_x86_64 problem1.o -o problem1 problem1.o: problem1.asm nasm -felf64 problem1.asm -o problem1.o

اجرای برنامه منجر به خطای زیر می‌شه:

$ make && ./problem1 [1] 365385 segmentation fault (core dumped) ./problem1

هممم! منطقیه که ما می‌دونیم دستورات‌مون کجا تمام شدن؛ سی‌پی‌یو اما بعد از اجرای خط اول، به روند اجرا ادامه می‌ده و رجیستر instruction pointer (ip) رو جلو می‌بره. پس لازمه در نقطه‌ای از برنامه خارج بشیم.

بیایین نگاهی بندازیم به لیست syscallهای لینوکس (اینجا)

linux syscalls table
linux syscalls table

هر syscall با یک عدد مشخص شده که با رجیستر rax بهش اشاره می‌کنیم و آرگومان‌های اون به ترتیب روی rdi، rsi و rdx نوشته می‌شه. سخت شد؟ مثال پایین رو ببینیم:

mov rax, 3 mov rdi, 666 syscall

به این شکل sys_close رو صدا زدیم (rax = 3) و فایلی با شناسه ۶۶۶ (rdi = 666) رو بستیم.

با ایده‌ای مشابه می‌تونم sys_exit رو صدا بزنم و از برنامه خارج بشم:

section .text global _start _start: mov rax, 60 mov rdi, 69 syscall

که اگه اجراش کنیم:

$ make && ./problem1

اینطوری می‌تونم exit code دستور قبلی رو چک کنم:

$ echo $? 69

خب! نگاهی به سوال اول بندازیم:

marhale3.github.io
marhale3.github.io

برای شروع سعی می‌کنم از ۱ تا ۱۰ بشمارم. قبلش ساختار کلی یک رجیستر رو در یک سیستم ۶۴ بیتی x86 ببینیم:

برای شمارش از ۱ تا ۱۰ یک رجیستر ۸ بیتی هم کافیه. برای شمارش اعداد کمتر از ۱۰۰۰ چطور؟ نه!

پس می‌تونم از ۱۶ بیت رجیستر استفاده کنم:

section .text global _start _start: + mov bx, 1 + loop: + inc bx + cmp bx, 10 + jne loop mov rax, 60 mov rdi, 69 syscall

حالا کافیه ببینیم کدوم اعداد به ۳ یا ۵ بخش‌پذیرن.

با نگاهی به instruction setهای x86 متوجه می‌شیم که باقی‌مانده تقسیم جفت‌رجیستر DX:AX به یک رجیستر ۱۶ بیتی داخل DX ذخیره می‌شه. می‌تونیم چنین ماکرویی برای محاسبه باقی‌مانده بنویسیم:

%macro mod 2 mov ax, %1 mov cx, %2 mov dx, 0 div cx %endmacro

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

section .text global _start _start: mov bx, 1 loop: + mov cx, 3 + mod bx, cx + cmp dx, 0 + je _divisible + + mov cx, 5 + mod bx, cx + cmp dx, 0 + jne _after + + _divisible: + _after: inc bx cmp bx, 10 jne loop + _exit: mov rax, 60 mov rdi, 69 syscall

حالا می‌تونیم اعدادی که بخش‌پذیر بودن رو داخل یک بافر جمع بزنیم و روی exit code خروجی بدیم:

section .text global _start _start: mov bx, 1 loop: mov cx, 3 mod bx, cx cmp dx, 0 je _divisible mov cx, 5 mov bx, cx cmp dx, 0 jne _after _divisible: + add [result], ebx _after: inc bx cmp bx, 10 jne loop _exit: mov rax, 60 + mov rdi, [result] syscall + section .data + result dd 0

با اجرای برنامه به عدد ۲۳ می‌رسیم که خروجی درستیه. روال مشابهی رو برای اعداد ۱ تا ۱۰۰۰ انجام می‌دیم:

$ make && ./problem1 $ echo $? 208

به عدد ۲۰۸ می‌رسیم که به نظر خروجی درستی نیست. با کمی جستجو متوجه می‌شیم که exit codeهای استاندارد لینوکس همیشه بین ۰ تا ۲۵۵ هستن.

می‌تونیم نتیجه رو مستقیم از مموری بخونیم. با gdb برنامه رو اجرا می‌کنم و روی لِیبل _exit متوقف می‌شم:

$ gdb ./problem1 (gdb) break _exit (gdb) run

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

(gdb) disass _divisible Dump of assembler code for function _divisible: 0x0000000000401032 <+0>: add DWORD PTR ds:0x402000,ebx

و مقدار داخل بافر رو دامپ می‌کنیم:

(gdb) print *0x402000 $1 = 233168




اسمبلیAssemblyalgorithmlinux
مطالبی که می‌خوانید حاصل ذهن مغشوش یک دانشجوی کامپیوتر بوده و مسئولیت هرگونه خطای احتمالی به عهده ساکنین سیاره "کپلر ۶۹ سی" می باشد!
شاید از این پست‌ها خوشتان بیاید