Foundationপ্রথম নীতি থেকে
LEVEL 3লেসন ৭/১২মাঝারি৫০ মিনিট

Addressing ও Pointer Arithmetic — ঠিকানার গণিত

Addressing and Pointer Arithmetic in Assembly

x86-64-এর memory operand একটা ছোট্ট সমীকরণ — base + index×scale + displacement। এই একটামাত্র প্যাটার্ন দিয়েই array indexing, pointer arithmetic, struct field access, আর array-of-struct access — সবকিছু এনকোড হয়। এই লেসনে আমরা computer-architecture module-এর addressing-mode তত্ত্বকে সরাসরি real x86-64 syntax-এ নামিয়ে আনব, আর দেখব offsetof-এর সংখ্যাটা ঠিক কীভাবে একটা instruction-এর displacement field হয়ে ওঠে।

এই লেসন শেষে আপনি পারবেন

  • x86-64-এর disp(base, index, scale) addressing syntax পড়তে ও effective address হাতে গণনা করতে পারবেন
  • C-এর typed pointer arithmetic (ptr + i) কীভাবে compiler-এর element-size scaling-এ রূপান্তরিত হয় তা ট্রেস করতে পারবেন
  • struct field access কীভাবে base+fixed-displacement addressing-এ পরিণত হয়, আর সেই displacement মান কোথা থেকে আসে তা ব্যাখ্যা করতে পারবেন
  • array-of-struct access-এ base+index×scale+displacement — চারটা উপাদান একসাথে কীভাবে এক instruction-এ কাজ করে তা বিশ্লেষণ করতে পারবেন
  • lea instruction কেন শুধু ঠিকানা গণনার জন্য না, সাধারণ integer arithmetic-এর জন্যও ব্যবহৃত হয় তা ব্যাখ্যা করতে পারবেন
  • Compiler কখন SIB-এর scale field ব্যবহার করে আর কখন explicit multiply বসায় তা predict করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনগুলোতে আমরা control flow দেখেছি — condition, jump, loop। কিন্তু প্রায় প্রতিটা loop-এর ভেতরে যা ঘটে তা হলো একটা array বা struct-এর কোনো field পড়া বা লেখা। arr[i] — এই একটা ছোট্ট C expression আসলে assembly-তে কী হয়ে ওঠে?

computer-architecture module-এর addressing-modes লেসনে আমরা একটা সাধারণ তত্ত্ব শিখেছিলাম: একটা operand field হয় সরাসরি মান দেয় (immediate), নয়তো একটা effective address গণনা করে সেখান থেকে মান আনে (register indirect, base+displacement, indexed)। RISC-V দিয়ে সেই তত্ত্বটা প্রমাণ করা হয়েছিল — কিন্তু RISC-V-তে জটিল ঠিকানা (array indexing) সবসময় কয়েকটা আলাদা instruction-এ ভাঙতে হতো, কারণ RV32I-এর একমাত্র addressing mode base+displacement।

x86-64 সম্পূর্ণ ভিন্ন পথ নেয়। এখানে একটা একক memory operand-ই base register, index register, একটা scale factor (১, ২, ৪, বা ৮), আর একটা displacement — এই চারটা উপাদান একসাথে ধরতে পারে। ফলে arr[i] প্রায়ই মাত্র একটা instruction-এ চলে যায়:

movl (%rdi,%rax,4), %eax

এই একটা লাইনেই array-এর base ঠিকানা (%rdi), index (%rax), element size (4) — সবকিছু বসে আছে। এই লেসনের কাজ এই সিনট্যাক্সটা সম্পূর্ণভাবে পড়তে শেখা, আর দেখানো যে এটা কোনো নতুন জাদু না — computer-architecture module-এর “indexed addressing” ধারণাটাই, শুধু x86-64-এর নিজস্ব concrete চেহারায়।

আরও গুরুত্বপূর্ণ — আমরা দেখব offsetof-এর সংখ্যা (computer-representation module-এর serialization-binary-formats লেসনের struct layout আলোচনা) আক্ষরিক অর্থেই এই instruction-এর একটা constant field হয়ে বসে যায়। বিমূর্ত struct layout, আর কংক্রিট assembly displacement — এই দুটো একই সংখ্যা।

মূল ধারণা

x86-64-এর memory operand — একটা সমীকরণ

x86-64-এ প্রতিটা memory operand আসলে একটা ছোট্ট সমীকরণের ফলাফল। AT&T syntax-এ (যা GCC/Clang ডিফল্টভাবে emit করে) এটা লেখা হয়:

displacement(base, index, scale)

আর effective address (EA):

EA=base+(index×scale)+displacement\text{EA} = \text{base} + (\text{index} \times \text{scale}) + \text{displacement}

Intel syntax-এ (যা NASM, MASM, আর Intel-এর নিজের ডকুমেন্টেশন ব্যবহার করে) একই জিনিস উল্টো ক্রমে, বর্গবন্ধনীতে লেখা হয়:

