این برنامرو به چند روش میشه نوشت و برای مثال من ۳ نمونه از اونو اینجا مینویسم:
int main(void){
return 0;
}
و یا:
#include <stdlib.h>
int main(void){
exit(0);
}
و حتی:
#include <unistd.h>
int main(void){
_exit(0);
}
فعلا خیلی درگیر پیدا کردن تفاوت این کدها باهم نشید.
کاری که ما قراره انجام بدیم در اصل درک اتفاقاتیه که پشت صحنه میفتن و معمولا برنامه نویس از اونا بیخبره یا خیلی کم با اونا سرو کار داره. اینکه چقدر این اتفاقات قابل لمس هستن یا نه رابطه مستقیم با abstract و سطح زبانی که به اون کد مینویسید داره. هرچقدر اون زبان بیشتر به “فلز”(سخت افزار و مخصوصا CPU) نزدیک باشه، حجم کدی که مینویسید بیشتره و قاعدتا نگهداری(maintenance) و عیب یابی(debugging) اون هم سخت تره.
نکته: راجع به “سطح” زبان C خیلی ها اشتباه فکر میکنن. زبان C سطح پایین یا low-level نیست! زبان low-level در اصل به زبان ماشین گفته میشه و حتی دستورات اسمبلی هم بعضی وقتا نسبت یک-به-یک با زبان ماشین اون معماری ندارن! پس یادتون نره، هرچیزی بالاتر از اسمبلی باشه سطح پایین حساب نمیشه. حالا C دقیقا بین low-level و high-level قرار داره و دستتون رو کاملا باز میزاره که امکاناتی مثل struct یا function pointer داشته باشید و در عین حال هم بتونید تو دل کد Cتون کدهای اسمبلی inline کنید.
این برنامه(true
) بنظر من یونیکسی ترین برنامه دنیاست وفلسفه UNIX رو بزبان خیلی
ساده بیان میکنه: “.Do one thing and do it well”
وقتی این برنامه اجرا میشه، بلافاصله پایان کارشو به سیستم عامل اطلاع میده. همینو بس!
چجوری این اتفاق میفته؟ اگه از دید سیستم عامل و مخصوصا یونیکس بهش نگاه کنیم، عددی که یه برنامه به سیستم عامل برمیگردونه error code حساب میشه و در حالتی که هیچ خطایی در زمان اجرای برنامه رخ نداده باشه، این عدد باید صفر باشه و معنیش هم اینه که “برنامه مورد نظر کامل اجرا شد و پایان کار خودشو به سیستم عامل اطلاع داد”.
(که البته به این معنی نیست که کارشو درست انجام داده و هیچ باگ و یا ارروری در حین کار نداشته. این دوتا موضوع رو همیشه از هم جدا نگه دارید.)
تمرین: هر دستور(command) که از این به بعد اجرا کردید بلافاصله return valueشو ببینید. مثال:
$ ls
$ echo $?
یا
$ date ; echo $?
حالا ()exit
چیه خودش. در اصل(AT&T Unix Version 1) سیستم کال برای خروج و یا
ترمینیت(terminate) یه برنامه بوده و بنا به دلایلی به ()exit_
تغییر نام پیدا
میکنه و در حال حاظر هم لایبرری کال بحساب میاد.
این تابع(function) یه آرگومنت(argument) میگیره که اون هم status یا exit code برنامست. يادآوری: صفر به معنی موفق(successful) و هر عدد دیگه، چه مثبت چه منفی به معنی “خطا در زمان اجرای برنامه” بحساب میاد.
برای نوشتن برنامه true
بزبان اسمبلی نیاز به دونستن عدد سیستم کال exit
داریم
که میشه اونرو تو فایل syscall.h
پیدا کرد. البته ناگفته نماند که تو گنو/لینوکس
این فایل (تو بهترین حالت!) یه خط توش نوشته شده و اون هم یه فایل دیگرو اینکلود
میکنه و خلاصه اینکه عمر آدم دوپا کوتاه تر از این حرفاست و به همین دلیل من برای
نشون دادن عدد سیستم کال ها از syscall.h
سیستم عامل OpenBSD
استفاده میکنم:
...
...
/* syscall: "exit" ret: "void" args: "int" */
#define SYS_exit 1
/* syscall: "fork" ret: "int" args: */
#define SYS_fork 2
/* syscall: "read" ret: "ssize_t" args: "int" "void *" "size_t" */
#define SYS_read 3
/* syscall: "write" ret: "ssize_t" args: "int" "const void *" "size_t" */
#define SYS_write 4
/* syscall: "open" ret: "int" args: "const char *" "int" "..." */
#define SYS_open 5
...
...
همونطور که میبینید هر سیستم کال عدد خودشو داره و آرگومنت های اونا هم با هم متفاوتن. عدد سیستم کالی که ما نیاز داریم یکه(1) و آرگومنتی هم که میگیره از نوع عددیه.
برای استفاده از این سیستم کال، اولین کاری که باید انجام بدیم اینه که این دوتا عدد رو توی رجیسترهای مربوطه بنویسیم.
تو لینوکس ۳۲ بیتی، eax
عدد سیستم کال رو نگه میداره و آرگومنت تابع exit
هم
تو رجیستر ebx
نوشته میشه. اگه قرار باشه شبهه-کد(pseudo-code) اون رو بنویسیم
چیزی شبیه به (0)SYS_exit
یا حتی (0)1
از آب در میاد.
تو اسمبلر GNU
این دوتا عملیات رو به این شکل میشه انجام داد:
(فعلا چیزی رو جایی ننویسید و فقط به کدها نگاه کنید.)
movl $1, %eax
movl $0, %ebx
حالا برای اینکه اسمبلر(as
) بفهمه که برنامه از کجا باید شروع بشه باید این
دو خط رو هم اول فایل(سورس کد) بنویسیم:
.globl _start
_start:
حالا یه چیز میمونه و اون هم اینه که به کرنل دستور اجرای این کد یا سیستم کال
رو بدیم. تو لینوکس ۳۲ بیتی از دستور int 0x80
برای این کار استفاده میکنیم که
تو اسمبلر گنو اون رو بصورت int $0x80
مینویسیم.
فایلی که در نهایت قراره داشته باشیم اسمش exit.s
(است!) و محتوی اون هم:
.globl _start
_start:
movl $1, %eax
movl $0, %ebx
int $0x80
تبریک! کدمون آمادست و برای اجرایی(binary, executable) شدنش دوتا کار دیگرو باید انجام بدیم.
اولیش تبدیل کردن کد اسمبلی به آبجکت فایل(object file) و دومیش هم لینک کردن اون(آبجکت فایل)ه. به این دوتا فعلا مثل “کامپایل” نگاه کنید.
کار هایی که برای ساخت این برنامه تو اسمبلی انجام میدیم رو میشه بشکل زیر دسته بندی کرد:
۱) کد
- نوشتن کدهای مربوط به اسمبلر. (
globl.
و غیره.) - آماده کردن رجیستر های
eax
وebx
برای اجرای سیستم کالexit
. - فرستادن فرمان اجرای سیستم کال به کرنل.
۲) کامپایل
- اسمبل. (
as
) - لینک. (
ld
)
برای اسمبل کردن فایل به دیرکتوری مورد نظر(جایی که exit.s
اونجاست) میریم
و بعد هم با استفاده از اسمبلر گنو(as
) فایل اسمبلی(سورس کد) رو تبدیل به
آبجکت فایل میکنیم:
$ cd ~/asm
$ as -o exit.o exit.s
وقتی که آبجکت فایل آماده شد و فایل exit.o
با موفقیت ساخته شد، این فایل رو
با استفاده از لینکر(ld
) تبدیل به یه فایل اجرایی میکنیم:
$ ld -o exit exit.o
همین!
برای اجرای اون هم مثل بقیه برنامه ها عمل میکنیم و برای اینکه ببینیم کارشو درست انجام میده error code اون رو چک میکنیم:
$ ./exit
$ echo $?
حجم فایل های اجرایی رو باهم مقایسه کنیم، نظرتون چیه؟
- زبان اسمبلی:
452
- زبان سی:
7452
- برنامه
bin/true/
تو دبین ۹:30516
جالب تر کدیه که کامپایلر C زمان تبدیل کد C به اسمبلی میسازه:
(با استفاده از cc -S source.c $
میتونید کد اسمبلی سورس کد C تونو ببینید.)
.file "exit.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
leal 4(%esp), %ecx
.cfi_def_cfa 1, 0
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
.cfi_escape 0x10,0x5,0x2,0x75,0
movl %esp, %ebp
pushl %ebx
pushl %ecx
.cfi_escape 0xf,0x3,0x75,0x78,0x6
.cfi_escape 0x10,0x3,0x2,0x75,0x7c
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
subl $12, %esp
pushl $0
movl %eax, %ebx
call exit@PLT
.cfi_endproc
.LFE0:
.size main, .-main
.section .text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
.globl __x86.get_pc_thunk.ax
.hidden __x86.get_pc_thunk.ax
.type __x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB1:
.cfi_startproc
movl (%esp), %eax
ret
.cfi_endproc
.LFE1:
.ident "GCC: (Debian 6.3.0-18) 6.3.0 20170516"
.section .note.GNU-stack,"",@progbits
“چقدر خوب! اگه اینجوریه از این به بعد همه چیو به اسمبلی بنویسم!!!”
نه! نقطه.
درسته که کدهایی که تو اسمبلی مینویسیم efficient ترن(چه از نظر حجم و چه سرعت اجرا) ولی موضوعات دیگه مثل نگهداری(maintain) و عیب یابی(debugging) و حتی توسعه(development) یه برنامه هم هستن که هرکدومشون حتی بیرون از دنیای اسمبلی برای خودشون کابوس هایین.
موضوع portability هم بهتره کلا چیزی راجع بهش نگم و بعنوان کار عملی ازتون بخوام
یه لیست از معماری(architecture) هایی که سیستم عامل NetBSD
از اونها پشتیبانی
میکنه تهیه کنید و سعی کنید برای ۳ تا از اونها ساده ترین کد یا همون true
رو
بنویسید.
سلامت و شاد باشید