Foundationপ্রথম নীতি থেকে
LEVEL 1লেসন ১৩/১৪মাঝারি৫০ মিনিট

Endianness — বাইট অর্ডারের যুদ্ধ

Endianness

একটা multi-byte value byte-addressable memory-তে কোন ক্রমে বসবে সেটা প্রকৃতির নিয়ম না, একটা convention — big-endian না little-endian — আর সেই কনভেনশন না জানলে network packet আর file format ভুল পড়া হয়।

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

  • Big-endian ও little-endian-এর সংজ্ঞা এবং একটা multi-byte value memory address-এ কীভাবে সাজে তা ব্যাখ্যা করতে পারবেন
  • একটা hex value হাতে-কলমে দুই রকম byte order-এই লিখতে পারবেন এবং address-এর সাথে byte মেলাতে পারবেন
  • নিজের মেশিনের native byte order প্রোগ্রাম চালিয়ে যাচাই করতে পারবেন
  • কেন network protocol বিগ-এন্ডিয়ান ব্যবহার করে এবং htonl/ntohl আসলে কী করে তা বলতে পারবেন
  • ফাইল ফরম্যাটে endianness magic byte দিয়ে কীভাবে declare করা হয় তা চিনতে পারবেন
  • endian-swap function নিজে লিখে একটা value round-trip করাতে পারবেন

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

আগে এটা বুঝি

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 — একই সংখ্যা, দুই ভিন্ন byte layout। প্রতিটা বাক্সের নিচে তার memory address।

লক্ষ্য করুন — দুই ক্ষেত্রেই মান একই (0x12345678)। শুধু address 1000-এ কোন byte বসবে, সেটাই আলাদা। যদি আপনি শুধু address 1000-এর byte-টা পড়েন, big-endian-এ পাবেন 0x12, little-endian-এ পাবেন 0x78 — সম্পূর্ণ ভিন্ন সংখ্যা মনে হবে, যদি আপনি convention না জানেন।

নামের ইতিহাস

কে কোনটা ব্যবহার করে

ArchitectureByte orderনোট
x86 / x86-64Little-endianIntel 8086 (1978) থেকেই — প্রায় পুরো consumer computing এখানে দাঁড়িয়ে
ARMBi-endian (default little)Hardware দুটোই সমর্থন করে; বাস্তবে প্রায় সব ARM Linux/Android/iOS little-endian মোডে চলে
RISC-Vসাধারণত little-endianSpec অনুযায়ী little-endian-কে base হিসেবে ধরা হয়
প্রাচীন PowerPC / SPARC / Motorola 68kBig-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 20002007-এ রাখতে হবে:

AddressBig-endianLittle-endian
20000x010x08
20010x020x07
20020x030x06
20030x040x05
20040x050x04
20050x060x03
20060x070x02
20070x080x01

প্যাটার্নটা 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-এর মতোই ট্রিট করে:

AddressBig-endianLittle-endian
30000xFF0x9C
30010xFF0xFF
30020xFF0xFF
30030x9C0xFF

গুরুত্বপূর্ণ বিচ্ছেদ: 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-endianExplicit big-endian
C (POSIX)\<endian.h>-এর __BYTE_ORDER__htole32() / le32toh()htobe32() / htonl()
C++20std::endian::nativestd::byteswap + conditionএকই
Pythonsys.byteorderstruct.pack('\<I', x)struct.pack('>I', x)
Javaনেই (সবসময় big-endian ডিফল্ট)ByteBuffer.order(LITTLE_ENDIAN)ডিফল্ট আচরণ
Rustcfg!(target_endian)x.to_le_bytes()x.to_be_bytes()
Gobinary.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-এর বিবেচনার উপর ছেড়ে না দিয়ে।

একটা 'ধ্রুবক int' আসলে যা যা conceal করে
  1. uint32_t x = 8080উৎস কোডে একটা মান — এখানে endianness অদৃশ্য
  2. CPU registerসংখ্যা হিসেবে ধরা থাকে, কোনো byte-order নেই — register-এর ভেতরে "order" অর্থহীন
  3. store to memoryএখানেই ISA-নির্ধারিত byte order প্রয়োগ হয় — x86 = little-endian
  4. raw bytes at address78 1F 00 00 — মেশিনের native order
  5. socket send()raw byte হুবহু কপি — কোনো conversion হয় না, প্রোগ্রামারের দায়িত্ব
  6. network wireযা পাঠানো হয়েছে তাই যায় — সংজ্ঞা অনুযায়ী কোনো "correct" order নেই এখানে
  7. receiver memoryযদি receiver-এর ধারণা ভিন্ন হয়, একই byte ভিন্ন সংখ্যা হয়ে যায়

উদাহরণ

একটা সম্পূর্ণ ওয়াকথ্রু

চলুন 0xDEADBEEF সংখ্যাটা নিয়ে দুই ক্রমেই memory address 0x20000x2003-এ বসাই।