[base + index*scale + displacement]

চারটা উপাদান — প্রতিটাই ঐচ্ছিক, দরকার না হলে বাদ দেওয়া যায়:

উপাদানকীসীমা
baseএকটা general-purpose registerযেকোনো ৬৪-বিট register
indexআরেকটা register, যেটা দিয়ে গুণ করা হবেযেকোনো register, %rsp বাদে
scaleindex-কে কত দিয়ে গুণ করা হবেশুধু ১, ২, ৪, ৮ — আর কিছু না
displacementএকটা constant যোগফল৮-বিট বা ৩২-বিট signed immediate

প্রথম উদাহরণ — array indexing হাতে পড়া

int get(int *arr, int i) {
    return arr[i];
}

System V calling convention অনুযায়ী arr আসে %rdi-তে, i আসে %esi-তে (৩২-বিট int, তাই e-প্রিফিক্স register)। বাস্তব GCC -O1 output:

get:
        movslq  %esi, %rax
        movl    (%rdi,%rax,4), %eax
        ret

দুইটা instruction, কিন্তু মেমরি অ্যাক্সেস মাত্র একটা:

লাইন ১ — movslq %esi, %rax i আসে %esi-তে, ৩২-বিট signed integer হিসেবে। কিন্তু x86-64-এ memory addressing-এর index register সবসময় ৬৪-বিট হতে হবে (পুরো address space cover করতে)। তাই আগে sign-extend করে %esi-কে %rax-এ ৬৪-বিটে রূপান্তর করা হচ্ছে — movslq মানে “move sign-extended long (৩২-বিট) থেকে quad (৬৪-বিট)।”

লাইন ২ — movl (%rdi,%rax,4), %eax এখানেই আসল কাজ:

fieldমানঅর্থ
base%rdiarr-এর শুরুর ঠিকানা
index%raxi, ৬৪-বিটে রূপান্তরিত
scale4sizeof(int)
displacement(নেই, ০)কোনো fixed offset নেই

EA=%rdi+(%rax×4)+0\text{EA} = \%rdi + (\%rax \times 4) + 0

Intel syntax-এ ঠিক একই instruction লেখা হতো:

mov eax, [rdi+rax*4]

একটামাত্র instruction — load, index-to-byte-offset scaling, আর address computation — সব একসাথে। computer-architecture module-এর RISC-V উদাহরণে এই একই কাজ পাঁচটা আলাদা instruction লেগেছিল (auipc, addi, slli, add, lw) — কারণ RV32I-তে কোনো indexed addressing mode নেই। x86-64-এর CISC দর্শন সেই পুরো গণনাটাকে hardware-এর SIB (Scale-Index-Base) byte-এ সংকুচিত করে।

Pointer arithmetic — একই গণিত, ভিন্ন C syntax

C-তে arr[i] আর *(arr + i) হুবহু একই জিনিস — ভাষার সংজ্ঞা অনুযায়ীই। কম্পাইলার এই দুটো থেকে একই assembly generate করে:

int get_v2(int *arr, int i) {
    return *(arr + i);
}
get_v2:
        movslq  %esi, %rax
        movl    (%rdi,%rax,4), %eax
        ret

হুবহু আগের মতোই। কিন্তু লক্ষ্য করার বিষয় হলো — C-তে arr + i raw byte address-এ i যোগ করে না। arr-এর টাইপ int *, তাই arr + i মানে “arr-এর iটা int-পরিমাণ সামনে” — কম্পাইলার স্বয়ংক্রিয়ভাবে i-কে sizeof(int) = 4 দিয়ে scale করে। এই scaling-টাই ঠিক উপরের instruction-এর scale=4 field হয়ে বসে গেছে।

“pointer arithmetic মানে raw byte offset যোগ করা।”

C-এর সোর্স লেভেলে না — ptr + i টাইপ-নির্ভর, i × sizeof(*ptr) byte এগিয়ে যায়, এক byte না। কিন্তু hardware-এর দৃষ্টিতে হ্যাঁ, চূড়ান্তভাবে effective address গণনাটা raw byte arithmetic-ই — শুধু সেই multiply (sizeof(*ptr) দিয়ে) কম্পাইলার আগেই করে দিয়েছে, হয় compile-time constant হিসেবে (এই লেসনের উদাহরণে scale=4), নয়তো একটা explicit multiply instruction হিসেবে (নিচে দেখুন, যখন element size ১,২,৪,৮-এর কোনোটাই না)। “Type-aware scaling” আর “raw byte math” — দুটোই সত্যি, শুধু ভিন্ন স্তরে।

ভেতরে কী ঘটছে

যখন scale যথেষ্ট না — explicit multiply আর lea-এর কৌশল

SIB byte-এর scale field শুধু ১, ২, ৪, ৮ সমর্থন করে। কিন্তু বাস্তব প্রোগ্রামে struct-এর size প্রায়ই এর বাইরে। তখন কী হয়?

