NASM در لینوکس
برنامه نویسی با زبان اسمبلی میتواند سرگرمکننده باشد. به شما درک بسیار عمیقتری در مورد فعالیتهای داخلی پردازنده و کرنل میدهد. این مقاله برای برنامه نویسان تازهکار اسمبلی که نمیتوانند توجیه کنند چرا که کاری به عنوان ماسوچیستیک (masochistic) را به عنوان نوشتن کل برنامه به زبان اسمبلی انجام میدهند تهیه شده. اگر شما در حال حاضر به یک یا چند زبان برنامه نویسی آشنایی ندارید هیچ دلیلی بر خواندن این مقاله ندارید. خیلی از ساختارها در شرایط زبان C توضیح داده میشوند. شما همچنین باید با خط فرمان NASM آشنا باشید. هیچ دلیلی برای یاد گرفتن آنها در اینجا وجود ندارد.
بنابراین شما میخواهید یک برنامه بنویسید که واقعاً یک کاری انجام میدهد. “Hello World! “ دیگر مثل قبل برش ندارد. ابتدا، یک نگاه بر قسمتهای مختلف یک برنامه اسمبلی میاندازیم: (برای مستندسازی بدون حاشیه، راهنمای NASM جای مناسبی برای ماست)
بخش دادهها
ویرایشاین بخش برای تعریف ثابت هاست، مثل اسم فایلها یا سایز بافر، این اطلاعات زمان اجرا تغییر نمیکنند. مستندات NASM توصیفهای خوبی برای چگونگی استفاده از دستورات پایگاه داده DB و غیره در این قسمت استفاده میشوند دارد.
بخش bss
ویرایشاینجا قسمتی است که شما متغیرهایتان رو اعلان میکنید
آنها شبیه به چنین چیزی میباشند
filename: resb 255 ; REServe 255 Bytes
number: resb 1 ; REServe 1 Byte
bignum: resw 1 ; REServe 1 Word (1 Word = 2 Bytes)
longnum: resd 1 ; REServe 1 Double Word
pi: resq 1 ; REServe 1 double precision float
morepi: rest 1 ; REServe 1 extended precision float
قسمتی متنی
ویرایشاینجا در حقیقت همان جایی است که کدهای اسمبلی نوشته میشوند. عبارت «کدهای خود اصلاح کننده» به معنی برنامهای است که در هنگام اجرا خود این قسمت را تغییر میدهد.
نکته بعدی که ممکن است در نگاه کردن به منبع برنامههای اسمبلی متوجه شوید، همیشه “Global_Start” یا چیزی مشابه به این در ابتدای قسمت متنی دیده شود. این روش برنامههای اسمبلی است که به کرنل بگوید کی اجرای برنامه آغاز میشود. این به طور دقیق، بنا بر دانش من، مثل تابع main() در زبان C میباشد غیر از آنکه اصلاً یک تابع نمیباشد بلکه یک نقطه شروع است
پشته و متفرقه
ویرایشهمچنین مانند زبان C، کرنل محیط را به همراه متغیرهای آن راه میاندازد، و آرگومانها و تعداد آنها. در موردی که فراموش کردهاید **argv همان رشته آرایه هاست که به عنوان آرگومان به برنامه ارسال میگردد و argc تعداد آن هاست. اینها در پشته (کتابخانهها) قرار داده شده. اگر علم کامپیوتر۱۰۱ را مطالعه کرده باشید، یا هر مقدمه علوم کامپیوتر دیگری را میدانید که پشته (کتابخانه) چیست. یک روش ذخیرهسازی اطلاعات به طوری که آخرین چیزی که در آن قرار میدهید اولین چیزی است که خارج میشود. این خوب است، ولی اکثر مردم فهمی از ارتباط آن با با کامپیوتر خود ندارند. «پشته» همانطور که ارجاع داده میشود، به منظور همان RAM شما میباشد؛ و این همان RAM شما میباشد که به این روش سازماندهی میکند وقتی شما چیزی را به درون پشته میگذارید، تمام کاری که شما در حال انجام آن هستید ذخیرهسازی چیزی در RAM میباشد؛ و وقتی شما به چیزی در پشته اشاره میکنید در حقیقت آخرین چیزی که در آنجا ذخیره کردهاید را بازیابی میکنید.
خب اکنون نگاهی به کدها میاندازیم که شما خواهید دید.
section.text ; declaring our.text segment
global _start ; telling where program execution should start
_start: ; this is where code starts getting exec'ed
pop ebx ; get first thing off of stack and put into ebx
dec ebx ; decrement the value of ebx by one
pop ebp ; get next 2 things off stack and put into ebx
pop ebp
این کدها چه کاری میکنند؟ به سادگی اولین آرگومان را در ebx register قرار میدهد. بیاید برنامه را در خط فرمان به این صورت اجرا کنیم:
$. /program 42 A
وقتی که ما در خط شروع هستیم، پشته شبیه به همچین چیزی است:
| 3 | The number of arguments, including argv[0],
| | which is the program name
|"program"| argv[0]
| "42" | argv[1] NOTE: This is the character "4" and "2",
| | not the number 42
| "A" | argv[2]
بنابر این اولین دستور، ۳ را برمیدارد، و آن را درebx قرار میدهد؛ و سپس ما آن را یک واحد کم میکنیم به خاطر اینکه نام برنامه واقعاً یک آرگومان نیست.
با توجه به اینکه بعداً میخواهید از شمارش آرگومانها استفاده کنید یا خیر، در نظر میگیرد که بقیه آرگومانها را در یک جا ثبت کنید (در یک حوزه) یا در حوزههای متفاوت.
اکنون “pop ebp” نام برنامه را در ebp میگذارد، و “pop ebp” بعدی آن را بازنویسی میکند؛ و "۴۲" را درون ebp قرار میدهد. آخرین مقدار (ارزشی) که در ebp میباشد محفوظ نیست و هنگامی که آنرا از پشته بپرانید برای همیشه رفتهاست.
انجام کارهایی جذاب تر
ویرایشتکون بخورید، شما دقیقاً چگونه با بقیه سیستم در تعامل هستید؟ شما میدونید که چگونه پشته را دستکاری کنید، ولی چگونه زمان حاضر را دریافت کنید، یا یک دایرکتوری ایجاد کنید یا یک فعالیت را چندشاخه کنید یا هر چیز خارق العاده دیگری که در توان یونیکس میباشد؟ من خوشحالم که شما را به “System Call” معرفی میکنم. فراخوانی سیستم مترجمی است که اجازه میدهد برنامههای کاربری (همان چیزی که شما نوشتهاید)، با کرنل صحبت کند، چه کسی در زمینه کرنل است البته. هر فراخوانی سیستم شماره منحصر به فردی دارد، که میتوانید آنرا در حوزهeax قرار دهید، و به کرنل بگویید «بلند شو و این را انجام بده»، و امیدوارانه انجام میدهد. اگر فراخوانی سیستم آرگومانهایی دریافت کند، که اغلب میکند، اینها به ebx , ecx , edx , esi , edi , ebp به ترتیب میروند.
بعضی کدهای مثال همیشه کمک میکنند:
mov eax,۱ ; the exit syscall number
mov ebx,۰ ; have an exit code of 0
int 80h ; interrupt 80h, the thing that pokes the kernel
; and says, «do this»
کد قبلی معادل return ۰; در پایان تابع اصلی میباشد. قبوله هنوز خیلی کاربرد نداره، ولی بالاخره رسیدیم.
مثالهای پرکاربردتر:
pop ebx ; argc
pop ebx ; argv[0]
pop ebx ; the first real arg, a filename
mov eax,5 ; the syscall number for open()
; we already have the filename in ebx
mov ecx,0 ; O_RDONLY, defined in fcntl.h
int 80h ; call the kernel
; now we have a file descriptor in eax
test eax,eax ; lets make sure it is valid
jns file_function ; if the file descriptor does not have the
; sign flag (which means it is less than 0)
; jump to file_function
mov ebx,eax ; there was an error, save the errno in ebx
mov eax,1 ; put the exit syscall number in eax
int 80h ; bail out
حالا داریم به یک جاهایی میرسیم. شما باید متوجه شوید که در اسمبلی وووودوووو یا جادوویی وجود نداره، فقط تودهای از دستورات سفت و سخت. اگر بدانید که دستورات چگونه کار میکنند، شما تقریباً میتوانید هر کاری بکنید. با اینکه خودم امتحانش نرکردم. کدنویسی شبکه را در اسمبلی دیدهام. کنسولهای گرافیکی و بله حتی ویندوزX در اسمبلی.
پس از کجا تمامی معناهای که برای فراخوانیهای سیستم مختلف هست را پی ببریم؟ خب اول، شمارهها در asm/unistd.h در لینوکس، و sys/syscall.h در BSD لیست شدهاند. برای پی بردن به اطلاع در مورد هر یک، مثل اینکه هر کدام چه آرگومانهایی را دریافت میکنند و چه مقادیری را باز میگردانند، بی وقفه به آنها نگاه کنید. من شما را برای فراخوانی سیستم بعدی که به آن پی خواهیم برد نگه میدارم، read().
«بخوان مرد» دقیقاً همان چیزی را که میخواستید به شما نداده، داده؟ این به خاطر اینست که راهنمای برنامه و راهنمای شل قبل از راهنمای برنامه نویسی نمایش داده میشوند. اگر از بش استفاده میکنید، شما احتمالاً اکنون به BASH_BUILTINS (۱) نگاه میکنید. اکنون شما باید به قسمتهایی مثل SYNOPSIS , DESCRIPTION , DESCRIPTION , ERRORS و چندتای دیگر نگاه کنید. اینها مهمترینها هستند. به synopsis یک نگاه بیاندازید، احتمالاً چنین شکل و شمایلی دارد:
ssize_t read(int fd, void *buf, size_t count);
NOTE: ssize_t and size_t are just integers.
اولین آرگومان تشریح کننده فایل میباشد، که توسط بافر دنبال میشود، و سپس چگونه بایتهای زیادی خوانده شوند، که هرچه قدر که بافر طولانی باشد باید باشند، برای بهترین کارایی، ۸۱۹۲بایت را استفاده کنید، که ۸k میباشد، بنا بر شمارش شما. بافر خود را از مضرب این بگذارید، ۸۱۹۲ مناسبه. اکنون شما میدانید که چه چیزهایی را در حوزه هایتان بگذارید. قسمت Return Value را بخوانید، شما باید متوجه شوید که read() چگونه تعداد بایتهایی را که میخواند را باز میگرداند، ۰ برای EOF، و -۱ برای خطاها.
file_function:
mov ebx,eax ; sys_open returned file descriptor into eax
mov eax,3 ; sys_read
; ebx is already setup
mov ecx,buf ; we are putting the ADDRESS of buf in ecx
mov edx,bufsize ; we are putting the ADDRESS of bufsize in edx
int 80h ; call the kernel
test eax,eax ; see what got returned
jz nextfile ; got an EOF, go to read the next file
js error ; got an error, bail out
; if we are here, then we actually read some bytes
اکنون ما یک تیکه از فایل را خواندهایم (تا ۸۱۹۲ بایت) و جایگزین کردهایم چیزی که شما در C آن را آرایه مینامید. اکنون چه کاری میتوانید بکنید؟ خب، اولین چیزی که به ذهن میرسد این است که آن را چاپ کنید. یک ثانیه صبر کنید، در بخش دوم هیچ صفحهای برای printf وجود ندارد. خب چه میشود؟ خب، printf یک تابع کتابخانهای است که توسط کتابخانههای C اجرا میشود. شما مجبورید که کمی بیشتر کنکاش کنید، و از write() استفاده کنید، خب اکنون به همان صفحه نگاه کنید، write() به درون توصیف دهنده فایل مینویسد. خب این به چه درد من میخوره؟!!!!!! من میخوام که اون رو چاپ کنم (نمایشش بدم)!! به یاد بیاورید، همه چیز در یونیکس یک فایل میباشد، بنابراین تمام کاری که شما باید انجام دهید این است که به STDOUT بنویسید، از /usr/include/unistd.h، که به عنوان ۱ تعریف میشوند. خب تکه دیگر کد شبیه به این است:
mov edx,eax ; save the count of bytes for the write syscall
mov eax,4 ; system call for write
mov ebx,1 ; STDOUT file descriptor
; ecx is already set up
int 80h ; call kernel
; for the program to properly exit instead of segfaulting right here
; (it doesn't seem to like to fall off the end of a program), call
; a sys_exit
mov eax,1
mov ebx,0
int 80h
آنچه که شما اکنون نوشتهاید اساساً یک “cat” میباشد، به جز اینکه آن فقط ۸۱۹۲بایت اولی را چاپ میکند.
قابل حمل بودن
ویرایشدر قسمت بعدی، چگونگی فراخوانی کرنل در لینوکس با NASM را مشاهده میکنید. اگر در آینده هرگز به سراغ استفاده از سیستم عامل دیگری نمیروید خوب است، و شما از جستجوی اعداد کرنل سیستم لذت خواهید برد، ولی خیلی عملی نیست، و شدیداً غیرقابل حمل (روی سیستمهای دیگر نمیشود استفاده کرد). چه کار بکنیم؟ یک بسته عظیم کوچک به نام اسموتیلز که توسط کنستانتین بلدیشو آغاز گشته وجود دارد، کسی که سایت linuxassembly.org را راهاندازی کرد.
اگر شما تمام اسناد خوب را در آن سایت نخواندهاید، میتواند قدم بعدی شما باشد. اسمولیتز یک محیط ساده و قابل حمل را برای فراخوانی سیستم در هر نوع سیستم عامل یونیکس که استفاده میکنید (و حتی برای BeOS نیز پشتیبانی دارد) را فراهم میکند. حتی اگر شما مایل به استفاده از این فواید یونیکس که در اسمبلی باز نویسی شدهاند نیستید، اگر شما میخواهید یک کد NASM که قابل حمل است بنویسید، بهتر است که شما از فایلهای سروند آن استفاده کنید تا آنکه خودتان یکی را پیادهسازی کنید. با اسمولیتز، کد شما چنین شکلی خواهد گرفت:
%include «system.inc» ; all the magic happens here
CODESEG ;.text section
START: ; always starts here
sys_write STDOUT,[somestring],[strlen]
END ; code ends here
این بیشتر قابل خواندن است، سپس هر کاری با شماره فراخوانی سیستم انجام میدهید و در سیستم عاملهای Linux , FreeBSD, OpenBSD , NetBSD , BeOS , و چند سیستم عامل دیگر که کمتر شناخته شدهاند قابل حمل (استفاده) خواهد بود. شما میتوانید اکنون فراخوانی سیستم را با استفاده از اسم بکار ببرید، و از ثابتهای استانداردی مثل STDOUT یا O_RDONLY استفاده کنید، درست مثل C. دستور “#include” درست همانطور که درC کار میکند، کارمیکند و محتوای آن فایل را شامل منبع میکند.
برای یادگیری بیشتر در مورد اسمولیتز، Asmulits – HOWTO را بخوانید، که در doc/ directory منبع میباشد. همچنین، برای گرفتن آخرین منبع، از این دستورات استفاده کنید:
export CVS_RSH=ssh
cvs -d:pserver:anonymous@cvs.linuxassembly.org:/cvsroot/asm login
cvs -z۳ -d:pserver:anonymous@cvs.linuxassembly.org:/cvsroot/asm co asmutils
این دستور آخرین و جدیدترین منبع را دانلود کرده و به درون “asmulits” که زیرشاخه، دایرکتوری فعلی است ذخیره میکند. نگاهی به برنامههای سادهتر بیاندازید، مثل cat , Sleep , In , head or mount، میبینید که هیچ چیز سختی درباره آنها وجود ندارد. head اولین برنامه اسمبلی من بود، من همچنین توضیحات زیادی را ساختم، بنابراین جای خوبی برای شروع میباشد.
اشکال زدایی
ویرایشStrace قطعاً دوست شماست. آن ساده ترین ابزار برای اشکال زدایی برنامه شماست. اکثر اوغات که در اسمبلی برنامه نویسی میکنید، جدای از خطاهای نحوی، خطاهای سگمنتی خواهید داشت. این اطلاعات صفر مفیدی را برای شما فراهم میکند. با Strace حداقل شما میبینید که فراخوانی سیستم شما گیر کردهاست. مثال:
$ strace. /cal2
execve(«. /cal2», [«. /cal2»], [/* 46 vars */]) = 0
read(1, "", 0) = 0
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
حال شما میدانید که به کجا نگاه بیاندازید؛ ولی شروع به آزار دهنده بودن میکند وقتی شما چندین (خطای) اسمبلی خالص داشته باشید که نمیتواند نشان دهد. این هنگامی است که gdb به بازی میآید. اطلاعات بسیار خوب و مفیدی درباره استفاده gdb و فعال کردن اشکال زدایی در NASM در Asmulits – HOWTO وجود دارد، خب من آنها رو اینجا نمیگذارم. برای یک راه حل سریع و کثیف، میتوانید کار زیر را انجام دهید:
٪define notdeadyet sys_write STDOUT,0,__LINE__
واضح است که این برای اشتباهات پیچیده و منابع چندگانه عملی نیست اما خیلی خوب برای اشتباهات ناشی از بی دقتی وقتی که به تازگی شروع کردهاید کار میکند. مثال:
$ strace. /cal2
execve(«. /cal2», [«. /cal2»], [/* 46 vars */]) = ۰
write(1, NULL, 16) = 16
write(1, NULL, 26) = 26
write(1, NULL, 41) = 41
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
اکنون ما میدانیم که هنوز در خط ۴۱ هستیم، و برنامه به دنبال آن است
اکنون نوبت شماست که درون سیستم عامل خود را جستجو کنید، و افتخار کنید در فهمیدن این که چه اتفاقهایی در جریان است
برای دریافت اطلاعات بیشتر قرار داده شدهاند:
Linux Assembly
Assembly Programming Journal
Mammon 's textbase -
Art Of Assembly
Sandpile
NASM
ضمیمه: پرشها
وقتی که در ابتدا شروع به نگاه کردن به کدهای منبع اسمبلی کردم، به دستورات دیوانه واری همانند "jnz" بر خوردم که همگی شبیه باتلاقی از دستورات بود. اما بعد از مدتی در نهایت به آن چیزی که واقعاً بودند دست یافتم. در اصل آنها فقط “if statements” میباشند که آنرا میدانید و عاشق آن هستید؛ که در حوزه EFLAGS کار میکند. حوزه EFLAGS چیست؟ فقط یک حوزه با کلی بیت که صفر یا یک میشوند، بنابر مقایسه قبلی که کد انجام دادهاست
بعضی کدها که صفحه نمایش را تنظیم میکنند:
mov eax,82
mov ebx,69
test eax,ebx
jle some_function
بر روی کره خاکی “jle” چیست؟! چرا آن “Jump if Less than or Equal” میباشد؟! اگرeax کمتر یا مساوی ebx باشد اجرای کد به some_function میرود. اگر نبود، به ادامه خود میپردازد. اینجا لیستی از قسمتی از اسمبلی را که برای من هم در ابتدا مرموز به نظر میرسید را نور افشانی میکند (معلوم میکند). بعضی از اینها به صورت منطقی یکی هستند، ولی در بعضی موارد قابل درک تر از بقیه میباشد.
Jump Meaning Signedness (S or U)
ja | Jump if above | U
jae | Jump if above or Equal | U
jb | Jump if below | U
jbe | Jump if below or Equal | U
jc | Jump if Carry |
jcxz | Jump if CX is Zero |
je | Jump if Equal |
jecxz | Jump if ECX is Zero |
jz | Jump if Zero |
jg | Jump if greater | S
jge | Jump if greater or Equal | S
jl | Jump if less | S
jle | Jump if less or Equal | S
jmp | Unconditional jump |
jna | Jump Not above | U
jnae | Jump Not above or Equal | U
jnc | Jump if Not Carry |
jncxz | Jump if CX Not Zero |
jne | Jump if Not Equal |
jng | Jump if Not greater | S
jnge | Jump if Not greater or Equal | S
jnl | Jump if Not less | S
jnle | Jump if Not less or Equal | S
jno | Jump if Not Overflow |
jnp | Jump if Not Parity |
jns | Jump if Not signed |
jnz | Jump if Not Zero |
jo | Jump if Overflow |
jp | Jump if Parity |
jpe | Jump if Parity Even |
jpo | Jump if Parity Odd |
js | Jump if signed |
jz | Jump if Zero |
انتقادات و پیشنهادات با کمال میل پذیرفته میشوند، امیدوارم که این مقاله برای برنامه نویسان اسمبلی یونیکس مورد استفاده قرار بگیرد
آخرین نسخه فعلی این سند باید در leto.net/writing/nasm.php موجود باشد