~/ زبان اسمبلی:‌ true

این برنامرو به چند روش میشه نوشت و برای مثال من ۳ نمونه از اونو اینجا مینویسم:

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) و دومیش هم لینک کردن اون(آبجکت فایل)ه. به این دوتا فعلا مثل “کامپایل” نگاه کنید.

کار هایی که برای ساخت این برنامه تو اسمبلی انجام میدیم رو میشه بشکل زیر دسته بندی کرد:

۱)‌ کد

۲) کامپایل

برای اسمبل کردن فایل به دیرکتوری مورد نظر(جایی که exit.s اونجاست) میریم و بعد هم با استفاده از اسمبلر گنو(as) فایل اسمبلی(سورس کد) رو تبدیل به آبجکت فایل می‌کنیم:

$ cd ~/asm
$ as -o exit.o exit.s

وقتی که آبجکت فایل آماده شد و فایل exit.o با موفقیت ساخته شد، این فایل رو با استفاده از لینکر(ld) تبدیل به یه فایل اجرایی میکنیم:

$ ld -o exit exit.o

همین!

برای اجرای اون هم مثل بقیه برنامه ها عمل میکنیم و برای اینکه ببینیم کارشو درست انجام میده error code اون رو چک میکنیم:

$ ./exit
$ echo $?

حجم فایل های اجرایی رو باهم مقایسه کنیم، نظرتون چیه؟

جالب تر کدیه که کامپایلر 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 رو بنویسید.

سلامت و شاد باشید