struct Vec3 { double x, y, z; };   // 8+8+8 = 24 byte, কোনো padding দরকার নেই

double get_x(struct Vec3 *arr, int i) {
    return arr[i].x;
}

sizeof(struct Vec3) = 24, যা ১,২,৪,৮-এর কোনোটাই না। বাস্তব GCC output:

get_x:
        movslq  %esi, %rax
        leaq    (%rax,%rax,2), %rax
        movsd   (%rdi,%rax,8), %xmm0
        ret

লাইন ২-ই আসল কৌশল। leaq (%rax,%rax,2), %rax — এটা memory access না, কোনো মান পড়ছে না। এটা effective-address গণনার সার্কিটটাই ধার করে একটা arithmetic গণনা করছে:

%raxনতুন=%rax+(%rax×2)=%rax×3\%rax_{\text{নতুন}} = \%rax + (\%rax \times 2) = \%rax \times 3

অর্থাৎ i × 3 গণনা হলো, কোনো imul instruction ছাড়াই। কেন × 3? কারণ 24 = 3 × 8 — কম্পাইলার 24-কে 3 × 8-এ ভেঙেছে, × 3 করেছে lea-এর trick দিয়ে (এক cycle-এ), আর বাকি × 8-টা তৃতীয় লাইনের memory operand-এর scale=8 field-এই বসিয়ে দিয়েছে — (%rdi,%rax,8)

EA=%rdi+(i×3)×8=%rdi+i×24\text{EA} = \%rdi + (i \times 3) \times 8 = \%rdi + i \times 24

ঠিক sizeof(struct Vec3) দিয়ে scale করা।

Struct field access — displacement-ই offsetof

computer-representation module-এর serialization-binary-formats লেসনে আমরা offsetof(struct A, field) হাতে গণনা করেছিলাম — padding-সহ struct-এর ভেতর প্রতিটা field ঠিক কত byte-এ বসে। এখন সেই সংখ্যাটাই সরাসরি একটা instruction-এর displacement:

struct Point { int x; int y; };   // offsetof(x)=0, offsetof(y)=4

int get_y(struct Point *p) {
    return p->y;
}
get_y:
        movl    4(%rdi), %eax
        ret

কোনো index, কোনো scale নেই — শুধু base (%rdi, struct-এর ঠিকানা) আর displacement (4)। 4 সংখ্যাটা exactly offsetof(struct Point, y) — compile-time-এ কম্পাইলার struct layout গণনা করেছে, সেই গণনার ফলাফলটাই এখানে একটা bit-encoded constant হয়ে বসে গেছে।

Array-of-struct — চারটা উপাদানই একসাথে

এখন এই লেসনের কেন্দ্রীয় উদাহরণ — array indexing আর struct field access, দুটোই একটা instruction-এ একসাথে।

struct Point { int x; int y; };   // sizeof = 8 (দুইটা int, কোনো padding লাগে না)

int get_y_at(struct Point *arr, int i) {
    return arr[i].y;
}
get_y_at:
        movslq  %esi, %rax
        movl    4(%rdi,%rax,8), %eax
        ret

sizeof(struct Point) = 8, যা বৈধ scale value — তাই এবার আর lea-trick লাগেনি, সরাসরি scale=8 বসানো গেছে। শেষ instruction-এর memory operand-এ চারটা উপাদানই একসাথে:

fieldমানউৎস
base%rdiarr — array-এর শুরুর ঠিকানা
index%raxi — কোন element
scale8sizeof(struct Point)
displacement4offsetof(struct Point, y)

EA=%rdi+(i×8)+4\text{EA} = \%rdi + (i \times 8) + 4

একটা instruction, চারটা ভিন্ন উৎসের সংখ্যা — array base (runtime, register), index (runtime, register), element size (compile-time, struct layout থেকে), field offset (compile-time, struct layout থেকে)। এটাই x86-64-এর addressing mode-এর পূর্ণ ক্ষমতা এক জায়গায়।

arr[i].y — উৎস থেকে একটা instruction পর্যন্ত
  1. arr[i].y — C সোর্সে একটা expressionarray index + field access, একসাথে লেখা
  2. i → 64-bit sign-extendmovslq %esi, %rax — index register প্রস্তুত
  3. element size → scalesizeof(struct Point)=8, বৈধ SIB scale, তাই সরাসরি বসে
  4. field offset → displacementoffsetof(struct Point, y)=4, compile-time constant
  5. movl 4(%rdi,%rax,8), %eaxএকটা instruction — base+index×scale+displacement সব একসাথে

উদাহরণ

RIP-relative addressing — global variable-এর ঠিকানা

computer-architecture module-এর addressing-modes লেসনে PC-relative addressing-এর কথা ছিল branch/jump-এর জন্য। x86-64-এ এর নাম RIP-relative (%rip = instruction pointer), আর এটা global variable access-এও ব্যবহৃত হয় — বিশেষত position-independent executable (PIE, আজকাল Linux-এ ডিফল্ট) কম্পাইল করলে:

int counter;

int increment(void) {
    return ++counter;
}
increment:
        movl    counter(%rip), %eax
        addl    $1, %eax
        movl    %eax, counter(%rip)
        ret

counter(%rip) মানে “%rip-এর বর্তমান মান (এই instruction-এর নিজের ঠিকানার কাছাকাছি) থেকে counter-এর দিকে একটা fixed distance — সেই distance assembler/linker গণনা করে দেয়”। প্রোগ্রামটা memory-তে যেখানেই লোড হোক না কেন (ASLR randomize করলেও), %rip + offset সবসময় সঠিক জায়গায় পৌঁছায় — কোনো absolute ঠিকানা instruction-এ hardcode করা লাগে না।

সম্পূর্ণ তুলনা — একই কাজ, তিনটা ভিন্ন addressing চেহারা

struct Row { int values[4]; };

int matrix_get(struct Row *rows, int i, int j) {
    return rows[i].values[j];
}

sizeof(struct Row) = 16 (৪টা int, বৈধ scale)। এই লেসনের সবচেয়ে ঘন উদাহরণ — দুইটা independent runtime index (i আর j) একসাথে, আর lea-এর arithmetic ব্যবহার একবারে। বাস্তব GCC output:

matrix_get:
        movslq  %esi, %rax      # i → 64-bit
        movslq  %edx, %rdx      # j → 64-bit
        leaq    (%rdx,%rax,4), %rax   # rax = j + i*4   — দুইটা index এক রেজিস্টারে merge
        movl    (%rdi,%rax,4), %eax   # rows + (j + i*4)*4  =  rows + i*16 + j*4
        ret

তৃতীয় লাইনটাই কেন্দ্রীয় কৌশল — leaq (%rdx,%rax,4), %rax কোনো memory access না, নিছক arithmetic: %rdx + %rax×4 = j + i×4। একটা instruction-এই দুইটা independent index মিশে একটা combined index হয়ে যাচ্ছে, ঠিক যেভাবে প্রতি row-তে ৪টা int থাকলে i-তম row-এর শুরু হয় i×4-তম element-এ, আর তারপর jটা element এগিয়ে।

চতুর্থ লাইনে সেই combined index-টাই আবার scale (4, sizeof(int)) দিয়ে গুণ হয়ে চূড়ান্ত effective address দেয়:

EA=rows+(j+i×4)×4=rows+i×16+j×4\text{EA} = \text{rows} + (j + i \times 4) \times 4 = \text{rows} + i \times 16 + j \times 4

হুবহু rows[i].values[j]-এর প্রত্যাশিত ঠিকানা — `i × sizeof(struct Row)

  • j × sizeof(int)। **একটা lea(SIB-কে pure arithmetic হিসেবে ব্যবহার) আর একটাmov (SIB-কে আসল addressing হিসেবে ব্যবহার) — দুইটা independent index-সহ একটা 2D access মাত্র চারটা instruction-এ।** computer-architecture module-এর RISC-V উদাহরণে এই একই কাজ (grid[i][j]`) ছয়টা instruction লেগেছিল, কারণ সেখানে কোনো SIB-জাতীয় hardware “free multiply” ছিল না।

নিজে চালিয়ে দেখুন

EXPERIMENT

Compiler Explorer-এ scale field বদলে যেতে দেখুন

ব্রাউজার — godbolt.org· ১৫ মিনিট

godbolt.org-এ x86-64 gcc (কমপক্ষে -O1) বেছে নিন। এই চারটা function একসাথে কম্পাইল করুন:

struct S1 { char  a[1]; };   // size 1
struct S2 { short a[1]; };   // size 2
struct S3 { int   a[3]; };   // size 12 — বৈধ scale না
struct S4 { long  a[1]; };   // size 8

int f1(struct S1 *p, int i) { return p[i].a[0]; }
int f2(struct S2 *p, int i) { return p[i].a[0]; }
int f3(struct S3 *p, int i) { return p[i].a[0]; }
long f4(struct S4 *p, int i) { return p[i].a[0]; }

f1, f2, f4-এ সরাসরি scale=1,2,8 দেখা উচিত — একটামাত্র movl/movb/movqf3-এ (size=12, বৈধ না) একটা explicit imul $12 বা lea-ভিত্তিক trick (12 = 4×3, তাই সম্ভবত lea (%rax,%rax,2) দিয়ে ×3, তারপর scale=4) দেখা উচিত। প্রতিটার instruction সংখ্যা গুনুন — f3-এ বাকিগুলোর চেয়ে বেশি instruction লাগবে।

এটা কী প্রমাণ করে

