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 হয়ে ওঠে।
আগে এটা বুঝি
গত লেসনগুলোতে আমরা 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):
Intel syntax-এ (যা NASM, MASM, আর Intel-এর নিজের ডকুমেন্টেশন ব্যবহার করে) একই জিনিস উল্টো ক্রমে, বর্গবন্ধনীতে লেখা হয়:
[base + index*scale + displacement]চারটা উপাদান — প্রতিটাই ঐচ্ছিক, দরকার না হলে বাদ দেওয়া যায়:
| উপাদান | কী | সীমা |
|---|---|---|
| base | একটা general-purpose register | যেকোনো ৬৪-বিট register |
| index | আরেকটা register, যেটা দিয়ে গুণ করা হবে | যেকোনো register, %rsp বাদে |
| scale | index-কে কত দিয়ে গুণ করা হবে | শুধু ১, ২, ৪, ৮ — আর কিছু না |
| 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 | %rdi | arr-এর শুরুর ঠিকানা |
| index | %rax | i, ৬৪-বিটে রূপান্তরিত |
| scale | 4 | sizeof(int) |
| displacement | (নেই, ০) | কোনো fixed offset নেই |
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 গণনা করছে:
অর্থাৎ i × 3 গণনা হলো, কোনো imul instruction ছাড়াই। কেন × 3?
কারণ 24 = 3 × 8 — কম্পাইলার 24-কে 3 × 8-এ ভেঙেছে, × 3
করেছে lea-এর trick দিয়ে (এক cycle-এ), আর বাকি × 8-টা তৃতীয়
লাইনের memory operand-এর scale=8 field-এই বসিয়ে দিয়েছে —
(%rdi,%rax,8)।
ঠিক 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
retsizeof(struct Point) = 8, যা বৈধ scale value — তাই এবার আর
lea-trick লাগেনি, সরাসরি scale=8 বসানো গেছে। শেষ instruction-এর
memory operand-এ চারটা উপাদানই একসাথে:
| field | মান | উৎস |
|---|---|---|
| base | %rdi | arr — array-এর শুরুর ঠিকানা |
| index | %rax | i — কোন element |
| scale | 8 | sizeof(struct Point) |
| displacement | 4 | offsetof(struct Point, y) |
একটা 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 — C সোর্সে একটা expressionarray index + field access, একসাথে লেখা
- i → 64-bit sign-extendmovslq %esi, %rax — index register প্রস্তুত
- element size → scalesizeof(struct Point)=8, বৈধ SIB scale, তাই সরাসরি বসে
- field offset → displacementoffsetof(struct Point, y)=4, compile-time constant
- 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)
retcounter(%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 দেয়:
হুবহু 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” ছিল না।
নিজে চালিয়ে দেখুন
Compiler Explorer-এ scale field বদলে যেতে দেখুন
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/movq। f3-এ (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-এই আটকে থাকে।
gdb দিয়ে array-of-struct-এর ঠিকানা সরাসরি যাচাই
// 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 = 20। disassemble main চালিয়ে কোথাও 16(%rax,%rdx,8)-জাতীয়
(বা -O0-এ সমতুল্য কিন্তু stack-ভিত্তিক ধাপে-ধাপে) গণনা খুঁজুন।
displacement আর scale শুধু তত্ত্ব না — একটা চলমান প্রোগ্রামে সত্যিই EA = base + index×scale + displacement সমীকরণ ধরে রাখে।
নিজে বানান
Addressing Mode Decoder — একটা instruction থেকে EA বের করুন
- একটা struct আর array-of-struct সংজ্ঞায়িত করুন — নিজের choice-এর কমপক্ষে দুইটা field-সহ
- একটা function লিখুন যা array indexing + field access একসাথে করে (এই লেসনের arr[i].y প্যাটার্ন)
- gcc -S -O1 দিয়ে assembly output নিন, চূড়ান্ত movl/movq instruction-টা খুঁজুন
- সেই instruction থেকে base, index, scale, displacement — চারটা field আলাদা করে লিখুন
- হাতে EA = base + index*scale + displacement গণনা করুন একটা নির্দিষ্ট i মান ধরে (i=2 ইত্যাদি)
- gdb দিয়ে সত্যিকারের ঠিকানা প্রিন্ট করে আপনার হাতে-গণনা করা EA-র সাথে মিলিয়ে দেখুন
- 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
এটা এমন একটা প্যাটার্নে ঘটতে পারে যেখানে %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), %rax — x + 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 · এই লেসনের প্রতিটা উদাহরণ নিজে কম্পাইল করে যাচাই করার সবচেয়ে ভালো জায়গা