AddressBig-endian byteLittle-endian byte
0x20000xDE0xEF
0x20010xAD0xBE
0x20020xBE0xAD
0x20030xEF0xDE

সহজ 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.bin

Output:

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.bin
00000000: 1234 5678                               .4Vx

এই দুইটা ফাইলের byte সম্পূর্ণ ভিন্ন, অথচ দুটোই “একই সংখ্যা” represent করে — এটাই এই পুরো লেসনের দাবিটার সবচেয়ে প্রত্যক্ষ প্রমাণ। Windows-এ xxd না থাকলে certutil -f /hex demo.bin বা PowerShell-এর Format-Hex demo.bin একই কাজ করে।

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

EXPERIMENT

আপনার মেশিনের native byte order বের করুন — Python

Python 3 (যেকোনো OS)· ৫ মিনিট

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 পড়ে অনুমান করতে হয় না।

EXPERIMENT

Classic union trick — C-তে byte order নিজে দেখা

Linux / macOS / Windows (কোনো C compiler)· ১০ মিনিট

এটা দশকের পুরনো, বহুল ব্যবহৃত 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_check

x86-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 এখানে নেই।

EXPERIMENT

htonl আসলে কী প্রমাণ করে — একটা real loopback socket দিয়ে দেখুন

Python 3, Linux/macOS/Windows· ১৫ মিনিট

আমাদের ল্যাপটপ/সার্ভার প্রায় সবই 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 সম্পূর্ণ ভিন্ন সংখ্যা হয়ে যায়। এটা কোনো তাত্ত্বিক দাবি না, নিজের মেশিনে সরাসরি পরিমাপযোগ্য।

নিজে বানান

BUILD IT

Endian-swap ফাংশন লিখুন এবং round-trip verify করুন

C অথবা Python · ●●○○○
  1. একটা swap16() ফাংশন লিখুন যা একটা 16-bit value-র দুই byte অদলবদল করে
  2. একটা swap32() ফাংশন লিখুন যা 32-bit value-র চারটা byte সম্পূর্ণ উল্টে দেয়
  3. যাচাই করুন swap32(swap32(x)) == x সবসময় সত্য (round-trip property)
  4. নিজের swap32() -এর output স্ট্যান্ডার্ড library (Python struct বা C htonl) -এর সাথে মিলিয়ে দেখুন
  5. একটা 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) = 0xcdab

Python-এ (আর 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 মান মিলেছে ✓

নিজে বাড়ান:

  1. swap64() লিখুন uint64_t/Python int-এর জন্য, একই shift-mask প্যাটার্ন বাড়িয়ে
  2. একটা generic swap_bytes(data: bytes) -> bytes লিখুন যা যেকোনো length-এর byte string উল্টে দেয় (কোনো fixed size ধরে না নিয়ে) — এটা array[::-1] স্লাইসিং দিয়ে এক লাইনেও হয়, কিন্তু আগে shift-mask পদ্ধতিতে বোঝাটাই আসল লক্ষ্য
  3. একটা IEEE 754 float নিয়ে দেখুন byte-swap করলে কী হয় — struct.pack('\<f', 3.14) বনাম struct.pack('>f', 3.14)-এর byte তুলনা করুন; float-এর bit pattern-ও ঠিক integer-এর মতোই byte-order-নির্ভর, কারণ শেষমেশ এটাও শুধু ৩২/৬৪টা bit-এর একটা sequence
  4. প্রমাণ করুন swap16(swap16(x)) != x কখনো হয় না, formally: প্রতিটা byte ঠিক দুইবার সরানো হয়, তাই এটা তার নিজের inverse (একটা involution)
  5. (চ্যালেঞ্জ — পরের লেসনের প্রস্তুতি) একটা ছোট 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 যা ইন্টারনেটে ভ্রমণ করে তার সার্বজনীন আইন নয়।

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

1

0xCAFEBABE সংখ্যাটা memory address 0x30000x3003-এ big-endian আর little-endian দুই ক্রমেই লিখুন। কোন byte কোথায় বসবে তা স্পষ্টভাবে দেখান।

যুক্তি

সংখ্যাটার চারটা byte (সবচেয়ে গুরুত্বপূর্ণ থেকে সবচেয়ে কম): CA, FE, BA, BE

Big-endian — MSB প্রথমে, তাই সংখ্যা যেভাবে লেখা হয়েছে ঠিক সেই ক্রমেই address বাড়ে:

AddressByte
0x30000xCA
0x30010xFE
0x30020xBA
0x30030xBE

Little-endian — LSB প্রথমে, তাই ক্রম উল্টে যায়:

AddressByte
0x30000xBE
0x30010xBA
0x30020xFE
0x30030xCA

পরীক্ষা করার সহজ উপায়: 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]);
প্রয়োগ

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 = decimal 255
  • p[1] = MSB = 0x00 = decimal 0

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-নির্ভর।

3

Network 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;   // বাগ কোথায়?
প্রয়োগ

বাগটা: 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 order

htons (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 ঘোষণা করার ক্লাসিক উদাহরণ