Element size-এর সাথে সাথে compiler কখন সরাসরি SIB scale ব্যবহার করে আর কখন lea-trick-এ পড়ে, সেই সীমারেখাটা 1,2,4,8-এই আটকে থাকে।

EXPERIMENT

gdb দিয়ে array-of-struct-এর ঠিকানা সরাসরি যাচাই

Linux — gdb· ১৫ মিনিট
// test.c
struct Point { int x; int y; };

int main() {
    struct Point arr[3] = {{1,2},{3,4},{5,6}};
    int i = 2;
    int y = arr[i].y;
    return y;
}
gcc -g -O0 test.c -o test
gdb ./test
(gdb) break main
(gdb) run
(gdb) next
(gdb) next
(gdb) next
(gdb) print &arr[0]
(gdb) print &arr[2].y
(gdb) print (long)&arr[2].y - (long)&arr[0]

শেষ লাইনের ফলাফল 20 হওয়া উচিত — i×sizeof(struct Point) + offsetof(y) = 2×8 + 4 = 20disassemble main চালিয়ে কোথাও 16(%rax,%rdx,8)-জাতীয় (বা -O0-এ সমতুল্য কিন্তু stack-ভিত্তিক ধাপে-ধাপে) গণনা খুঁজুন।

এটা কী প্রমাণ করে

displacement আর scale শুধু তত্ত্ব না — একটা চলমান প্রোগ্রামে সত্যিই EA = base + index×scale + displacement সমীকরণ ধরে রাখে।

নিজে বানান

BUILD IT

Addressing Mode Decoder — একটা instruction থেকে EA বের করুন

C · ●●○○○
  1. একটা struct আর array-of-struct সংজ্ঞায়িত করুন — নিজের choice-এর কমপক্ষে দুইটা field-সহ
  2. একটা function লিখুন যা array indexing + field access একসাথে করে (এই লেসনের arr[i].y প্যাটার্ন)
  3. gcc -S -O1 দিয়ে assembly output নিন, চূড়ান্ত movl/movq instruction-টা খুঁজুন
  4. সেই instruction থেকে base, index, scale, displacement — চারটা field আলাদা করে লিখুন
  5. হাতে EA = base + index*scale + displacement গণনা করুন একটা নির্দিষ্ট i মান ধরে (i=2 ইত্যাদি)
  6. gdb দিয়ে সত্যিকারের ঠিকানা প্রিন্ট করে আপনার হাতে-গণনা করা EA-র সাথে মিলিয়ে দেখুন
  7. sizeof(struct)-কে ইচ্ছাকৃতভাবে একটা অ-বৈধ scale (যেমন 12, 20) বানিয়ে আবার কম্পাইল করুন — lea-trick আসছে কি না দেখুন

লক্ষ্য: শুধু পড়া না, নিজে হাতে-কলমে একটা memory operand থেকে চারটা field আলাদা করার অভ্যাস তৈরি করা — এটাই পরের লেসনে (compiler output পড়া) পুরো function analyze করার সময় সবচেয়ে বেশি কাজে লাগবে।

Struct নকশা করার সময় সচেতনভাবে দুটো ভার্সন বানান — একটাতে sizeof বৈধ scale (২, ৪, ৮-এর গুণিতক), আরেকটাতে না (যেমন তিনটা char + একটা int কোনোভাবে মিলিয়ে ৯ বা ১৩ byte বানানো)। দুটোর generated code-এর instruction সংখ্যা তুলনা করুন — এটাই এই লেসনের সবচেয়ে concrete প্রমাণ যে element size হার্ডওয়্যার-স্তরে বিনামূল্যে না।

Bonus: -O0-এ কম্পাইল করে একই function দেখুন — সেখানে কোনো lea-trick থাকবে না, বরং প্রতিটা ধাপ (sign-extend, multiply, add, load) আলাদা আলাদা, verbose instruction-এ, stack-ভিত্তিক মধ্যবর্তী ফলাফল-সহ। এই পার্থক্যটাই পরের লেসনের মূল বিষয়।

বাস্তব সিস্টেমে

Addressing mode বাস্তব কোডে

memcpy/memmove-এর ভেতরের loop। glibc-র optimized memcpy প্রতিটা iteration-এ base+displacement addressing দিয়ে source আর destination থেকে একসাথে বড় chunk (১৬, ৩২ byte SIMD register-এ) পড়ে-লেখে — এই লেসনের সমীকরণটাই, শুধু displacement প্রতি iteration-এ বদলায় না, base register-টাই বাড়ে (add $16, %rsi)।

Array-of-struct বনাম struct-of-arrays (AoS বনাম SoA)। এই লেসনের arr[i].y প্যাটার্নে প্রতিটা access memory-তে একে অপরের থেকে sizeof(struct) দূরে — যদি struct বড় হয় কিন্তু আপনি শুধু একটা field বারবার দরকার হয় (যেমন একটা particle simulation-এ শুধু y-স্থানাঙ্ক), cache line-এর বেশিরভাগ অংশ অপচয় হয়। এই কারণেই performance-critical কোড (game engine, HPC) প্রায়ই struct-of-arrays-এ রূপান্তরিত হয় — আলাদা x[], y[], z[] array, যেখানে প্রতিটা cache line শুধু প্রাসঙ্গিক ডেটায় ভরা।

