.text
.data
.bss
.section
.section .rodata
.byte 10 # یک بایت با مقدار ۱۰
.word 1000 # دو بایت با مقدار ۱۰۰۰
.long 123456
.quad 123456789
.octa ?
.ascii "hello"
.asciz "hello"
.string "hello" # مشابه .asciz
.space 14
.zero 14
.space 14, 0xFF # مقدار اولیه دادن فضاهای رزرو شده
.equ BUFFER_SIZE, 14
# .space BUFFER_SIZE
.set value, 100
.
#.size message, .-message
.include
.macro
.endm
مثال
.macro CLEAR reg
xor \reg, \reg
.endm
# CLEAR %rax
.if
.else
.endif
.ifdef
.ifndef
مثال
.ifndef DEBUG
...
.endif
.file
.loc
مثال
.file "main.c"
.loc 1 10 5
این اطلاعات به debuggerهایی مثل GDB کمک میکنند ارتباط بین assembly و source code را بفهمند.
.section
.type
.size
.symver
مثال
.section .rodata
.type foo, @function
🔵 .size دادهای در برنامه ایجاد نمیکند
بلکه به اسمبلر میگوید اندازهی یک symbol را در اطلاعات
symbol
آن فایل ثبت کند. این اندازه در symbol table ثبت میشود.
معمولا همراه با .type استفاده میشود.
.globl main
.type main, @function
main:
mov $0, %eax
ret
.size main, .-main
⬅ مثال
بعد از کامپایل با
$ readelf -s a.out
$ objdump -t a.out
تغییرات را بررسی کنید.
🔵 وقتی میگوییم یک داده باید 8-byte aligned باشد یعنی باید آدرس شروع آن مضربی از ۸ باشد.
0x1000 ✅
0x1008 ✅
0x1010 ✅
0x1018 ✅
0x1001 ❌
0x1005 ❌
0x100A ❌
ABI ممکن است alignment خاصی را الزام کند، حتی اگر خود CPU الزام نکند.
.balign 8 یعنی مکان بعدی را روی مرز ۸ بایتی قرار بده.
فاصلهی نقطهی فعلی را با مکان بعدی padding قرار میدهد.
.p2align 3 : میگوید که روی
مرز 2^3=8 align کن.
.balign 8, fill # .balign 8, 0x90 (nop)
.p2align 3, fill, max
در مثال دوم اگر padding بیشتر از max بشود اصلا alignment انجام نمیشود.
⬅ مثال
🔵 ارسال آرگومانهای سیستم کال و تابع اندکی با هم متفاوت است:
Function: RDI RSI RDX RCX R8 R9
Syscall: RDI RSI RDX R10 R8 R9
⬅ مثال. فایلهای مورد نیاز برای این برنامه lib.h و lib.c
💡 آرگومان هفتم و به بعدِ توابع روی stack ذخیره میشوند. مثال
از کد بالا به شیوهی زیر خروجی اسمبلی بگیرید و فایل تولید شده را بررسی کنید:
$ gcc -S -otest.s seventh_argument.c
❗ توضیحات زیر از هوش مصنوعی است.
در قرارداد فراخوانی توابع System V AMD64 ABI، رجیسترها از نظر حفظ شدن هنگام فراخوانی تابع به دو گروه تقسیم میشوند. این قواعد در لینوکس و معمولاً FreeBSD روی معماری x86-64 کاربرد دارند.
به این رجیسترها Callee-saved registers میگویند. اگر تابعی مقدار یکی از آنها را تغییر دهد، باید مقدار قبلی را پیش از بازگشت بازیابی کند.
| رجیستر | کاربرد معمول |
|---|---|
RBX |
رجیستر عمومی |
RBP |
معمولاً Frame Pointer |
R12 |
رجیستر عمومی |
R13 |
رجیستر عمومی |
R14 |
رجیستر عمومی |
R15 |
رجیستر عمومی |
RSP |
اشارهگر پشته؛ باید هنگام بازگشت به وضعیت مورد انتظار برگردد |
نکته: حفظ RSP کمی متفاوت است. تابع میتواند موقتاً آن را تغییر دهد؛ مثلاً با PUSH، POP یا تخصیص فضای پشته، اما هنگام بازگشت باید پشته را به وضعیت مورد انتظار برگرداند.
calc:
pushq %rbx
movq %rdi, %rbx
addq $5, %rbx
movq %rbx, %rax
popq %rbx
ret
در این مثال، مقدار قبلی RBX با PUSHQ ذخیره و پیش از بازگشت با POPQ بازیابی میشود.
به این رجیسترها Caller-saved registers یا Volatile registers میگویند. تابع فراخواندهشده میتواند مقدار آنها را تغییر دهد. اگر تابع فراخواننده به مقدار قبلی نیاز داشته باشد، خودش باید آن را حفظ کند.
| رجیستر | کاربرد معمول |
|---|---|
RAX |
مقدار بازگشتی تابع |
RCX |
رجیستر عمومی |
RDX |
رجیستر عمومی |
RSI |
رجیستر عمومی |
RDI |
رجیستر عمومی |
R8 |
رجیستر عمومی |
R9 |
رجیستر عمومی |
R10 |
رجیستر عمومی |
R11 |
رجیستر عمومی |
برای مثال:
movq $10, %rdi
call some_function
پس از اجرای CALL، نمیتوان فرض کرد که مقدار RDI هنوز 10 است؛ تابع some_function مجاز است آن را تغییر دهد.
فهرست بالا مربوط به رجیسترهای عمومی ۶۴ بیتی است. رجیسترهای دیگر نیز قواعد خاص خود را دارند.
RIP: آدرس دستور جاری را مشخص میکند و رجیستر عمومی برای ذخیرهٔ داده نیست.RFLAGS: پرچمهای وضعیت؛ بعضی از آنها ممکن است هنگام فراخوانی تابع تغییر کنند.XMM0–XMM15: رجیسترهای برداری که در System V AMD64 ABI از نوع Caller-saved هستند.YMM و ZMM: رجیسترهای برداری گستردهتر؛ قواعد آنها به ABI و افزونههای مورد استفاده مربوط است.MXCSR و رجیستر کنترل x87: قواعد حفظ آنها به بیتهای وضعیت و کنترل بستگی دارد؛ بنابراین نمیتوان همهٔ بیتهایشان را یکسان دستهبندی کرد.| گروه | رجیسترها |
|---|---|
| Callee-saved عمومی | RBX, RBP, R12–R15 |
| Caller-saved عمومی | RAX, RCX, RDX, RSI, RDI, R8–R11 |
| وضعیت ویژهٔ پشته | RSP باید هنگام بازگشت به وضعیت مورد انتظار برگردد |
اگر در تابع اسمبلی از RBX، RBP یا R12 تا R15 استفاده میکنی، مقدار قبلی آنها را حفظ کن.
برای RAX، RCX، RDX، RSI، RDI و R8 تا R11، تابع فراخواندهشده ملزم به حفظ مقدار قبلی نیست.
توجه: این قواعد مربوط به System V AMD64 ABI هستند. در قرارداد فراخوانی ویندوز ۶۴ بیتی، بعضی از این دستهبندیها متفاوتاند.
⬅ مثالی از نکتهی بالا همزمان با ترکیب کد C و اسمبلی. calc.c و calc.s
فایلهای بالا را به صورت زیر کامپایل و لینک کنید:
$ gcc -no-pie calc.c calc.s
⬅ فراخوانی تابع printf از کد اسمبلی
💡
طبق قرارداد فراخوانی
System V AMD64، برای فراخوانی
توابع variadic مانند printf، تعداد رجیسترهای برداری استفادهشده
برای آرگومانهای برداری را در
%al مشخص میکند؛
چون در مثال بالا آرگومان برداری نداریم، صفر میگذاریم یعنی:
xorl %eax, %eax
منظور از vector argument آرگومانی است که مقدارش در یک رجیستر برداری
vector register
قرار میگیرد؛ مثلا رجیسترهای XMM0 تا XMM15 در معماری X86-64.
این رجیسترها برای کار با دادههایی مثل اعداد اعشاری و چند مقدار به صورت
همزمان استفاده میشوند. این
کد
را با دستور زیر اجرا کنید تا خروجی اسمبلی بگیرید. سپس محل قرارگیری
متغیرهای تابع را بررسی کنید.
$ gcc -S vector_arguments.c
🔔
در قرارداد System V AMD64، آرگومانهای ممیز شناور
مانند float و double معمولاً از رجیسترهای XMM منتقل میشوند.
برای فراخوانی توابع variadic مانند printf، مقدار %al تعداد رجیسترهای
برداری استفادهشده برای آرگومانها را مشخص میکند؛
نه تعداد اعداد موجود در هر رجیستر.
⬅ مثال . کد اسمبلی این فایل را با gcc تولید کنید و محل قرارگیری آرگومانها و خروجی را مشاهده نمایید.
تعداد شانزده عدد general purpose register وجود دارد که بسیاری از آنها نامهای تاریخی و منظور ویژه دارند ولی امروزه عموما general purpose محسوب میشوند.
%rax %rbx %rcx %rdx %rsi %rdi %rbp %rsp %r8 ... %r15
%eax %ebx %ecx %edx %esi %edi %ebp %esp %r8d ... %r15d
%ax %bx %cx %dx %si %di %bp %sp %r8w ... %r15w
%al %bl %cl %dl %sil %dil %bpl %spl %r8b ... %r15b
%ah %bh %ch %dh none none none none none ... none
%rax accumulator / return value
%rbx callee-saved general register
برای تعریف آرایه به صورت زیر عمل میکنیم:
arr4byte:
.long 1, 2, 3, 4
arr8byte:
.quad 1, 2, 3, 4
arrbyte:
.zero 10 # 10 zero nytes at arrbyte
arrstr:
.string "hello"
Labels are replaced by the assembler/linker with an address.
یک شیوهی آدرسدهی حافظه به این صورت است:
displacement(base, index, scale) . scale باید ۱، ۲، ۴ و یا ۸ باشد
و به صورت زیر تفسیر میشود:
base + index*scale + displacement
در X86-64 معمولا از RIP-relative addressing برای متغیرهای سراسری استفاده
میکنیم: symbol(%rip) .
چند مثال از دستور mov کپی شده از
اینجا
movq (%rbx), %rax # load 8 bytes from address in RBX into RAX
movq %rbx, var(%rip) # store RBX into global variable var
movl -4(%rsi), %eax # load 4 bytes from (RSI-4) into EAX (zero-extends into RAX)
movb %cl, (%rsi,%rax,1) # store 1 byte (CL) to address RSI+RAX
movq (%rsi,%rbx,4), %rdx # load 8 bytes from address RSI + 4*RBX into RDX
suffix هایی که به instruction ها وصل میشوند طول oprand ها را مشخص میکنند. این suffix ها عبارتند از:
b # 1 byte
w # 2 byte
l # 4 byte
q # 8 byte
وقتی که طول عملوندها ابهام برانگیز است صراحتا از suffix ها استفاده کنید:
movb $2, (%rbx) # store 1 byte
movw $2, (%rbx) # store 2 bytes
movl $2, (%rbx) # store 4 bytes
movq $2, (%rbx) # store 8 bytes
machine instruction ها به سه دستهی بزرگ تقسیم میشوند:
🔵
سادهترین دستور، دستور mov است که برای انتقال استفاده میشود.
⛔ mov کردن memory-to-memory امکانپذیر نیست. برای این کار از یک
رجیستر به عنوان کمک استفاده کنید.
mov{b,w,l,q} <reg>, <reg>
mov{b,w,l,q} <reg>, <mem>
mov{b,w,l,q} <mem>, <reg>
mov{b,w,l,q} <con>, <reg> # immediate constant to register
mov{b,w,l,q} <con>, <mem> # immediate constant to memory
# examples
movq %rbx, %rax # copy RBX into RAX
movb $5, var(%rip) # store 5 into the byte at var
movl $0, %eax # set EAX to 0 (also clears upper half of RAX)
🔵
دستور push هشت واحد از رجیستر %rsp کم میکند و سپس operand داده شده را
در (%rsp) ذخیره میکند:
pushq <reg64>
pushq <mem>
pushq <con>
# examples
pushq %rax
pushq var(%rip)
🔵
دستور pop هشت بایت از stack (%rsp)
واکشی میکند و آن را در operand داده شده که memory یا رجیستر است قرار
میدهد و سپس هشت واحد به %rsp اضافه میکند.
popq <reg64>
popq <mem>
# examples
popq %rdi
popq (%rbx)
🔵
دستور lea یک آدرس را محاسبه میکند و آدرس محاسبه شده را در رجیستر
داده شده قرار میدهد. توجه کنید که هیچ دیتایی از حافظه خوانده
نمیشود. معمولا برای pointer arithmetic و RIP-relative addressing
استفاده میشود.
leaq <mem>, <reg64>
leaq (%rbx,%rsi,8), %rdi # RDI = RBX + 8*RSI
leaq var(%rip), %rax # RAX = &var
🔴
دستور add دو operand را جمع میکند و نتیجه را در operand دوم
قرار میدهد. حداکثر یکی از operand ها میتواند memory باشد.
addq $10, %rax # RAX = RAX + 10
addb $10, (%rax) # add 10 to the byte at address RAX
🔴
دستور sub عملوند اول را از عملوند دوم کم میکند و نتیجه را در عملوند دوم
قرار میدهد.
subq $216, %rax
sub %ah, %al # still valid for 8-bit sub-registers
🔴
inc و dec عملوند داده شده را یکی زیاد و یا یکی کم میکند.
decq %rax
incl var(%rip) # add one to a 32-bit integer at var
💡
processor رجیستری به نام instruction pointer %rip را مدیریت میکند.
مقدار این رجیستر که ۶۴ بیت است به current instruction اشاره میکند.
نمیتوان این مقدار را مستقیما تغییر داد بلکه این مقدار توسط
control-flow instruction ها تغییر میکند.
⚪ دستور jmp در واقع یک پرش بدون شرط به یک label یا
indirect target
است.
jmp begin
jmp *%rax # indirect jump to address in RAX
⚪
خانواده دستورهای j<condition> پرش بر مبنای فلگهای ست شدهی
دستور قبلی (اغلب cmp) است. شرطهای رایج عبارتند از:
jejnejgjgejljlecmpq %rbx, %rax
jle done
⚪
دستور cmp مانند دستور sub ولی برای flag هاست و نتیجه را دور میریزد.
(یعنی flag ها را ست میکند.)
cmpb $10, (%rbx)
je loop
⚪
دستور call هشت بایت آدرس بازگشت (%rip بعدی) را روی stack قرار میدهد
و به target داده شده پرش میکند. دستور ret آن
آدرس را pop میکند و به آن مکان برمیگردد.
⭐ استاندارد System V AMD64 ABI بیان میکند که اولین ۶ آرگومان تابع که integer یا pointer باشند در رجیسترها پاس میشوند:
| arg | reg |
|---|---|
| arg1 | %rdi |
| arg2 | %rsi |
| arg3 | %rdx |
| arg4 | %rcx |
| arg5 | %r8 |
| arg6 | %r9 |
⭐ آرگومان هفتم به بعد روی stack پاس میشود. return value های از نوع
integer و pointer روی رجیستر %rax قرار داده میشوند.
⭐ رجیسترها به دو دستهی caller-saved و callee-saved تقسیم میشوند:
Callee-saved %rbx %rbp %r12 %r13 %r14 %r15
Caller-saved %rax %rcx %rdx %rsi %rdi %r8 %r9 %r10 %r11
⭐ Before executing a call, the stack pointer %rsp must be aligned to a 16-byte boundary. Because call pushes an 8-byte return address, a typical callee entry sees %rsp misaligned by 8 and fixes alignment in its prologue as needed.
⭐
فرض کنید فراخوانی تابعی به صورت myFunc(p1, 216, *p3) داشته باشیم. p1 در
%rax باشد و %rbx
هم حاوی آدرسی باشد که آرگومان سوم تابع است:
movq %rax, %rdi # arg1 = p1
movq $216, %rsi # arg2 = 216
movq (%rbx), %rdx # arg3 = *p3
# ensure 16-byte stack alignment here if needed
call myFunc # return value in %rax
⭐
پیادهسازی یک تابع ساده که arg1 + arg2 + arg3 را برمیگرداند:
.text
.globl myFunc
.type myFunc, @function
myFunc:
# Prologue
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp # space for locals, keeps stack aligned
# Body (args in %rdi, %rsi, %rdx)
movq %rdx, -8(%rbp) # local = arg3
addq %rsi, -8(%rbp) # local += arg2
movq %rdi, %rax # rax = arg1
addq -8(%rbp), %rax # rax += local
# Epilogue
leave
ret
⬅ مثال. این دو فایل را با هم کامپایل و لینک کنید. print_string.s و print_string_test.c
🔔 در مثال بالا به استفاده از رجیسترهای callee-saved توجه کنید.
🔵
در GCC، عبارت __attribute__((naked)) یک ویژگی مخصوص تابع است که به کامپایلر
میگوید برای آن تابع کد معمول مربوط به ورود و خروج تابع
(Function Prologue/Epilogue)
را تولید نکند.
کد زیر را یک بار با بودن خط attribute و یک بار با حذف آن کامپایل کنید و فایل اجرایی تولید شده را با ابزار objdump بررسی کنید و تفاوت را ببینید.
#include <stdio.h>
__attribute__((naked))
int
main(void)
{
return 0;
}
🔵 به دو صورت میتوانیم مستقیما در C از اسمبلی استفاده کنیم: