Endianness — বাইট অর্ডারের যুদ্ধ
Endianness
একটা multi-byte value byte-addressable memory-তে কোন ক্রমে বসবে সেটা প্রকৃতির নিয়ম না, একটা convention — big-endian না little-endian — আর সেই কনভেনশন না জানলে network packet আর file format ভুল পড়া হয়।
আগে এটা বুঝি
Bits, bytes, words-এর লেসনে আমরা দেখেছি একটা int সাধারণত ৪ byte — ৩২ bit। মেমরি byte-addressable, মানে প্রতিটা byte-এর নিজের address আছে। তাহলে একটা ৪-byte সংখ্যা মেমরিতে রাখতে গেলে সেই ৪টা byte কোন ক্রমে পরপর address-এ বসবে?
ধরুন সংখ্যাটা 0x12345678। এটার চারটা byte: 12, 34, 56, 78। মেমরির address 1000 থেকে 1003 — চারটা slot। কোনটা কোথায় যাবে?
এর কোনো “স্বাভাবিক” উত্তর নেই। এটা গণিতের কোনো সত্য নয় — সংখ্যা কী, সেটা number-systems লেসনে যেভাবে সংজ্ঞায়িত হয়েছে তার সাথে address-এ byte-এর ক্রমের কোনো সম্পর্ক নেই। এটা নিছক একটা সিদ্ধান্ত — একটা convention যেটা CPU designer-রা নিয়েছেন। দুইটা জনপ্রিয় সিদ্ধান্ত আছে, আর দুটোই আজও ব্যবহার হয়:
address: 1000 1001 1002 1003
big-endian: 12 34 56 78 ← সবচেয়ে গুরুত্বপূর্ণ byte আগে
little-endian: 78 56 34 12 ← সবচেয়ে কম গুরুত্বপূর্ণ byte আগেদুটোই সমানভাবে “সঠিক”। সমস্যা হয় তখনই যখন একটা মেশিন এক convention-এ লেখে আর আরেকটা মেশিন অন্য convention-এ পড়ে — তখন 0x12345678 হয়ে যায় 0x78563412। সংখ্যাটা সম্পূর্ণ ভিন্ন, অথচ প্রতিটা byte ঠিকই ছিল।
এই লেসনে আমরা এই কনভেনশনটাই বুঝব — এটার নাম কোথা থেকে এলো, কোন মেশিন কোনটা ব্যবহার করে, কেন network protocol-এর নিজস্ব নিয়ম আছে, আর ফাইল ফরম্যাট কীভাবে এই সমস্যা সমাধান করে।
মূল ধারণা
সংজ্ঞা
Big-endian (BE): সবচেয়ে গুরুত্বপূর্ণ byte (most significant byte, MSB) সবচেয়ে ছোট address-এ থাকে। মানুষ যেভাবে সংখ্যা লেখে ঠিক সেই ক্রমে — বাম থেকে ডানে বড় থেকে ছোট।
Little-endian (LE): সবচেয়ে কম গুরুত্বপূর্ণ byte (least significant byte, LSB) সবচেয়ে ছোট address-এ থাকে। সংখ্যাটা “উল্টো” ক্রমে সাজানো থাকে।
0x12345678 = 305,419,896 (decimal)
Big-endian:
address: 1000 1001 1002 1003
byte: 0x12 0x34 0x56 0x78
MSB ─────────────────────► LSB
Little-endian:
address: 1000 1001 1002 1003
byte: 0x78 0x56 0x34 0x12
LSB ─────────────────────► MSBলক্ষ্য করুন — দুই ক্ষেত্রেই মান একই (0x12345678)। শুধু address 1000-এ কোন byte বসবে, সেটাই আলাদা। যদি আপনি শুধু address 1000-এর byte-টা পড়েন, big-endian-এ পাবেন 0x12, little-endian-এ পাবেন 0x78 — সম্পূর্ণ ভিন্ন সংখ্যা মনে হবে, যদি আপনি convention না জানেন।
নামের ইতিহাস
কে কোনটা ব্যবহার করে
| Architecture | Byte order | নোট |
|---|---|---|
| x86 / x86-64 | Little-endian | Intel 8086 (1978) থেকেই — প্রায় পুরো consumer computing এখানে দাঁড়িয়ে |
| ARM | Bi-endian (default little) | Hardware দুটোই সমর্থন করে; বাস্তবে প্রায় সব ARM Linux/Android/iOS little-endian মোডে চলে |
| RISC-V | সাধারণত little-endian | Spec অনুযায়ী little-endian-কে base হিসেবে ধরা হয় |
| প্রাচীন PowerPC / SPARC / Motorola 68k | Big-endian | পুরনো Mac (PowerPC যুগ, 2006-এর আগে), পুরনো SPARC server |
| Network protocol header (TCP/IP) | Big-endian | “Network byte order” — নিচে বিস্তারিত |
কেন little-endian প্রযুক্তিগতভাবে সুবিধাজনক হতে পারে
Little-endian-কে “উল্টো” মনে হয় বলে অনেকে ভাবেন এটা একটা দুর্ঘটনাজনিত বা ভুল ডিজাইন। বাস্তবে এর একটা বাস্তব সুবিধা আছে: type casting সহজ হয়।
ধরুন একটা int32_t value-কে int8_t হিসেবে পড়তে চান (শুধু নিচের byte-টুকু)। Little-endian-এ সেই byte-টা একই address-এ পাওয়া যায় — কোনো offset হিসাব লাগে না:
int32_t x = 0x12345678;
int8_t low_byte = *(int8_t*)&x; // little-endian: address x, মান 0x78
// big-endian: একই address, মান 0x12 (MSB!)Little-endian মেশিনে একটা ছোট type-এর pointer বড় type-এর একই address থেকে সরাসরি কাজ করে — একই address থেকে ৪ byte পড়লে int32, ২ byte পড়লে int16, ১ byte পড়লে int8, আর প্রতিটাই সেই একই সংখ্যার নিচের অংশ। এই কারণে multi-precision arithmetic (বড় সংখ্যা ছোট ছোট chunk-এ ভাগ করে যোগ/গুণ করা, যেমন cryptography library-তে) little-endian-এ কিছুটা সহজ।
Big-endian-এর নিজস্ব সুবিধাও আছে — network protocol-এ header পড়ার সময় byte-গুলো “মানুষের মতো” ক্রমে আসে, আর hex dump পড়া সহজ হয় কারণ raw byte-ই সংখ্যার স্বাভাবিক ক্রম। এজন্যই network byte order হিসেবে big-endian-কেই স্থির করা হয়েছিল, x86 ততটা প্রভাবশালী হওয়ার আগেই।
৬৪-বিট উদাহরণ — word-এর জন্যও একই নিয়ম
bits-bytes-words লেসনে আমরা দেখেছি একটা word (আধুনিক x86-64-এ সাধারণত ৮ byte, uint64_t) কতগুলো byte নিয়ে গঠিত। একই নিয়ম যেকোনো byte-সংখ্যার জন্য খাটে — শুধু বেশি byte নিয়ে। ধরুন 0x0102030405060708 সংখ্যাটা address 2000–2007-এ রাখতে হবে:
| Address | Big-endian | Little-endian |
|---|---|---|
2000 | 0x01 | 0x08 |
2001 | 0x02 | 0x07 |
2002 | 0x03 | 0x06 |
2003 | 0x04 | 0x05 |
2004 | 0x05 | 0x04 |
2005 | 0x06 | 0x03 |
2006 | 0x07 | 0x02 |
2007 | 0x08 | 0x01 |
প্যাটার্নটা 0x12345678-এর ৩২-বিট উদাহরণের সাথে হুবহু মিলছে, শুধু byte সংখ্যা দ্বিগুণ — কারণ endianness একটা per-value নিয়ম, value-র আকারের সাথে তার সংজ্ঞা বদলায় না। ৮ byte, ২ byte, বা ১৬ byte — যেকোনো length-এর multi-byte value-তে ঠিক একই নীতি প্রযোজ্য: big-endian-এ MSB প্রথমে, little-endian-এ LSB প্রথমে।
Signed integer-এও একই নিয়ম — two’s complement bit pattern-কেই সাজানো হয়
Signed integers লেসনে আমরা দেখেছি negative সংখ্যা two’s complement-এ একটা নির্দিষ্ট bit pattern হিসেবে থাকে। Endianness সেই bit pattern-এর মানে নিয়ে মাথা ঘামায় না — এটা শুধু সেই bit pattern-কে byte-এ ভেঙে কোন ক্রমে সাজাবে সেটা ঠিক করে। int32_t x = -100-এর two’s complement bit pattern হলো 0xFFFFFF9C; সেই bit pattern-কে endianness ঠিক 0x12345678-এর মতোই ট্রিট করে:
| Address | Big-endian | Little-endian |
|---|---|---|
3000 | 0xFF | 0x9C |
3001 | 0xFF | 0xFF |
3002 | 0xFF | 0xFF |
3003 | 0x9C | 0xFF |
গুরুত্বপূর্ণ বিচ্ছেদ: two’s complement সংজ্ঞা করে সংখ্যাটা কী (negative না positive, কোন মান), byte order সংজ্ঞা করে সেই bit pattern কোথায় বসবে। দুটো সম্পূর্ণ স্বাধীন সিদ্ধান্ত — একটা “কী বোঝাচ্ছে” স্তরে, আরেকটা “কীভাবে সাজানো আছে” স্তরে। এই বিচ্ছেদটাই এই পুরো module-এর মূল থিম: bit pattern নিজে নিরর্থক, অর্থ আসে convention-এর স্তর থেকে — আর এখানে একই bit pattern-এর উপর দুইটা ভিন্ন convention (two’s complement আর byte order) স্বাধীনভাবে কাজ করছে।
বিভিন্ন ভাষায় byte order নিয়ন্ত্রণ — একটা দ্রুত রেফারেন্স
| ভাষা | Native/host order পাওয়া | Explicit little-endian | Explicit big-endian |
|---|---|---|---|
| C (POSIX) | \<endian.h>-এর __BYTE_ORDER__ | htole32() / le32toh() | htobe32() / htonl() |
| C++20 | std::endian::native | std::byteswap + condition | একই |
| Python | sys.byteorder | struct.pack('\<I', x) | struct.pack('>I', x) |
| Java | নেই (সবসময় big-endian ডিফল্ট) | ByteBuffer.order(LITTLE_ENDIAN) | ডিফল্ট আচরণ |
| Rust | cfg!(target_endian) | x.to_le_bytes() | x.to_be_bytes() |
| Go | — | binary.LittleEndian.PutUint32() | binary.BigEndian.PutUint32() |
লক্ষ্য করুন — প্রতিটা আধুনিক ভাষাই native/host-নির্ভর আচরণ এড়িয়ে explicit byte order নির্বাচনের সুবিধা দেয়। এটা কোনো কাকতালীয় ব্যাপার না — এই লেসনে বারবার দেখা bug-গুলোর সবচেয়ে সাধারণ প্যাটার্নই হলো “native order-কে implicit ধরে নেওয়া”, তাই ভাষা designer-রা ইচ্ছাকৃতভাবে explicit API-কে সহজ ও প্রথম পছন্দ বানিয়ে রাখেন।
ভেতরে কী ঘটছে
যেখানে endianness নিজেকে প্রকাশ করে
স্বাভাবিক C/Python/Java কোড লিখলে আপনি প্রায় কখনো endianness দেখেন না — int x = 5; লিখলে compiler ঠিক ব্যবস্থা করে। কিন্তু abstraction-টা বেশ কিছু জায়গায় ফুটো, আর সেখানেই বাগ জন্মায়।
১. Type punning — memory-কে সরাসরি byte হিসেবে দেখা
যখনই আপনি একটা multi-byte value-কে byte array হিসেবে দেখেন (union, pointer cast, memcpy), endianness সরাসরি দৃশ্যমান হয়ে যায়:
#include \<stdio.h>
int main(void) {
uint32_t x = 0x12345678;
unsigned char *bytes = (unsigned char *)&x;
printf("bytes[0] = 0x%02x\n", bytes[0]);
printf("bytes[1] = 0x%02x\n", bytes[1]);
printf("bytes[2] = 0x%02x\n", bytes[2]);
printf("bytes[3] = 0x%02x\n", bytes[3]);
return 0;
}x86-64-এ (little-endian) এর output:
bytes[0] = 0x78
bytes[1] = 0x56
bytes[2] = 0x34
bytes[3] = 0x12একই কোড একটা big-endian মেশিনে (যেমন একটা পুরনো PowerPC Mac, বা বিগ-এন্ডিয়ান মোডে চলা ARM) সম্পূর্ণ উল্টো output দেবে। কোড একই, compiler একই ভাষা মেনেছে, কিন্তু ফলাফল ভিন্ন — কারণ ভাষার spec (C standard) ইচ্ছাকৃতভাবে byte order-কে implementation-defined রাখে।
২. Debugger / hex editor-এ raw memory দেখা
gdb-তে একটা int x = 0x12345678; নিয়ে দুইভাবে memory inspect করলে ভিন্ন output দেখা যায়:
(gdb) x/4xb &x
0x7ffee3a1c9ac: 0x78 0x56 0x34 0x12
(gdb) x/1xw &x
0x7ffee3a1c9ac: 0x12345678প্রথম command (x/4xb — ৪টা byte, hex) raw memory হুবহু দেখাচ্ছে, byte order নিয়ে কোনো interpretation ছাড়াই — এটাই x86-64-এর little-endian layout, 78 56 34 12। দ্বিতীয় command (x/1xw — ১টা word, hex) gdb নিজে ৪টা byte-কে জুড়ে, host-এর native byte order জেনে, সঠিক সংখ্যা 0x12345678 হিসেবে দেখাচ্ছে। এই দুই command-এর ফলাফল ভিন্ন দেখে অনেকে প্রথমবার বিভ্রান্ত হন — আসলে দুটোই সঠিক, শুধু একটা raw byte দেখাচ্ছে আর আরেকটা interpret করে দেখাচ্ছে। Level 3 (Assembly)-এ gdb-তে আরও গভীরভাবে register আর memory inspect করা শিখব।
৩. Socket-এ integer পাঠানো — সবচেয়ে সাধারণ real bug
uint32_t port_number = 8080;
send(sock, &port_number, sizeof(port_number), 0); // ❌ বিপজ্জনকএই কোড port_number-এর raw byte পাঠাচ্ছে — যেটা পাঠানো মেশিনের native byte order-এ আছে। যদি x86 (little-endian) মেশিন থেকে পাঠানো হয় আর receiver সেটাকে network byte order (big-endian) ধরে নিয়ে পড়ে, 8080 (0x00001F90) হয়ে যাবে 0x901F0000 — একটা বিশাল ভুল সংখ্যা। এই কারণেই BSD sockets API-তে htonl/htons বাধ্যতামূলক convention — নিচে experiment-এ আমরা এটা নিজে verify করব।
৪. একই protocol-এর মধ্যেই মিশ্র byte order
সবচেয়ে বিপজ্জনক কেস — যখন একটা protocol বা file format-এর ভেতরেই কিছু field এক ক্রমে, কিছু আরেক ক্রমে থাকে। Bitcoin protocol-এ প্রায় সব integer little-endian, কিন্তু block hash আর transaction hash প্রথাগতভাবে উল্টো (byte-reversed) ক্রমে display ও কখনো store করা হয় — historical কারণে (SHA-256-এর output byte array-কে বিগ সংখ্যা হিসেবে দেখানোর রীতি থেকে)। এই অসামঞ্জস্যতা এতটাই কুখ্যাত যে Bitcoin developer-দের মধ্যে এটাকে “endianness ভূত” বলা হয় — নতুন developer-রা প্রায় নিয়মিতভাবে ভুল দিকে byte reverse করে debugging-এ ঘণ্টার পর ঘণ্টা সময় নষ্ট করে।
৫. ফাইলে সরাসরি struct লিখে ফেলা (এবং পরের লেসনের ভূমিকা)
FILE *f = fopen("data.bin", "wb");
fwrite(&my_struct, sizeof(my_struct), 1, f); // ❌ native byte order, কোনো portability নেই
fclose(f);এই ফাইলটা যে মেশিনে লেখা হয়েছে সেই মেশিনের byte order (আর padding layout, যেটা পরের লেসনের বিষয়) নিয়েই আটকে থাকে। ভিন্ন architecture-এ পড়লে ভুল সংখ্যা বেরোবে — কোনো error ছাড়াই, নিঃশব্দে। এটাই serialization লেসনের সূচনা বিন্দু।
৬. C bit-field-এর ভেতরের bit-order — implementation-defined, endianness-এর সাথে জড়িত
struct flags {
unsigned int a : 4;
unsigned int b : 4;
};C standard বলে না a কোন দিক থেকে শুরু হবে — সবচেয়ে গুরুত্বপূর্ণ nibble না সবচেয়ে কম গুরুত্বপূর্ণ nibble। বাস্তবে বেশিরভাগ compiler এই সিদ্ধান্তটা মেশিনের byte order-এর সাথে মিলিয়ে নেয় (little-endian মেশিনে সাধারণত a নিচের bit-এ বসে), কিন্তু এটা guarantee নয় — GCC আর MSVC ভিন্ন architecture-এ ভিন্ন আচরণ দেখাতে পারে। তাই bit-field দিয়ে সরাসরি network packet বা file format-এর header define করা একটা সুপরিচিত antipattern — portability কখনো নিশ্চিত থাকে না। যেখানে bit-level layout নিয়ন্ত্রণ জরুরি (protocol header), সেখানে explicit shift আর mask ব্যবহার করাই নিরাপদ — bit-field-কে compiler-এর বিবেচনার উপর ছেড়ে না দিয়ে।
- uint32_t x = 8080উৎস কোডে একটা মান — এখানে endianness অদৃশ্য
- CPU registerসংখ্যা হিসেবে ধরা থাকে, কোনো byte-order নেই — register-এর ভেতরে "order" অর্থহীন
- store to memoryএখানেই ISA-নির্ধারিত byte order প্রয়োগ হয় — x86 = little-endian
- raw bytes at address78 1F 00 00 — মেশিনের native order
- socket send()raw byte হুবহু কপি — কোনো conversion হয় না, প্রোগ্রামারের দায়িত্ব
- network wireযা পাঠানো হয়েছে তাই যায় — সংজ্ঞা অনুযায়ী কোনো "correct" order নেই এখানে
- receiver memoryযদি receiver-এর ধারণা ভিন্ন হয়, একই byte ভিন্ন সংখ্যা হয়ে যায়
উদাহরণ
একটা সম্পূর্ণ ওয়াকথ্রু
চলুন 0xDEADBEEF সংখ্যাটা নিয়ে দুই ক্রমেই memory address 0x2000–0x2003-এ বসাই।
| Address | Big-endian byte | Little-endian byte |
|---|---|---|
0x2000 | 0xDE | 0xEF |
0x2001 | 0xAD | 0xBE |
0x2002 | 0xBE | 0xAD |
0x2003 | 0xEF | 0xDE |
সহজ trick মনে রাখার জন্য: little-endian মানে হাতে লেখা hex-কে দুই-দুই করে byte-জোড়ায় ভেঙে পুরো ক্রমটা উল্টে দেওয়া। DE AD BE EF → উল্টে EF BE AD DE।
ফাইল ফরম্যাটে magic byte দিয়ে ঘোষণা
TIFF (image format) ফাইলের প্রথম দুই byte-ই বলে দেয় বাকি ফাইলটা কোন byte order-এ লেখা:
"II" (0x49 0x49) = "Intel" → little-endian
"MM" (0x4D 0x4D) = "Motorola" → big-endianএকটা TIFF parser প্রথমে এই দুই byte পড়ে, তারপর সিদ্ধান্ত নেয় বাকি ফাইলের প্রতিটা multi-byte field কীভাবে পড়তে হবে। এটা সমস্যাটার একটা সুন্দর সমাধান — ফাইল নিজেই তার convention ঘোষণা করছে, তাই একই format দুই architecture-এই native ভাবে লেখা ও পড়া যায় কোনো fixed নিয়ম ছাড়াই।
UTF-16 (আর UTF-32)-এও একই সমস্যা আছে — এক code unit একাধিক byte, তাই byte order লাগে। সমাধান একই স্টাইলের: BOM (Byte Order Mark), code point U+FEFF। ফাইলের শুরুতে যদি byte FF FE দেখেন, সেটা little-endian UTF-16; FE FF দেখলে big-endian। Unicode/UTF-8 লেসনে আমরা দেখেছি কেন UTF-8-এর নিজের এই সমস্যা নেই — UTF-8 প্রতিটা code unit-ই ১ byte, তাই “কোন ক্রমে byte সাজাব” প্রশ্নটাই ওঠে না। BOM শুধু multi-byte code unit-ওয়ালা encoding-এই (UTF-16, UTF-32) দরকার হয়, single-byte-unit encoding-এ (ASCII, UTF-8, Latin-1) অর্থহীন।
ELF binary-ও নিজের byte order ঘোষণা করে
Linux-এর executable format ELF (Executable and Linkable Format)-এর একদম প্রথম ১৬ byte-এর নাম e_ident, আর তার মধ্যে একটা byte (EI_DATA, index 5) থাকে যেটার মান 1 মানে little-endian, 2 মানে big-endian। Loader (kernel-এর সেই অংশ যেটা প্রোগ্রাম চালু করে) এই byte পড়েই বাকি header (entry point address, section table offset, ইত্যাদি) কোন byte order-এ interpret করতে হবে সিদ্ধান্ত নেয়। Level 3 (Assembly)-এ আমরা readelf -h-এ ঠিক এই byte-টা দেখব।
RIFF/WAV বনাম AIFF — একই সমস্যা, দুই ঐতিহ্যের দুই সমাধান
অডিও ফাইলের ইতিহাসেও এই একই যুদ্ধের ছাপ আছে। Microsoft/IBM-এর WAV ফরম্যাট RIFF (“Resource Interchange File Format”)-এর উপর ভিত্তি করে তৈরি — শুরু হয় magic byte "RIFF" দিয়ে, আর সব multi-byte সংখ্যা little-endian (x86 ঐতিহ্য মেনে)। Apple-এর পুরনো AIFF ফরম্যাট (Motorola 68k/PowerPC যুগের Mac-এর জন্য) শুরু হয় "FORM" দিয়ে, আর সব সংখ্যা big-endian (তৎকালীন Mac hardware-এর ঐতিহ্য মেনে)। দুটো ফরম্যাটের গঠন প্রায় একইরকম (chunk-ভিত্তিক), কিন্তু byte order উল্টো — যা প্রমাণ করে endianness-এর পছন্দ প্রযুক্তিগত প্রয়োজনীয়তা নয়, বরং যে হার্ডওয়্যার ইকোসিস্টেমে ফরম্যাটটা জন্মেছে তার ঐতিহ্য।
টার্মিনালে সরাসরি দেখুন — xxd দিয়ে raw byte পরীক্ষা
তত্ত্ব যথেষ্ট না — নিজের terminal-এ verify করা যাক। একটা ছোট ফাইল বানিয়ে তার raw byte দেখি:
python3 -c "import struct; open('demo.bin','wb').write(struct.pack('\<I', 0x12345678))"
xxd demo.binOutput:
00000000: 7856 3412 xV4.প্রথম কলাম offset, দ্বিতীয় অংশ raw hex byte। লক্ষ্য করুন byte sequence 78 56 34 12 — ঠিক little-endian layout, এই লেসনের একদম প্রথম figure-এর সাথে হুবহু মিলছে। ডান পাশের ASCII column-এ xV4. দেখাচ্ছে কারণ 0x78 = 'x', 0x56 = 'V', 0x34 = '4', 0x12-এর কোনো printable ASCII glyph নেই তাই .।
এবার একই মান big-endian-এ লিখে তুলনা করুন:
python3 -c "import struct; open('demo_be.bin','wb').write(struct.pack('>I', 0x12345678))"
xxd demo_be.bin00000000: 1234 5678 .4Vxএই দুইটা ফাইলের byte সম্পূর্ণ ভিন্ন, অথচ দুটোই “একই সংখ্যা” represent করে — এটাই এই পুরো লেসনের দাবিটার সবচেয়ে প্রত্যক্ষ প্রমাণ। Windows-এ xxd না থাকলে certutil -f /hex demo.bin বা PowerShell-এর Format-Hex demo.bin একই কাজ করে।
নিজে চালিয়ে দেখুন
আপনার মেশিনের native byte order বের করুন — Python
Python-এ sys.byteorder সরাসরি বলে দেয়, কিন্তু আমরা সেটা নিজেরাও verify করব byte-level manipulation দিয়ে:
import sys
import struct
print("sys.byteorder বলছে:", sys.byteorder)
# একটা int নিয়ে দুই ক্রমেই byte বানাই
x = 0x12345678
le_bytes = x.to_bytes(4, byteorder='little')
be_bytes = x.to_bytes(4, byteorder='big')
print(f"\nx = {hex(x)}")
print("little-endian bytes:", le_bytes.hex(' '))
print("big-endian bytes: ", be_bytes.hex(' '))
# struct module দিয়ে native (@) বনাম explicit (< বা >) তুলনা
native = struct.pack('@I', x) # @ = native byte order, alignment সহ
explicit_le = struct.pack('\<I', x)
explicit_be = struct.pack('>I', x)
print("\nstruct native ('@I'): ", native.hex(' '))
print("struct little ('\<I'): ", explicit_le.hex(' '))
print("struct big ('>I'): ", explicit_be.hex(' '))
if native == explicit_le:
print("\n→ এই মেশিন little-endian")
elif native == explicit_be:
print("\n→ এই মেশিন big-endian")প্রায় সব আধুনিক desktop/laptop/phone-এ (x86-64, ARM64) output:
sys.byteorder বলছে: little
x = 0x12345678
little-endian bytes: 78 56 34 12
big-endian bytes: 12 34 56 78
struct native ('@I'): 78 56 34 12
struct little ('\<I'): 78 56 34 12
struct big ('>I'): 12 34 56 78
→ এই মেশিন little-endian.to_bytes(4, byteorder='little') আর .to_bytes(4, byteorder='big')-এর ফলাফল মিলিয়ে দেখুন উপরের figure-এর table-এর সাথে — হুবহু মিলবে।
একটা মেশিনের byte order একটা runtime property, আর সেটা কোড দিয়ে সরাসরি পর্যবেক্ষণ করা যায় — শুধু document পড়ে অনুমান করতে হয় না।
Classic union trick — C-তে byte order নিজে দেখা
এটা দশকের পুরনো, বহুল ব্যবহৃত trick — একটা union-এ int আর char[]-কে একই জায়গায় overlap করানো:
#include \<stdio.h>
#include \<stdint.h>
union byte_check {
uint32_t as_int;
uint8_t as_bytes[4];
};
int main(void) {
union byte_check u;
u.as_int = 1; // শুধু সবচেয়ে কম গুরুত্বপূর্ণ byte-টাই non-zero
printf("as_int = %u\n", u.as_int);
printf("as_bytes = [%u, %u, %u, %u]\n",
u.as_bytes[0], u.as_bytes[1], u.as_bytes[2], u.as_bytes[3]);
if (u.as_bytes[0] == 1) {
printf("→ Little-endian: সবচেয়ে কম গুরুত্বপূর্ণ byte সবচেয়ে ছোট address-এ\n");
} else {
printf("→ Big-endian: সবচেয়ে বেশি গুরুত্বপূর্ণ byte সবচেয়ে ছোট address-এ\n");
}
return 0;
}gcc -O0 -o endian_check endian_check.c && ./endian_checkx86-64-এ output:
as_int = 1
as_bytes = [1, 0, 0, 0]
→ Little-endian: সবচেয়ে কম গুরুত্বপূর্ণ byte সবচেয়ে ছোট address-একেন এটা কাজ করে: union-এ সব member একই memory address share করে। as_int-এ 1 লেখার মানে হলো ৪টা byte-এর মধ্যে একটাতে 1 বসানো, বাকিগুলো 0। কোন byte-এ 1 বসলো — address 0-তে (index as_bytes[0]) নাকি address 3-তে (index as_bytes[3]) — সেটাই সরাসরি বলে দেয় CPU কোন ক্রম ব্যবহার করছে।
একই memory address-এ ভিন্ন type দিয়ে দেখলে (union) raw byte layout সরাসরি প্রকাশ পায় — এটাই hardware-level সত্য, ভাষার কোনো abstraction এখানে নেই।
htonl আসলে কী প্রমাণ করে — একটা real loopback socket দিয়ে দেখুন
আমাদের ল্যাপটপ/সার্ভার প্রায় সবই little-endian, তাই সত্যিকারের big-endian মেশিন হাতের কাছে পাওয়া কঠিন। কিন্তু আমরা byte-level parsing simulate করে ঠিক সেই bug-টা reproduce করতে পারি যেটা একটা real big-endian receiver দেখত:
import socket
import struct
import threading
def server(ready_event, received_bytes):
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('127.0.0.1', 0))
port = srv.getsockname()[1]
srv.listen(1)
ready_event.port = port
ready_event.set()
conn, _ = srv.accept()
data = conn.recv(4)
received_bytes.extend(data)
conn.close()
srv.close()
ready = threading.Event()
received = bytearray()
t = threading.Thread(target=server, args=(ready, received))
t.start()
ready.wait()
port = ready.port
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', port))
value = 8080
# ❌ ভুল: raw native-order byte পাঠানো (htonl ছাড়া)
wrong_bytes = struct.pack('@I', value) # native = little-endian এখানে
# ✓ সঠিক: network byte order (big-endian) এ পাঠানো
correct_bytes = struct.pack('!I', value) # '!' মানে struct module-এ network order
client.sendall(wrong_bytes)
t.join()
client.close()
print(f"পাঠানো হয়েছিল value = {value}")
print(f"raw বাইট যা পাঠানো হলো (native/little-endian): {wrong_bytes.hex(' ')}")
print(f"সঠিক network byte order হলে যা পাঠানো উচিত ছিল: {correct_bytes.hex(' ')}")
print("\n--- এখন ভাবুন receiver একটা প্রকৃত big-endian মেশিন, যে সঠিকভাবেই ntohl() ব্যবহার করছে ---")
misread = struct.unpack('!I', bytes(received))[0] # receiver network-order ধরে নিয়ে পড়ছে
correct_read = struct.unpack('@I', bytes(received))[0]
print(f"receiver যা bytes পেয়েছে: {bytes(received).hex(' ')}")
print(f"receiver network-order ধরে নিয়ে decode করলে পায়: {misread} ← ভুল!")
print(f"receiver যদি ভুলবশত native-order ধরে নিত: {correct_read} ← কাকতালীয়ভাবে ঠিক, কিন্তু ভুল ধারণায়")প্রত্যাশিত output:
পাঠানো হয়েছিল value = 8080
raw বাইট যা পাঠানো হলো (native/little-endian): 90 1f 00 00
সঠিক network byte order হলে যা পাঠানো উচিত ছিল: 00 00 1f 90
--- এখন ভাবুন receiver একটা প্রকৃত big-endian মেশিন, যে সঠিকভাবেই ntohl() ব্যবহার করছে ---
receiver যা bytes পেয়েছে: 90 1f 00 00
receiver network-order ধরে নিয়ে decode করলে পায়: 2416967680 ← ভুল!
receiver যদি ভুলবশত native-order ধরে নিত: 8080 ← কাকতালীয়ভাবে ঠিক, কিন্তু ভুল ধারণায়এখানেই আসল শিক্ষা: misread মান 2,416,967,680 — সংখ্যাটা 8080-এর ধারেকাছেও না। একটা সঠিকভাবে-লেখা receiver (যেটা POSIX socket convention মেনে সবসময় ntohl ব্যবহার করে) ঠিক এই ভুল সংখ্যাটাই পাবে, যদি sender htonl ব্যবহার না করে। আর সবচেয়ে বিপজ্জনক অংশ: এই bug little-endian-থেকে-little-endian টেস্টিং-এ কখনো ধরা পড়বে না, কারণ struct.pack('@I', ...) (native) আর প্রকৃত receiver-এর ধারণা যদি দুটোই little-endian হয়, ভুলটা নিজেকে লুকিয়ে রাখে।
htonl বাদ দিলে raw byte-ই পাঠানো হয় — receiver যদি সেই byte-কে ভিন্ন convention-এ পড়ে, একই bit pattern সম্পূর্ণ ভিন্ন সংখ্যা হয়ে যায়। এটা কোনো তাত্ত্বিক দাবি না, নিজের মেশিনে সরাসরি পরিমাপযোগ্য।
নিজে বানান
Endian-swap ফাংশন লিখুন এবং round-trip verify করুন
- একটা swap16() ফাংশন লিখুন যা একটা 16-bit value-র দুই byte অদলবদল করে
- একটা swap32() ফাংশন লিখুন যা 32-bit value-র চারটা byte সম্পূর্ণ উল্টে দেয়
- যাচাই করুন swap32(swap32(x)) == x সবসময় সত্য (round-trip property)
- নিজের swap32() -এর output স্ট্যান্ডার্ড library (Python struct বা C htonl) -এর সাথে মিলিয়ে দেখুন
- একটা htonl_manual() লিখুন যা শুধু native byte order জানলেই কাজ করে — network byte order-এ রূপান্তর করে
মূল ধারণা: byte swap মানে bit-shift আর mask দিয়ে byte-গুলোকে বের করে উল্টো ক্রমে আবার জোড়া লাগানো।
আগে হাতে-কলমে হিসাব করে দেখি — swap32 কীভাবে কাজ করে
swap32(x) কোডটা চারটা পদের যোগফল। প্রতিটা পদ একটা byte-কে তার নতুন জায়গায় বসায়। x = 0x12345678 ধরে প্রতিটা পদ আলাদাভাবে হিসাব করি:
পদ ১: (x \<\< 24) & 0xFF000000
x \<\< 24 করলে সবচেয়ে নিচের byte (0x78) সবচেয়ে উপরে চলে যায়
0x12345678 \<\< 24 = 0x78000000 (উপচে যাওয়া উপরের byte-গুলো ৩২-বিটে কাটা পড়ে)
mask করার পর = 0x78000000
পদ ২: (x \<\< 8) & 0x00FF0000
0x12345678 \<\< 8 = 0x34567800
mask করার পর = 0x00560000 ← খেয়াল করুন: শুধু তৃতীয় byte (0x56) বাঁচল
পদ ৩: (x >> 8) & 0x0000FF00
0x12345678 >> 8 = 0x00123456
mask করার পর = 0x00003400 ← শুধু দ্বিতীয় byte (0x34) বাঁচল
পদ ৪: (x >> 24) & 0x000000FF
0x12345678 >> 24 = 0x00000012
mask করার পর = 0x00000012 ← MSB (0x12) এখন সবচেয়ে নিচে
যোগফল: 0x78000000
| 0x00560000
| 0x00003400
| 0x00000012
─────────────
= 0x78563412 ✓ (0x12345678-এর ঠিক byte-reversed রূপ)প্রতিটা পদ একটামাত্র byte-কে “বাঁচিয়ে” রাখে (mask দিয়ে বাকি সব শূন্য করে দিয়ে) আর shift দিয়ে সেই byte-কে তার নতুন target position-এ নিয়ে যায়। চারটা পদ একসাথে OR (বা যোগ, কারণ কোনো bit ওভারল্যাপ করে না) করলে সম্পূর্ণ byte-reversed সংখ্যা পাওয়া যায়। এই বিশ্লেষণটা মনে রাখলে shift-mask কোড আর মুখস্থ করা সূত্র মনে হবে না — প্রতিটা লাইনের যুক্তি স্পষ্ট থাকবে।
C-তে:
#include \<stdio.h>
#include \<stdint.h>
uint16_t swap16(uint16_t x) {
return (uint16_t)((x \<\< 8) | (x >> 8));
}
uint32_t swap32(uint32_t x) {
return ((x \<\< 24) & 0xFF000000) |
((x \<\< 8) & 0x00FF0000) |
((x >> 8) & 0x0000FF00) |
((x >> 24) & 0x000000FF);
}
int main(void) {
uint32_t x = 0x12345678;
uint32_t swapped = swap32(x);
uint32_t back = swap32(swapped);
printf("original : 0x%08x\n", x);
printf("swapped : 0x%08x\n", swapped);
printf("swap(swap(x)) == x ? %s\n", (back == x) ? "হ্যাঁ" : "না");
// যাচাই: 16-bit-ও একইভাবে কাজ করে
uint16_t y = 0xABCD;
printf("\nswap16(0x%04x) = 0x%04x\n", y, swap16(y));
return 0;
}প্রত্যাশিত output:
original : 0x12345678
swapped : 0x78563412
swap(swap(x)) == x ? হ্যাঁ
swap16(0xabcd) = 0xcdabPython-এ (আর standard library-র সাথে মিলিয়ে যাচাই):
def swap16(x: int) -> int:
return ((x \<\< 8) | (x >> 8)) & 0xFFFF
def swap32(x: int) -> int:
return (((x \<\< 24) & 0xFF000000) |
((x \<\< 8) & 0x00FF0000) |
((x >> 8) & 0x0000FF00) |
((x >> 24) & 0x000000FF))
def htonl_manual(x: int) -> int:
"""host থেকে network byte order — যদি host little-endian হয় তবেই swap লাগবে"""
import sys
if sys.byteorder == 'little':
return swap32(x)
return x # host already big-endian, network order = big-endian, কিছু করার নেই
# ── যাচাই ──────────────────────────────────────────
import struct, socket, random
x = 0x12345678
assert swap32(swap32(x)) == x, "round-trip ব্যর্থ!"
print("round-trip property: পাস ✓")
# আমাদের swap32 আর struct module মিলছে কি না
ours = swap32(x).to_bytes(4, 'big')
theirs = struct.pack('\<I', x) # little-endian bytes == big-endian value swapped
assert ours == theirs
print("struct module-এর সাথে মিলেছে ✓")
# আমাদের htonl_manual আর আসল socket.htonl মিলছে কি না
for _ in range(1000):
v = random.randint(0, 0xFFFFFFFF)
assert htonl_manual(v) == socket.htonl(v)
print("socket.htonl()-এর সাথে ১০০০টা random মান মিলেছে ✓")প্রত্যাশিত output:
round-trip property: পাস ✓
struct module-এর সাথে মিলেছে ✓
socket.htonl()-এর সাথে ১০০০টা random মান মিলেছে ✓নিজে বাড়ান:
swap64()লিখুনuint64_t/Pythonint-এর জন্য, একই shift-mask প্যাটার্ন বাড়িয়ে- একটা generic
swap_bytes(data: bytes) -> bytesলিখুন যা যেকোনো length-এর byte string উল্টে দেয় (কোনো fixed size ধরে না নিয়ে) — এটাarray[::-1]স্লাইসিং দিয়ে এক লাইনেও হয়, কিন্তু আগে shift-mask পদ্ধতিতে বোঝাটাই আসল লক্ষ্য - একটা IEEE 754
floatনিয়ে দেখুন byte-swap করলে কী হয় —struct.pack('\<f', 3.14)বনামstruct.pack('>f', 3.14)-এর byte তুলনা করুন; float-এর bit pattern-ও ঠিক integer-এর মতোই byte-order-নির্ভর, কারণ শেষমেশ এটাও শুধু ৩২/৬৪টা bit-এর একটা sequence - প্রমাণ করুন
swap16(swap16(x)) != xকখনো হয় না, formally: প্রতিটা byte ঠিক দুইবার সরানো হয়, তাই এটা তার নিজের inverse (একটা involution) - (চ্যালেঞ্জ — পরের লেসনের প্রস্তুতি) একটা ছোট self-describing ফাইল ফরম্যাট ডিজাইন করুন, TIFF-এর
II/MMকৌশল অনুকরণ করে
এই শেষ ধাপটা একটু বিস্তারিত করি, কারণ এটা সরাসরি পরের লেসনের ভূমিকা।
লক্ষ্য: একটা ফাইল লিখুন যেটার প্রথম byte-ই ঘোষণা করে বাকি ফাইলটা কোন byte order-এ লেখা — তারপর একটা reader লিখুন যেটা সেই ঘোষণা মেনে সঠিকভাবে পড়ে, নিজের মেশিনের native order যাই হোক না কেন।
import struct
import sys
MAGIC_LITTLE = 0x01
MAGIC_BIG = 0x02
def write_self_describing(path: str, value: int, order: str):
"""order: 'little' বা 'big' — লেখকের ইচ্ছামতো, native order-এর সাথে সম্পর্কহীন"""
marker = MAGIC_LITTLE if order == 'little' else MAGIC_BIG
fmt = '\<I' if order == 'little' else '>I'
with open(path, 'wb') as f:
f.write(bytes([marker])) # ১ byte: ঘোষণা
f.write(struct.pack(fmt, value)) # ৪ byte: আসল মান, ঘোষিত order-এ
def read_self_describing(path: str) -> int:
"""যেকোনো মেশিনে, যেকোনো native order থাকলেও সঠিকভাবে পড়বে"""
with open(path, 'rb') as f:
marker = f.read(1)[0]
raw = f.read(4)
fmt = '\<I' if marker == MAGIC_LITTLE else '>I'
return struct.unpack(fmt, raw)[0]
# যাচাই: দুইভাবেই লিখে, দুইভাবেই পড়ে দেখাই মান সবসময় সঠিক থাকে
write_self_describing('as_little.bin', 0xDEADBEEF, 'little')
write_self_describing('as_big.bin', 0xDEADBEEF, 'big')
print("little-endian ফাইল থেকে ফেরত:", hex(read_self_describing('as_little.bin')))
print("big-endian ফাইল থেকে ফেরত: ", hex(read_self_describing('as_big.bin')))
print("এই মেশিনের native order: ", sys.byteorder)প্রত্যাশিত output (যেকোনো মেশিনে, native order নির্বিশেষে):
little-endian ফাইল থেকে ফেরত: 0xdeadbeef
big-endian ফাইল থেকে ফেরত: 0xdeadbeef
এই মেশিনের native order: littleদুই ফাইলের raw byte সম্পূর্ণ ভিন্ন (01 EF BE AD DE বনাম 02 DE AD BE EF), কিন্তু দুটোই সঠিকভাবে একই মান-এ ফিরে আসে — কারণ ফাইল নিজেই তার convention বহন করছে, reader-কে অনুমান করতে হচ্ছে না। এটাই পরের লেসনে আমরা যে নীতি নিয়ে কাজ করব তার সবচেয়ে ছোট সম্ভব সংস্করণ: self-describing binary format।
বাস্তব সিস্টেমে
বাস্তব সিস্টেমে endianness
BSD Sockets API — htonl/ntohl/htons/ntohs। TCP/IP-এর প্রতিটা header field (port number, IP address-এর কিছু অংশ, ইত্যাদি) network byte order-এ (big-endian) পাঠাতে হয় — এটাই RFC 1700-এ আনুষ্ঠানিক। যেহেতু আজকের প্রায় সব desktop/server little-endian (x86-64), প্রতিটা socket program-কে explicit conversion করতে হয়: htonl (host to network, long/32-bit), ntohl (network to host), htons/ntohs (16-bit, port number-এর জন্য)। Big-endian মেশিনে এই ফাংশনগুলো কার্যত no-op — কিন্তু কোড লেখার সময় সবসময় কল করতে হয়, কারণ কোড কোন মেশিনে চলবে সেটা আগে থেকে জানা যায় না।
TIFF ফাইল ফরম্যাট। II/MM magic byte দিয়ে ফাইল নিজের byte order ঘোষণা করে — আমরা “উদাহরণ” অংশে দেখেছি। Adobe Photoshop, camera RAW format-এর ভিত্তি (অনেক camera RAW format TIFF-based)।
BMP (Windows Bitmap)। সম্পূর্ণ ফাইল সবসময় little-endian — Microsoft-এর নিজস্ব ফরম্যাট, x86-এর সাথে জন্ম, কখনো byte-order flag রাখার দরকার হয়নি কারণ target platform সবসময় নির্দিষ্ট ছিল।
PNG ফাইল ফরম্যাট। সব multi-byte integer field (chunk length, CRC checksum) সবসময় big-endian — নেটওয়ার্ক ঐতিহ্য মেনে, platform-নিরপেক্ষ থাকার জন্য ইচ্ছাকৃতভাবে fix করা।
Java class file / JVM। .class ফাইলের সব multi-byte সংখ্যা big-endian — JVM design-এ platform independence-ই মূল লক্ষ্য ছিল, তাই একটা fix করা convention বেছে নেওয়া হয়েছে। এই সিদ্ধান্তটা Java-র standard library-তেও ছড়িয়ে আছে — DataInputStream/DataOutputStream-এর readInt()/writeInt() সবসময় big-endian ধরে নেয়, কোনো parameter দিয়ে বদলানো যায় না:
DataOutputStream out = new DataOutputStream(new FileOutputStream("data.bin"));
out.writeInt(305419896); // সবসময় big-endian লেখে, platform নির্বিশেষে
out.close();যদি Java প্রোগ্রামারকে একটা little-endian ফরম্যাটের (যেমন একটা x86-origin file) সাথে কাজ করতে হয়, ByteBuffer.order(ByteOrder.LITTLE_ENDIAN) দিয়ে explicit override করতে হয় — Java-র ডিফল্ট নীরবে সবসময় big-endian, আর এটা না জানলে নতুন Java developer-রা প্রায়ই এই একই bug-এ পড়েন।
Bitcoin protocol-এর মিশ্র byte order। আগে “hood” section-এ যেমন দেখলাম — বেশিরভাগ integer field little-endian, কিন্তু hash (block hash, transaction ID) প্রথাগতভাবে byte-reversed (“big-endian” দেখানো, historical কারণে) প্রদর্শিত হয়। এই অসামঞ্জস্যতা এত বিখ্যাত যে Bitcoin Core-এর ডকুমেন্টেশনে আলাদা করে সতর্ক করা থাকে।
Modbus (industrial control protocol)। স্ট্যান্ডার্ড Modbus register বিগ-এন্ডিয়ান, কিন্তু বাস্তবে অনেক PLC vendor floating-point value পাঠানোর সময় word-swapped (“mid-little endian”) ফরম্যাট ব্যবহার করে — দুইটা ১৬-বিট register-এর মধ্যে ভেতরের byte order আর register-এর ক্রম আলাদাভাবে উল্টে যায়। একটা IEEE 754 float (৪ byte, দুইটা ১৬-বিট Modbus register-এ ভাগ হয়ে থাকে) চারটা ভিন্নভাবে সাজানো যেতে পারে:
byte: B3 B2 B1 B0 (মূল big-endian float, MSB প্রথমে)
Big-endian: B3 B2 B1 B0 ← "স্ট্যান্ডার্ড" Modbus
Little-endian: B0 B1 B2 B3 ← পুরোপুরি উল্টো
Word-swapped: B1 B0 B3 B2 ← প্রতিটা register-এর ভেতরে little, কিন্তু register-এর ক্রম অপরিবর্তিত
Byte-swapped: B2 B3 B0 B1 ← register-এর ক্রম উল্টো, কিন্তু ভেতরের byte অপরিবর্তিতচারটাই বাস্তবে কোনো-না-কোনো vendor-এর device-এ পাওয়া যায় — কোনো single standard নেই। Industrial automation ইঞ্জিনিয়াররা এই bug-টার সাথে নিয়মিত লড়াই করেন; একটা নতুন PLC device integrate করার সময় প্রথম কাজই হলো trial-and-error করে বের করা কোন চারটার মধ্যে কোনটা সেই device ব্যবহার করছে, কারণ ভুল combination-এ একটা সম্পূর্ণ valid-দেখতে ভাসমান বিন্দু সংখ্যা বেরোয় (crash করে না, শুধু ভুল সংখ্যা দেয়) — ঠিক এই কারণেই এই bug ধরা কঠিন।
Apple-এর PowerPC → Intel transition (2006)। Mac যখন PowerPC (big-endian) থেকে Intel (little-endian)-এ সরে যায়, তখন সব file format, saved document, আর third-party plugin-এর জন্য endianness একটা বাস্তব migration সমস্যা হয়ে দাঁড়ায় — Apple-এর “Universal Binary” পদ্ধতি একই application-এর দুই architecture-এর জন্য আলাদা কোড bundle করে দিত, কিন্তু data file-এর byte order প্রতিটা application developer-কে নিজে সামলাতে হয়েছে।
Wireshark/tcpdump-এ প্যাকেট বিশ্লেষণ। যখন আপনি Wireshark দিয়ে একটা captured packet-এর raw hex bytes দেখেন, প্রতিটা protocol field-কে সঠিকভাবে decode করতে টুলটাকে জানতে হয় কোন field কোন byte order-এ আছে — IP/TCP header সবসময় big-endian ধরে নেয়, কিন্তু কিছু higher-layer application protocol (যেমন SMB, যেটা মূলত Windows/x86 ঐতিহ্য থেকে এসেছে) সরাসরি little-endian ব্যবহার করে। Wireshark-এর protocol dissector-গুলোর ভেতরে তাই প্রতিটা field-এর জন্য explicit byte-order metadata থাকে — এটা ভুল হলে packet ভুল ব্যাখ্যা হবে, যদিও raw byte নিখুঁত ঠিক আছে।
Hardware-level সমর্থন — bswap/rev instruction। যেহেতু byte-swap এত ঘনঘন দরকার হয় (network I/O, cross-platform file parsing), CPU designer-রা এটাকে একটা single, hardware-accelerated instruction বানিয়ে দিয়েছেন — x86-এ bswap, ARM-এ rev। কম্পাইলার আপনার htonl()/__builtin_bswap32() কলকে এই একটা instruction-এ translate করে দেয় — তাই “endian conversion ধীর” এই ভয়টা অমূলক; বাস্তবে এটা প্রায় বিনামূল্যে (এক CPU cycle-এর কাছাকাছি)।
যে ভুলগুলো সবাই করে
“Little-endian একটা পুরনো/ভুল ডিজাইন সিদ্ধান্ত — big-endian বেশি 'স্বাভাবিক' কারণ মানুষ msb আগে পড়ে।”
এটা একটা সাধারণ প্রথম-প্রতিক্রিয়া, কিন্তু ভুল। দুটোরই বাস্তব প্রকৌশলগত যুক্তি আছে।
Little-endian-এর সুবিধা: একই address থেকে একটা value-কে ছোট type হিসেবে পড়লে সঠিক নিচের byte-টাই পাওয়া যায়, কোনো offset হিসাব ছাড়াই (আমরা “concept” section-এ এটা দেখেছি)। এটা multi-precision arithmetic আর কিছু compiler optimization-এ সুবিধা দেয়।
Big-endian-এর সুবিধা: raw byte-এর ক্রমই সংখ্যার স্বাভাবিক (মানুষের পড়ার) ক্রম, তাই hex dump পড়া সহজ, আর network protocol-এ header parse করার সময় byte-গুলো ক্রমানুসারে আসে।
দুটোই স্থাপত্যগত সিদ্ধান্ত, কোনোটাই প্রযুক্তিগতভাবে “ভুল” নয়। যেটা সত্য তা হলো — x86 জনপ্রিয় হওয়ায় little-endian আজ বাস্তবে প্রভাবশালী, কিন্তু network byte order আজও big-endian, কারণ সেই standard x86 প্রভাবশালী হওয়ার আগেই স্থির হয়ে গিয়েছিল।
“Endianness শুধু integer-এর ব্যাপার — text আর float-এ প্রযোজ্য না।”
ভুল। যেকোনো multi-byte value-ই endianness-এর অধীন — এটা integer-নির্দিষ্ট কোনো নিয়ম নয়, বরং “একাধিক byte দিয়ে একটা value গঠিত হলে সেগুলো কোন ক্রমে সাজবে” প্রশ্নের সাধারণ উত্তর।
IEEE 754 float/double-ও ঠিক integer-এর মতোই একটা bit pattern যেটা কয়েকটা byte-এ ভাগ হয়ে থাকে — তাই সেটাও endianness-নির্ভর। struct.pack('\<f', 3.14) আর struct.pack('>f', 3.14) ভিন্ন byte দেয়।
Single-byte text encoding (ASCII, Latin-1, UTF-8) endianness-এর বাইরে, কারণ প্রতিটা code unit-ই ১ byte — সাজানোর কোনো প্রশ্ন নেই। কিন্তু multi-byte code unit-ওয়ালা text encoding (UTF-16, UTF-32) ঠিক integer-এর মতোই endianness-নির্ভর, আর এজন্যই BOM দরকার হয়।
নিয়ম: প্রশ্ন করুন “এই value কি একের বেশি byte-এ গঠিত?” — উত্তর হ্যাঁ হলে, endianness প্রাসঙ্গিক।
“ASCII/UTF-8 টেক্সট ফাইলেও endianness নিয়ে ভাবা দরকার।”
না — এটাই আগের misconception-এর বিপরীত দিক, আর সমান গুরুত্বপূর্ণ। ASCII আর UTF-8-এ প্রতিটা “unit” ঠিক ১ byte, তাই এই সংখ্যাগুলো সাজানোর কোনো প্রশ্নই ওঠে না। একটা byte-কে “উল্টো ক্রমে” রাখার কোনো অর্থ নেই — উল্টানোর জন্য অন্তত দুইটা byte লাগে।
এই কারণেই UTF-8 কে অনেকে “endianness-নিরাপদ” বলেন, আর এটা UTF-8-এর একটা প্রকৃত ব্যবহারিক সুবিধা — নেটওয়ার্কে বা ফাইলে UTF-8 টেক্সট পাঠাতে কখনো BOM বা byte-order ঘোষণার দরকার হয় না, যেটা UTF-16/UTF-32-এ বাধ্যতামূলক।
“'Network byte order' মানে ইন্টারনেটে যাওয়া সব ডেটাই big-endian-এ থাকে।”
না। শুধু protocol header-এর fixed-format field (IP header-এর length/TTL, TCP header-এর port/sequence number, ইত্যাদি) network byte order মানতে বাধ্য — কারণ RFC সেগুলোর জন্য নির্দিষ্ট বাইনারি লেআউট সংজ্ঞায়িত করে, আর সব বাস্তবায়নকে (router, OS, application) একমত হতে হয়।
কিন্তু payload — মানে application যা পাঠাচ্ছে — সম্পূর্ণ application-এর সিদ্ধান্ত। একটা HTTP response-এর body যদি JSON টেক্সট হয়, সেখানে endianness প্রশ্নই ওঠে না (টেক্সট, তাই)। একটা custom binary protocol যদি payload-এ integer পাঠায়, সেই protocol নিজের byte order বেছে নিতে পারে — অনেক আধুনিক application protocol (যেমন Protocol Buffers-এর wire format varint encoding) ইচ্ছাকৃতভাবে little-endian ব্যবহার করে, কারণ আজকের দুনিয়ায় প্রায় সব endpoint-ই little-endian, আর সেক্ষেত্রে conversion overhead অপ্রয়োজনীয়।
সংক্ষেপে: “network byte order” একটা নির্দিষ্ট ঐতিহাসিক স্তরের (IP/TCP header) নিয়ম, প্রতিটা bit যা ইন্টারনেটে ভ্রমণ করে তার সার্বজনীন আইন নয়।
বুঝেছেন কি না দেখুন
10xCAFEBABE সংখ্যাটা memory address 0x3000–0x3003-এ big-endian আর little-endian দুই ক্রমেই লিখুন। কোন byte কোথায় বসবে তা স্পষ্টভাবে দেখান।
যুক্তি
0xCAFEBABE সংখ্যাটা memory address 0x3000–0x3003-এ big-endian আর little-endian দুই ক্রমেই লিখুন। কোন byte কোথায় বসবে তা স্পষ্টভাবে দেখান।সংখ্যাটার চারটা byte (সবচেয়ে গুরুত্বপূর্ণ থেকে সবচেয়ে কম): CA, FE, BA, BE।
Big-endian — MSB প্রথমে, তাই সংখ্যা যেভাবে লেখা হয়েছে ঠিক সেই ক্রমেই address বাড়ে:
| Address | Byte |
|---|---|
0x3000 | 0xCA |
0x3001 | 0xFE |
0x3002 | 0xBA |
0x3003 | 0xBE |
Little-endian — LSB প্রথমে, তাই ক্রম উল্টে যায়:
| Address | Byte |
|---|---|
0x3000 | 0xBE |
0x3001 | 0xBA |
0x3002 | 0xFE |
0x3003 | 0xCA |
পরীক্ষা করার সহজ উপায়: little-endian-এর byte sequence (BE BA FE CA) হলো big-endian sequence-এর (CA FE BA BE) ঠিক বিপরীত ক্রম — কোনো byte-এর ভেতরের bit বদলায় না, শুধু byte-গুলোর আপেক্ষিক ক্রম উল্টায়। এই কারণেই 0xCAFEBABE-এর মতো “readable” hex constant প্রায়ই debugging magic number হিসেবে ব্যবহার হয় — memory dump-এ চেনা সহজ, আর endianness ভুল হলে সেটাও চোখে পড়ে (BE BA FE CA vs CA FE BA BE আলাদা দেখতে)।
2নিচের C কোডটা x86-64 (little-endian) মেশিনে চালালে output কী হবে? পুরো reasoning দেখান।
uint16_t x = 0x00FF;
uint8_t *p = (uint8_t *)&x;
printf("%d %d\n", p[0], p[1]);
প্রয়োগ
uint16_t x = 0x00FF;
uint8_t *p = (uint8_t *)&x;
printf("%d %d\n", p[0], p[1]);x = 0x00FF-এর দুইটা byte: most significant byte 0x00, least significant byte 0xFF।
Little-endian মেশিনে LSB সবচেয়ে ছোট address-এ থাকে। p[0] মানে x-এর ঠিকানার প্রথম byte (সবচেয়ে ছোট address), p[1] তার পরের byte।
তাই:
p[0]= LSB =0xFF= decimal255p[1]= MSB =0x00= decimal0
Output:
255 0যদি এই একই কোড big-endian মেশিনে চলত, ফলাফল উল্টো হতো: p[0] = 0 (MSB প্রথমে), p[1] = 255।
গুরুত্বপূর্ণ পর্যবেক্ষণ: x-এর মান (255, decimal-এ) কোনোভাবেই বদলায়নি — x সবসময় 0x00FF = 255। যেটা বদলেছে সেটা হলো raw byte-কে কোন ক্রমে array হিসেবে দেখা হচ্ছে। এটাই এই পুরো লেসনের মূল কথা — value নিজে endianness-নিরপেক্ষ, শুধু সেটার byte representation-ই endianness-নির্ভর।
3Network byte order হিসেবে big-endian কেন বেছে নেওয়া হয়েছিল, যেখানে আজ পৃথিবীর অধিকাংশ কম্পিউটারই little-endian? এটা কি একটা ভুল সিদ্ধান্ত ছিল?
যুক্তি
সংক্ষিপ্ত উত্তর: না, এটা ভুল ছিল না — এটা historical timing-এর ব্যাপার, আর choice-টা arbitrary হলেও প্রয়োজনীয় ছিল।
TCP/IP-এর মূল standard (RFC-গুলো) তৈরি হয় ১৯৭০-এর দশকে ARPANET-এর যুগে, যখন নেটওয়ার্কে যুক্ত মেশিনগুলোর architecture ছিল বৈচিত্র্যময় — কিছু big-endian (যেমন তখনকার অনেক mainframe/mini), কিছু little-endian। x86 তখনও বাজারে প্রভাবশালী হয়নি (Intel 8086 আসে ১৯৭৮-এ, IBM PC ১৯৮১-এ) — তাই “সবচেয়ে জনপ্রিয় architecture-এর convention মেনে নিই” এই যুক্তিটা তখন খাটতো না।
সমস্যাটা আসলে choice নিয়ে না — কোনো একটা fixed convention দরকার ছিল, যেটাতে সব architecture-ই একমত হতে পারে, যাতে একটা IP packet যেকোনো দুই মেশিনের মধ্যে সঠিকভাবে interpret হয়। Danny Cohen-এর মূল যুক্তিই ছিল এটা — কোনটা “ভালো” তার তর্কে সময় নষ্ট না করে, একটা সিদ্ধান্তে স্থির হওয়াই গুরুত্বপূর্ণ। Big-endian বেছে নেওয়া হয় — সম্ভবত কারণ তৎকালীন প্রভাবশালী network hardware (যেমন কিছু Xerox/DEC সিস্টেম) big-endian ছিল, এবং raw byte order মানুষের পড়ার স্বাভাবিক ক্রমের সাথে মেলে, hex dump/protocol analyzer দিয়ে debug করা সহজ হয়।
আজকের ফলাফল: x86/ARM প্রভাবশালী হওয়ায় বেশিরভাগ endpoint little-endian, কিন্তু IP/TCP header এখনো big-endian — এটা বদলানো সম্ভব না, কারণ পুরো internet-এর প্রতিটা router/OS এই ধরে নেয়। ফলে প্রতিটা little-endian মেশিনকে সবসময় htonl/ntohl দিয়ে conversion করতে হয় — একটা ছোট কিন্তু চিরস্থায়ী runtime cost, যেটা একবার নেওয়া সিদ্ধান্তের ফল।
শিক্ষা: distributed protocol design-এ একবার একটা convention স্থির হয়ে গেলে, লক্ষ লক্ষ ডিভাইস সেটার উপর নির্ভরশীল হয়ে যায়, ফলে সেটা বদলানোর খরচ প্রায় অসীম হয়ে যায় — এমনকি যদি প্রযুক্তিগতভাবে “ভালো” বিকল্প থাকেও। IPv4-এর ৩২-বিট address space নিয়ে একই সমস্যা হয়েছিল।
4আপনি একটা নতুন বাইনারি ফাইল ফরম্যাট ডিজাইন করছেন যেটা x86-64, ARM64, আর সম্ভাব্য কিছু legacy big-endian সিস্টেমেও পড়া/লেখা যেতে হবে। Endianness নিয়ে আপনার ডিজাইন সিদ্ধান্ত কী হবে, আর কেন? দুইটা ভিন্ন কৌশল তুলনা করে সিদ্ধান্ত নিন।
ডিজাইন
দুইটা বৈধ কৌশল আছে, প্রতিটার trade-off ভিন্ন।
কৌশল ১ — Fixed byte order (সবসময় একটাই, ঘোষণা ছাড়াই)
ফরম্যাট স্পেসিফিকেশনে লিখে দিন: “সব multi-byte field সবসময় little-endian” (অথবা big-endian — যেকোনো একটা, কিন্তু consistently)।
সুবিধা: সহজতম বাস্তবায়ন, কোনো runtime branching লাগে না, parser code সবচেয়ে ছোট।
অসুবিধা: যে মেশিনের native order ভিন্ন, সেখানে প্রতিটা read/write-এ conversion (byte-swap) লাগবে — একটা constant overhead, যদিও আধুনিক CPU-তে এটা প্রায় বিনামূল্যে (single instruction, bswap)।
কৌশল ২ — Self-describing (magic byte দিয়ে ঘোষণা, TIFF-এর মতো)
ফাইলের প্রথম কয়েক byte-এ একটা magic marker রাখুন যা পড়ার সময় ঘোষণা করে ফাইলটা কোন byte order-এ লেখা হয়েছে (যেমন TIFF-এর II/MM, বা UTF-16-এর BOM)।
সুবিধা: যে মেশিনেই লেখা হোক, সেই মেশিনের native order-এই লেখা যায় — লেখার সময় কোনো conversion লাগে না। যে মেশিনে পড়া হচ্ছে, শুধু তখনই দরকার হলে conversion করে।
অসুবিধা: parser জটিল — প্রতিটা multi-byte read-এর আগে branch করতে হয় (“যদি ঘোষিত order আমার native order-এর সাথে না মেলে, swap করো”)। প্রতিটা tool/library-কে এই logic সঠিকভাবে বাস্তবায়ন করতে হয়, আর ভুল হলে silent data corruption হয়।
আমার সুপারিশ — পরিস্থিতির উপর নির্ভর করে:
- ফরম্যাটটা যদি মূলত একটা নির্দিষ্ট ecosystem-এর জন্য (যেমন একটা নির্দিষ্ট game engine-এর save file, যেখানে সব target platform জানা), fixed order যথেষ্ট এবং সরলতম।
- ফরম্যাটটা যদি সত্যিকারের cross-platform আদান-প্রদানের জন্য (যেমন একটা image/document format যেটা camera, phone, desktop, server — সব জায়গায় দেখা যাবে), self-describing কৌশল ভালো — বিশেষত embedded/legacy hardware যেখানে conversion cost বেশি গুরুত্বপূর্ণ হতে পারে।
- একটা তৃতীয় বাস্তবসম্মত বিকল্প: fixed order বেছে নিন, কিন্তু network byte order (big-endian) বা “সবচেয়ে প্রচলিত” (আজ little-endian) কোনটা বাছবেন তা নিয়ে ভাবুন। আজকের বাস্তবতায় little-endian বেছে নেওয়াই বাস্তবসম্মত — কারণ প্রায় সব আধুনিক endpoint native little-endian, conversion overhead প্রায় সবসময় শূন্য হবে। এটাই Protocol Buffers, FlatBuffers-এর মতো আধুনিক ফরম্যাট করেছে — legacy network-convention (big-endian) না মেনে ইচ্ছাকৃতভাবে little-endian বেছেছে।
যা করবেন না: কোনো convention না বেছে “যে মেশিনে লেখা হয়েছে সেই মেশিনের native order” ধরে নেওয়া (অর্থাৎ কোনো convention-ই না রাখা)। এটাই আগের section-এ দেখা fwrite(&struct, ...) bug-এর মূল কারণ — implicit “convention” আসলে কোনো convention না, শুধু একটা দুর্ঘটনা যেটা একই মেশিনে টেস্ট করলে ধরা পড়ে না।
5একজন developer রিপোর্ট করছেন: “আমার client x86-64 লিনাক্সে ঠিকমতো কাজ করছে, কিন্তু একটা big-endian embedded device-এর সাথে communicate করার সময় port number ভুল আসছে — port 8080 চাওয়ার জায়গায় device একটা বিশাল random number দেখছে।” কোড থেকে bug খুঁজে বের করুন এবং ঠিক করুন।
uint16_t port = 8080;
struct sockaddr_in addr;
addr.sin_port = port; // বাগ কোথায়?
প্রয়োগ
uint16_t port = 8080;
struct sockaddr_in addr;
addr.sin_port = port; // বাগ কোথায়?বাগটা: sin_port-এ port number সরাসরি assign করা হয়েছে, কোনো htons() কল ছাড়াই।
কেন এটা x86-64-এ “কাজ করে” (আসলে করে না, শুধু ভাগ্যক্রমে ধরা পড়ে না): যদি client আর server দুটোই little-endian হয়, আর দুই পাশেই এই একই ভুল থাকে (কোথাও htons/ntohs কল না করা), তাহলে raw byte যেভাবে পাঠানো হয় সেভাবেই পড়া হয় — bug থাকা সত্ত্বেও দুই পাশের ভুল একে অপরকে বাতিল করে দেয়, তাই কাকতালীয়ভাবে ঠিক port-এ connect হয়। এটা একটা বিপজ্জনক অবস্থা — কোড ভুল, কিন্তু test pass করে।
কেন big-endian device-এর সাথে ভেঙে পড়ে: sockaddr_in.sin_port ফিল্ড সবসময় network byte order (big-endian) আশা করে — এটা POSIX socket API-র চুক্তি। x86-64 client 8080 (0x1F90)-কে তার native little-endian order-এ raw bytes হিসেবে পাঠায়: byte sequence 90 1F। কিন্তু device (এবং socket API স্তর) সেই bytes-কে big-endian হিসেবে পড়ে — 90 1F বাইট-দুটোকে big-endian ধরে সংখ্যায় রূপান্তর করলে পাওয়া যায় 0x901F = decimal 36895, একটা সম্পূর্ণ ভিন্ন port। device যদি নিজে সঠিকভাবে network byte order মেনে চলে (যেটা করাই উচিত), তখনই এই client-এর ভুলটা প্রকাশ পায়।
সমাধান:
uint16_t port = 8080;
struct sockaddr_in addr;
addr.sin_port = htons(port); // host byte order → network byte orderhtons (host to network, short/16-bit) নিশ্চিত করে port number সবসময় big-endian-এ পাঠানো হচ্ছে, client-এর native order যাই হোক না কেন। little-endian মেশিনে এটা প্রকৃত byte-swap করবে; big-endian মেশিনে htons internally no-op (কিছুই বদলাবে না, কারণ native order আর network order ইতিমধ্যে একই)।
সাধারণ নীতি: socket API-র সাথে কাজ করার সময় প্রতিটা multi-byte integer field-এ (port, IPv4 address-এর কিছু অংশ) htons/htonl (পাঠানোর সময়) আর ntohs/ntohl (গ্রহণ করার সময়) নিঃশর্তভাবে ব্যবহার করা উচিত — নিজের মেশিনের byte order কী তা নিয়ে কখনো অনুমান না করে। এই bug-টা বিশেষভাবে বিপজ্জনক কারণ same-endianness টেস্টিং পরিবেশে এটা সম্পূর্ণ অদৃশ্য থাকে।
এরপর কী
এরপর কী
Endianness আমাদের শিখিয়েছে একটা single multi-byte value কীভাবে byte sequence-এ সাজে। কিন্তু বাস্তব প্রোগ্রামে আমরা শুধু একটা raw integer পাঠাই না — আমরা পুরো structure পাঠাই: একটা user record, একটা game object-এর state, একটা API response। সেই record-এ integer আছে, float আছে (IEEE 754 লেসন), string আছে (Unicode/UTF-8 লেসন), আর সবগুলো একসাথে একটা flat byte sequence-এ পরিণত করতে হয় — disk-এ লেখার জন্য বা network-এ পাঠানোর জন্য।
সেটাই এই module-এর শেষ আর সবচেয়ে সংশ্লেষণাত্মক লেসন — serialization ও binary format। সেখানে আমরা দেখব কেন একটা in-memory struct সরাসরি memcpy করে ফাইলে লেখা একটা real, বহুবার-ঘটা security bug-এর জন্ম দেয়, কীভাবে C compiler struct-এ padding বসায় (আর কীভাবে সেই padding কমানো যায়), আর কেন প্রতিটা বড় সিস্টেম (Protocol Buffers, JSON, MessagePack) binary format ডিজাইনের একই মৌলিক প্রশ্নে ফিরে আসে: speed, size, human-readability, আর backward compatibility-র মধ্যে কোন trade-off বেছে নেব?
এই module-এর প্রতিটা আগের লেসনের টুকরো — integer, float, char encoding, আর এখন endianness — সব একসাথে জড়ো হবে এই শেষ লেসনে একটা একক প্রশ্নে: একটা in-memory object-কে flat byte-এ রূপান্তর করে আবার হুবহু ফেরত পাওয়া যায় কীভাবে?
আরও পড়ুন
- On Holy Wars and a Plea for Peace (IEN 137) — Danny Cohen · যে paper থেকে 'endian' শব্দটার জন্ম — Gulliver's Travels-এর রূপক ব্যবহার করে
- RFC 1700 — Assigned Numbers · 'network byte order' মানে big-endian — এই সংজ্ঞা এখানে আনুষ্ঠানিক
- TIFF Revision 6.0 Specification — Adobe · 'II'/'MM' magic byte দিয়ে file-এর নিজের byte order ঘোষণা করার ক্লাসিক উদাহরণ