اینجا هستید : safarionline.ir / notes / sysvabi

اسمبلر gas

.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     # مقدار اولیه دادن فضاهای رزرو شده

تعریف symbol و ثابت

.equ BUFFER_SIZE, 14
# .space BUFFER_SIZE
.set value, 100

current location

.
#.size message, .-message

include و macro

.include
.macro
.endm

مثال

.macro CLEAR reg
    xor \reg, \reg
.endm

# CLEAR %rax

شرط‌ها و کنترل و امکانات preprocessing

.if
.else
.endif
.ifdef
.ifndef

مثال

.ifndef DEBUG
    ...
.endif

اطلاعات debugging

.file
.loc

مثال

.file "main.c"
.loc 1 10 5

این اطلاعات به debuggerهایی مثل GDB کمک می‌کنند ارتباط بین assembly و source code را بفهمند.

بخش‌های خاص ELF

.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

در قرارداد فراخوانی توابع 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 ها به سه دسته‌ی بزرگ تقسیم می‌شوند:

  1. data movement
  2. arithmetic and logic
  3. control flow

🔵 ساده‌ترین دستور، دستور 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) است. شرط‌های رایج عبارتند از:

  1. je
  2. jne
  3. jg
  4. jge
  5. jl
  6. jle
cmpq %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 از اسمبلی استفاده کنیم:

  1. inline assembly. example
  2. extended inline assembly
© کلیه‌ی حقوق برای safarionline.ir محفوظ است.