Null pointer dereference-এর নির্দিষ্ট crash ঠিকানা। p->y যেখানে p == NULL, তখন effective address হয় 0 + 4 = 4 (এই লেসনের get_y-এর মতোই, শুধু %rdi = 0)। এই কারণেই segfault backtrace-এ প্রায়ই খুব ছোট ঠিকানা (0x4, 0x8, 0x10) দেখা যায় — এলোমেলো crash না, ঠিক displacement মান, base শূন্য হওয়ায় সরাসরি দৃশ্যমান। এটা debug করার সময় displacement-টা কোন field তা চেনা সরাসরি সাহায্য করে।

Compiler bounds-check elimination। arr[i] যদি একটা loop-এর ভেতরে হয় যেখানে i প্রমাণযোগ্যভাবে 0 থেকে n-1-এর মধ্যে থাকে, কম্পাইলার (আর JIT, যেমন JVM/V8) বারবার bounds-check না করে সরাসরি এই লেসনের addressing mode-এ চলে যায় — check-টা loop-এর বাইরে একবার বসে (loop-invariant hoisting)। পরের লেসনে (-O0 বনাম -O2) আমরা ঠিক এই ধরনের optimization দেখব।

Hardware prefetcher-এর জন্য predictable pattern। memory hierarchy লেসনের prefetcher base + index×scale প্যাটার্নে ঘটা sequential access (i, i+1, i+2, ...) সহজে চিনতে পারে, কারণ প্রতিটা access-এর মধ্যে দূরত্ব ধ্রুবক (scale byte)। Random-access pattern এই predictability ভেঙে দেয় — যা asymptotic-notation লেসনের cache-locality আলোচনার সরাসরি hardware-ভিত্তি।

Struct padding-নির্ভর ABI compatibility bug। দুইটা ভিন্ন compiler (বা একই compiler-এর ভিন্ন flag) যদি একটা struct-এ ভিন্ন padding দেয়, একই field-এর displacement আলাদা হয়ে যায় — একটা library .so অন্য কোনো ভিন্নভাবে কম্পাইল-করা প্রোগ্রামের সাথে link করলে ভুল offset-এ পড়া/লেখা হয়, নীরব data corruption। এই কারণে ABI-স্থিতিশীল লাইব্রেরি (glibc, Qt) struct layout বদলাতে অত্যন্ত সতর্ক।

gdb/lldb-এর x (examine memory) কমান্ড সরাসরি এই সমীকরণ ব্যবহার করে। x/4xw $rdi+16 লেখার মানে হলো ঠিক এই লেসনের base+displacement হাতে বসিয়ে মেমরি দেখা — debugger-এর ভাষা আর compiler-এর generated addressing mode একই গণিত ব্যবহার করে, কাকতালীয় না।

যে ভুলগুলো সবাই করে

“arr[i]-এ i-কে element size দিয়ে গুণ করাটা প্রতিবারই একটা আলাদা imul instruction লাগে।”

প্রায়ই না। যদি element size ১, ২, ৪, ৮-এর একটা হয় (বেশিরভাগ প্রিমিটিভ টাইপ আর ছোট struct), সেই গুণটা কোনো আলাদা instruction ছাড়াই SIB byte-এর scale field-এ বিনামূল্যে ঘটে — hardware-এর address-computation সার্কিট নিজেই করে দেয়, এক cycle-এরও কম বাড়তি খরচে। শুধু element size অ-বৈধ scale হলে (এই লেসনের sizeof=24 উদাহরণ) explicit multiply বা lea-trick লাগে।

“lea শুধু ঠিকানা লোড করার জন্য — memory-সম্পর্কিত কাজেই সীমাবদ্ধ।”

নামটা (“load effective address”) এই ভুল ধারণা তৈরি করে। বাস্তবে lea-র underlying hardware শুধু একটা adder+shifter সার্কিট, আর কম্পাইলাররা এটা প্রায়ই কোনো memory-র সাথে সম্পর্কহীন সাধারণ integer arithmetic-এর জন্য ব্যবহার করে — x*3, x*5, 2x+8 — কারণ এটা একটা মাত্র instruction-এ, flags না বদলে (তুলনায় add/imul সাধারণত condition flags বদলায়), করে ফেলা যায়। এই লেসনের leaq (%rax,%rax,2), %rax উদাহরণ (i×3 গণনা) কোনো ঠিকানা লোড করছে না — নিছক গুণ।

“struct-এর displacement মান source code পড়েই বলা যায় — field-এর ক্রম অনুযায়ী।”

Padding না থাকলে হ্যাঁ, কিন্তু alignment থাকলে না। এই লেসনের struct Mixed { char a; int b; } উদাহরণে b-এর displacement 1 না, 4 — কারণ int-এর ৪-byte alignment padding যোগ করে। সঠিক displacement জানতে হলে হয় compiler-এর generated assembly দেখতে হবে, নয়তো offsetof() ব্যবহার করতে হবে — কখনোই field-এর ঘোষণার ক্রম ধরে আন্দাজ করা নিরাপদ না।

“scale field দিয়ে যেকোনো সংখ্যা দিয়ে গুণ করা যায়, x86 এতটাই flexible।”

না — SIB byte মাত্র দুই বিট scale-এর জন্য বরাদ্দ করে, তাই শুধু ১, ২, ৪, ৮ সম্ভব। এই লেসনের sizeof(struct Vec3)=24 উদাহরণ ঠিক এই সীমাটাই দেখায় — 24 সরাসরি বসে না, কম্পাইলারকে lea-trick (24 = 3×8) বা explicit imul দিয়ে ঘুরপথ নিতে হয়। x86-64 শক্তিশালী, কিন্তু এই একটা দিকে কঠোরভাবে সীমাবদ্ধ।

বুঝেছেন কি না দেখুন

1movl 12(%rdi,%rsi,4), %eax — এই instruction-এর base, index, scale, ও displacement আলাদা করে লিখুন। Effective address-এর সমীকরণ কী?স্মরণ
  • base = %rdi
  • index = %rsi
  • scale = 4
  • displacement = 12

EA=%rdi+(%rsi×4)+12\text{EA} = \%rdi + (\%rsi \times 4) + 12

এটা এমন একটা প্যাটার্নে ঘটতে পারে যেখানে %rdi একটা int-এর array-of-struct-এর base, %rsi কোন element (index), আর 12 সেই struct-এর ভেতরের কোনো field-এর offset — এই লেসনের arr[i].y উদাহরণেরই একটা রূপ, শুধু ভিন্ন সংখ্যায়।

2struct RGB { unsigned char r, g, b; }; (৩ byte, কোনো padding দরকার নেই কারণ char-এর alignment 1)। একটা array struct RGB pixels[N]-এর pixels[i].g access করতে কম্পাইলার কী কৌশল নেবে, আর কেন?প্রয়োগ

sizeof(struct RGB) = 3, যা বৈধ SIB scale (১,২,৪,৮) না। তাই সরাসরি scale=3 বসানো যাবে না। কম্পাইলার একটা explicit multiply করবে — হয় imul $3, %reg (সরাসরি), অথবা lea-trick (leaq (%rax,%rax,2), %raxx + x*2 = x*3, প্রায়ই imul-এর চেয়ে দ্রুত)। এরপর g-এর displacement (offsetof(RGB, g) = 1) যোগ হবে একটা সরল base+displacement দিয়ে, আর কোনো scale ছাড়াই (কারণ multiply আগেই শেষ):

movb 1(%rdi,%rax), %al

এই উদাহরণটা দেখায় কেন কিছু graphics/multimedia কোড ইচ্ছাকৃতভাবে RGBA (৪ byte, padding-সহ, বৈধ scale) ব্যবহার করে RGB-এর (৩ byte) বদলে — এমনকি এক byte বেশি memory খরচ করেও, addressing সরল ও দ্রুত হয়।

3leaq 8(%rdi,%rdi,1), %rax — এই instruction memory access করছে না, কিন্তু এখনো memory addressing syntax ব্যবহার করছে। এটা কী গণনা করছে, আর কেন কম্পাইলার সরাসরি add/shift-এর বদলে lea বেছে নিলো?যুক্তি

গণনা: %rax ← %rdi + (%rdi × 1) + 8 = %rdi × 2 + 8

lea বেছে নেওয়ার কারণ — এই একটা instruction-এই তিনটা কাজ একসাথে (double, constant যোগ) হয়ে যাচ্ছে, যা আলাদাভাবে করতে লাগত দুইটা instruction (addl %rdi, %rdi তারপর addl $8, %rdi, বা shll $1, %rdi তারপর addl $8)। এছাড়া lea condition flags (CF, ZF, ইত্যাদি) বদলায় না — তাই যদি কম্পাইলার জানে flags-এর মান পরে দরকার (যেমন একটা আগের comparison-এর ফলাফল এখনো দরকার), lea ব্যবহার করলে সেই flags সংরক্ষিত থাকে, add/shl ব্যবহার করলে হারিয়ে যেত।

4struct Pair { long a; long b; }; (১৬ byte)। একটা function long sum_pairs(struct Pair *arr, int n) যা সব pair-এর a+b যোগ করে, তার loop body-তে একটা মাত্র array access দিয়ে দুইটা field (a আর b) পড়তে চাইলে কী addressing pattern আশা করবেন?প্রয়োগ

দুইটা field-এর জন্য দুইটা আলাদা movq, কিন্তু একই base+index গণনা পুনরায় ব্যবহার হবে, শুধু displacement ভিন্ন:

movq (%rdi,%rax,16), %rcx    # arr[i].a — displacement 0
movq 8(%rdi,%rax,16), %rdx   # arr[i].b — displacement 8

লক্ষ্য করুন — base (%rdi), index (%rax), scale (16) হুবহু একই, শুধু displacement 0 বনাম 8। কম্পাইলার সাধারণত %rax-এর জন্য effective address-এর base অংশ (%rdi + %rax×16) একবারই গণনা করার সুযোগ খোঁজে (যদি optimization যথেষ্ট আক্রমণাত্মক হয়), কিন্তু x86-64-এর addressing mode-এই দুই displacement আলাদা করার ক্ষমতা থাকায় প্রায়ই আলাদা গণনা ছাড়াই দুইটা memory operand লিখে দেওয়াই যথেষ্ট — hardware নিজেই দুইবার %rdi + %rax×16 (একবার +0, একবার +8) গণনা করে নেয়, কোনো বাড়তি instruction ছাড়াই।

5একটা নতুন ISA ডিজাইন করছেন। কেউ প্রস্তাব দিলো scale field-এ শুধু ১,২,৪,৮-এর বদলে ১ থেকে ৩২-এর যেকোনো সংখ্যা সমর্থন করা হোক, যাতে যেকোনো struct size সরাসরি scale-এ বসে। এই প্রস্তাবের সুবিধা ও অসুবিধা কী?ডিজাইন

সুবিধা: এই লেসনের sizeof=24-জাতীয় উদাহরণে আর lea-trick বা explicit multiply লাগত না — সরাসরি এক instruction, এক scale।

অসুবিধা — কেন বাস্তবে এটা হয় না:

১. এনকোডিং খরচ। ১-৩২ represent করতে ৫ বিট লাগে, যেখানে ১,২,৪,৮ (শুধু 2ⁿ, n=0..3) মাত্র ২ বিটে ধরে — SIB byte-এর সীমিত বিট বাজেটে এই পার্থক্য গুরুত্বপূর্ণ।

২. Hardware সরলতা। scale = 2ⁿ মানে এটা একটা বিট-শিফট সার্কিট দিয়ে বাস্তবায়িত হয় — সস্তা, দ্রুত, single-cycle। যেকোনো ১-৩২ সমর্থন করতে একটা পূর্ণ multiplier সার্কিট লাগত address-computation পথে, যা critical path (প্রতিটা memory access-এর latency) লম্বা করে দিত — memory hierarchy লেসনের প্রতিটা access-ই speed-critical।

৩. ব্যবহারিক প্রয়োজন কম। বেশিরভাগ প্রিমিটিভ টাইপ আর সাধারণ struct আকার ২ⁿ-এর কাছাকাছি বা lea-trick দিয়ে সহজেই ভাঙা যায় (3=1+2, 5=1+4, 24=3×8)। VAX-এর ইতিহাস (addressing-modes লেসনের Callout) ঠিক এই শিক্ষাই দেয় — বিরল ব্যবহারের জন্য hardware জটিলতা যোগ করার মূল্য প্রায়ই লাভের চেয়ে বেশি।

এরপর কী

এখন আমাদের হাতে দুইটা সম্পূর্ণ টুকরো — control flow (আগের লেসন) আর addressing (এই লেসন)। এই দুইটাই একটা compiled function-এর প্রায় পুরো ভাষা।

পরের লেসনেই এই মডিউলের driving question-এর সরাসরি উত্তর — আমার C function compile হয়ে ঠিক কোন instruction-গুলো হলো, আর কেন? আমরা একটা real function নেব, তার -O0 output (আক্ষরিক, verbose, প্রতিটা মধ্যবর্তী মান stack-এ) আর -O2 output (register-ভিত্তিক, compressed, এই লেসনের lea-trick-এর মতো কৌশলে ভরা) পাশাপাশি রেখে প্রতিটা পার্থক্য ব্যাখ্যা করব। এই লেসনে যা শিখেছেন — memory operand পড়া, scale/displacement চেনা, lea-এর arithmetic ব্যবহার চেনা — সবকিছু সরাসরি সেখানে কাজে লাগবে।

আরও পড়ুন

  • Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 1 — Section 3.7, Addressing Modes — Intel Corporation · SIB byte-ভিত্তিক base+index×scale+displacement এনকোডিং-এর প্রামাণ্য উৎস
  • System V Application Binary Interface, AMD64 Architecture Processor Supplement — x86-64 psABI working group · struct layout, alignment, ও calling convention-এর প্রামাণ্য বিবরণ — displacement মান এখান থেকেই আসে
  • Compiler Explorer · এই লেসনের প্রতিটা উদাহরণ নিজে কম্পাইল করে যাচাই করার সবচেয়ে ভালো জায়গা