Bit Pattern কী — অর্থ কোথা থেকে আসে
What Is a Bit Pattern
একটা bit pattern নিজে কিছু বলে না — সংখ্যা, অক্ষর, float না instruction, সবটাই নির্ভর করে কোন convention দিয়ে আপনি সেটা পড়ছেন তার উপর।
আগে এটা বুঝি
Level 0 আমরা শেষ করেছিলাম একটা প্রশ্ন দিয়ে, উত্তর না দিয়ে।
একটা byte — 01000001। এটা কি সংখ্যা ৬৫? অক্ষর A? একটা
float-এর টুকরো? একটা machine instruction?
চলুন সততার সাথে শুরু করি: প্রশ্নটার কোনো একক উত্তর নেই। আর সেটাই এই পুরো module-এর থিসিস।
01000001 কে হেক্সে লিখলে 0x41। এবার চারটা সম্পূর্ণ বৈধ পাঠ
পাশাপাশি রাখি:
| যদি এটাকে পড়া হয়… | তাহলে এটা মানে | কোথায় ঘটে |
|---|---|---|
| একটা unsigned 8-bit integer | 65 | C-তে uint8_t x = 0x41; |
| একটা ASCII character | 'A' | Text file-এ, terminal-এ |
| একটা IEEE-754 float-এর প্রথম byte | 12.0f-এর high byte (41 40 00 00) | Big-endian float-এর memory dump |
| একটা x86 machine opcode | INC ECX (32-bit mode) | Disassembler-এ |
চারটাই একই আটটা bit। কোনোটাই “আসল” পাঠ নয় আর বাকিগুলো “ভুয়া” নয়। যে সিস্টেম এই byte-টা পড়ছে, সেটা আগে থেকেই জানত এটাকে কীভাবে পড়তে হবে — সেই জানাটাই convention। Bit pattern নিজে সেই তথ্য বহন করে না।
এই লেসনটাই সেই দাবিটার ভিত্তি বসাবে — bit আসলে কী, কেন কম্পিউটার binary বাছল, আর কেন “meaning” একটা বাইরে থেকে চাপানো জিনিস। এরপরের প্রতিটা লেসন এই একটা থিসিসের একেকটা বিশেষ ক্ষেত্র — two’s complement, IEEE 754, UTF-8, endianness, সবই “কোন convention” প্রশ্নের একেকটা উত্তর।
মূল ধারণা
একটা bit আসলে কী
সংজ্ঞাটা সহজ শোনায়: bit মানে বাইনারি ডিজিট — 0 অথবা 1।
কিন্তু এটা একটা গাণিতিক abstraction। ভৌতভাবে একটা bit হলো
যেকোনো দ্বি-অবস্থার, distinguishable physical quantity।
কয়েকটা বাস্তব উদাহরণ:
| মাধ্যম | দুইটা অবস্থা | কোথায় ব্যবহৃত |
|---|---|---|
| Voltage level (transistor) | High (~৩.৩V/৫V) / Low (~০V) | CPU, RAM (SRAM cell) |
| Magnetic domain polarity | উত্তরমুখী / দক্ষিণমুখী | Hard disk platter |
| Electric charge (capacitor) | Charged / Discharged | DRAM cell |
| আলোর তীব্রতা | উজ্জ্বল / নিভানো (pit/land) | CD, DVD, optical fiber |
| Relay বা switch | বন্ধ / খোলা | প্রাথমিক যুগের কম্পিউটার (Harvard Mark I) |
| পাঞ্চ কার্ডের ছিদ্র | ছিদ্র আছে / নেই | ১৯৫০-এর দশকের ইনপুট |
লক্ষ্য করুন — “bit” কোনো নির্দিষ্ট প্রযুক্তি নয়। এটা একটা প্যাটার্ন: যেকোনো কিছু যাকে নির্ভরযোগ্যভাবে দুইটা আলাদা অবস্থায় রাখা যায় এবং পরে সেই অবস্থাটা পড়া যায়, সেটাই একটা bit ধারণ করতে পারে।
শব্দটা নিজেই ১৯৪৮ সালের — Claude Shannon-এর A Mathematical Theory of Communication পেপারে, John Tukey-র প্রস্তাবিত সংক্ষেপণ হিসেবে: binary digit। Shannon-এর কাজটাই information theory-র ভিত্তি — “কতটুকু তথ্য” প্রশ্নটাকে প্রথমবার সংখ্যায় মাপা গেল, আর একক হলো bit।
কেন binary — একটা প্রকৌশল সিদ্ধান্ত, প্রাকৃতিক নিয়ম নয়
একটা স্বাভাবিক প্রশ্ন: কম্পিউটার কেন 0-9 (decimal) বা এমনকি
0-2 (ternary) ব্যবহার করে না? প্রতি অঙ্কে বেশি তথ্য থাকত,
কম অঙ্ক লাগত।
উত্তরটা তথ্যতত্ত্ব নয় — নির্ভরযোগ্যতা।
একটা electronic circuit-এ একটা মান পড়তে হলে voltage মাপতে হয়। Voltage-এ সবসময় সামান্য noise থাকে — তাপ, interference, component tolerance। প্রশ্ন হলো: কতটা বিশ্বাসযোগ্যভাবে দুইটা (বা তার বেশি) আলাদা অবস্থা আলাদা করা যায়?
দুইটা অবস্থার জন্য noise margin সবচেয়ে বড় রাখা সহজ — পুরো voltage range-টাকে ঠিক দুই ভাগে ভাগ করে দুই প্রান্তে ঠেলে দিলেই হয় (saturation-এ চালানো transistor — হয় পুরো ON, নয় পুরো OFF)।
২টা state (binary): ৩টা state (ternary):
0V ────┐ 0V ────┐
│ বড় gap │ ছোট gap
│ (noise margin) │
5V ────┘ 2.5V────┤
│ ছোট gap
5V ────┘Ternary-তে একই ৫V range-এ তিনটা level গুঁজলে প্রতিটা gap ছোট হয়ে যায় — noise সহজেই একটা level-কে ভুল করে পাশের level বলে ধরিয়ে দেয়। Binary-তে gap সবচেয়ে বড়, তাই error rate সবচেয়ে কম।
দ্বিতীয় কারণ — digital logic সরাসরি Boolean algebra-র সাথে
মেলে। Level 0-এর Boolean algebra লেসনে আমরা দেখেছি AND, OR,
NOT দিয়ে যেকোনো logical প্রশ্নের উত্তর বানানো যায়। দুইটা অবস্থা
মানে প্রতিটা signal সরাসরি একটা Boolean variable (true/false,
1/0) — আর তার উপর circuit বানানো Boolean algebra-র প্রতিটা
নিয়ম হুবহু প্রযোজ্য। Level 2-তে আমরা দেখব কীভাবে এই একই ধারণা
থেকে transistor থেকে সরাসরি logic gate তৈরি হয়।
থিসিস: Meaning = Interpretation Convention
এতক্ষণে আমরা জানি bit কী আর কেন সংখ্যা দুইটা। এবার মূল প্রশ্নে ফিরি: bit-এর group-কে কীভাবে অর্থ দেওয়া হয়?
উপরের 0x41 উদাহরণটা আবার দেখুন। প্রতিটা কলাম আসলে একটা
convention-এর নাম:
- “Unsigned integer” পড়তে হলে জানতে হবে: bit position গুলো কীভাবে সংখ্যায় যোগ হয় (positional notation — Level 0-এই শেখা)।
- “ASCII” পড়তে হলে জানতে হবে: কোন সংখ্যা কোন অক্ষরের সাথে map করা আছে (একটা lookup table, যেটা ১৯৬৩ সালে standardize হয়েছিল)।
- “Float” পড়তে হলে জানতে হবে: sign, exponent, mantissa কোথায় কতটা bit নিয়ে বসে (IEEE 754 — ১৯৮৫ সালের standard)।
- “Machine instruction” পড়তে হলে জানতে হবে: কোন CPU, কোন mode (16/32/64-bit), কারণ একই byte ভিন্ন architecture-এ ভিন্ন instruction।
একটা convention ছাড়া bit pattern একটা প্রশ্নহীন উত্তর — অর্থহীন। এই বাক্যটা মনে রাখুন, কারণ পুরো module এটার প্রমাণ।
বিটগুলো: 01000001 01000000 00000000 00000000
convention: "৪-byte unsigned int, big-endian"
→ 1,094,795,264
convention: "৪-byte IEEE-754 float, big-endian"
→ 12.0
convention: "ASCII string, ৪ বাইট"
→ "A@\0\0" (দুইটা অ-মুদ্রণযোগ্য byte সহ)
convention: "x86-64 machine code, ৪ instruction byte"
→ REX.B prefix + INC ECX + দুইটা ADD AL,[EAX] (address-এর উপর নির্ভর করে ভিন্ন)কোনো bit নিজে থেকে বলে না “আমি একটা float”। আপনি — অথবা programming language, অথবা file format, অথবা CPU — আগে থেকে স্থির করে রাখে কোন convention প্রযোজ্য, তারপর সেই একই বিটগুলো পড়ে।
এই module-এর roadmap
চৌদ্দটা লেসনে আমরা একে একে এই convention গুলো শিখব — সহজ থেকে জটিলে।
- একটা bitদ্বি-অবস্থার physical quantity (এই লেসন)
- Positional number systembinary/octal/hex — মানুষের পড়ার সুবিধার জন্য (পরের লেসন)
- bit → nibble → byte → wordকতগুলো bit একসাথে "এক জিনিস" (এই module-এর ৩য় লেসন)
- unsigned integerpositional notation সরাসরি প্রয়োগ
- signed integersign-magnitude, one's complement, two's complement
- integer overflowwraparound — mod 2ⁿ arithmetic ফিরে আসে
- fixed-point vs floating-pointtrade-off — range বনাম precision
- IEEE 754sign + exponent + mantissa — প্রতিটা bit-এর কাজ
- float error, NaN, Infকেন 0.1 + 0.2 ≠ 0.3
- ASCII, Unicode, UTF-8সংখ্যা → অক্ষর, বাংলা সহ
- endiannessmulti-byte value কোন দিক থেকে পড়া হবে
- serializationসব মিলিয়ে একটা struct-কে byte stream বানানো
একটা প্যাটার্ন খেয়াল করুন — প্রতিটা layer আগেরটার উপর দাঁড়িয়ে। Positional number system ছাড়া integer বোঝা যায় না। Integer বোঝা ছাড়া IEEE 754-এর exponent bit-গুলো বোঝা যায় না (exponent নিজেই একটা biased unsigned integer)। Endianness বোঝা ছাড়া serialization বোঝা অসম্ভব, কারণ প্রশ্নটাই হলো “কোন byte প্রথমে পাঠাব”। এটাই dependency-aware curriculum-এর মানে — লাফ দেওয়া যায় না।
ভেতরে কী ঘটছে
যেখানে “meaning = convention” থিসিসটা ভয়ংকরভাবে সত্য প্রমাণিত হয়
উপরে যা বলেছি সেটা নিছক দার্শনিক পর্যবেক্ষণ শোনাতে পারে। বাস্তবে এটা নিরাপত্তা, সঠিকতা আর ইতিহাসের অনেক গুরুতর সমস্যার মূল।
১. একই byte যখন “data” আর “instruction” দুটোই
Von Neumann architecture-এ (যা প্রায় প্রতিটা আধুনিক CPU ব্যবহার করে) program আর data একই memory-তে থাকে, একই bit দিয়ে গঠিত। CPU একটা byte-কে instruction হিসেবে পড়বে নাকি data হিসেবে — সেটা নির্ধারণ করে শুধু একটাই জিনিস: Program Counter সেই address-এ পৌঁছেছে কি না।
এখান থেকেই buffer overflow attack-এর জন্ম। যদি একজন attacker এমন data পাঠাতে পারে যা memory-তে এমন জায়গায় বসে যেখানে পরে CPU সেটাকে instruction হিসেবে পড়বে (যেমন stack-এ return address overwrite করে), তাহলে “নিরীহ” data হঠাৎ চালানো code হয়ে যায়। এটাই shellcode injection-এর মূল নীতি — কোনো “hacking magic” নেই, শুধু আমাদের থিসিসের একটা ভয়ংকর প্রয়োগ: bit নিজে জানে না সে data না instruction, শুধু context জানে।
Level 4 (Operating Systems) আর Level 10 (Security)-এ আমরা দেখব কীভাবে NX bit (No-eXecute) আর ASLR এই সমস্যাটার সরাসরি প্রতিকার — memory-র একটা অংশকে “শুধু data, কখনো instruction হিসেবে পড়া যাবে না” বলে চিহ্নিত করা। মজার ব্যাপার হলো Harvard architecture (program আর data memory শারীরিকভাবে আলাদা) এই সমস্যাটা প্রথম থেকেই এড়িয়ে যায় — কিন্তু flexibility-র মূল্যে। বেশিরভাগ আধুনিক CPU একটা hybrid নেয়: von Neumann memory, কিন্তু আলাদা L1 instruction ও data cache।
২. Polyglot file — একই byte, দুইটা সম্পূর্ণ ভিন্ন বৈধ parser
একটা ফাইলের “type” কে ঠিক করে দেয় কোন parser সেটা পড়ছে, ফাইলের ভেতরের বিটগুলো নয়। এই সত্যটার একটা চমৎকার (ও কখনো কখনো বিপজ্জনক) প্রমাণ হলো polyglot file — এমন একটা byte sequence যা একই সাথে দুইটা ভিন্ন ফরম্যাটের জন্য বৈধ।
- ZIP আর JAR ফাইল ফরম্যাট দুটোরই central directory ফাইলের শেষে থাকে (ZIP পার্সার শেষ থেকে খোঁজে), তাই আপনি একটা ফাইলের শুরুতে একটা বৈধ JPEG আর শেষে একটা বৈধ ZIP জুড়ে দিতে পারেন — ছবিটাও ঠিক দেখাবে, আবার unzip-ও চলবে। এই কৌশলটার নাম ছিল GIFAR (GIF+JAR) — ২০০৮ সালে একটা পুরনো ব্রাউজার vulnerability-তে ব্যবহৃত হয়েছিল same-origin policy ফাঁকি দিতে।
- PDF ফরম্যাটও নমনীয় — শুরুর দিকে “আবর্জনা” byte থাকলেও PDF
reader সেটা উপেক্ষা করে ভেতরে
%PDFheader খোঁজে। এই ফাঁক ব্যবহার করে PDF+ZIP, এমনকি PDF+PNG polyglot বানানো সম্ভব।
এই আক্রমণগুলোর মূল কারণ ঠিক আমাদের থিসিস: একটা byte sequence-এর “টাইপ” ফাইলে লেখা নেই। এটা একটা negotiation — কোন সিস্টেম, কী নিয়মে, কোথা থেকে পড়া শুরু করছে তার উপর নির্ভর করে।
৩. Type punning — C যা করতে দেয়, কিন্তু কীভাবে গুরুত্বপূর্ণ
একই memory-র bit-কে দুই ভিন্নভাবে পড়াকে বলে type punning। C-তে এটা করার তিনটা পথ আছে, আর তিনটার নিরাপত্তা সমান নয় — নিচের experiment section-এ আমরা এটা হাতে-কলমে দেখব।
সংক্ষেপে: memcpy আর union দিয়ে reinterpret করা defined
behaviour, কিন্তু একটা float*-কে int*-এ cast করে dereference
করা strict aliasing rule ভাঙে — এটা undefined behaviour,
মানে compiler ধরে নেয় এটা কখনো ঘটবে না, আর সেই অনুমানের উপর
ভিত্তি করে optimization চালায় যা আপনার প্রত্যাশিত ফলাফল ভেঙে
দিতে পারে। “কম্পাইল হয়েছে আর কাজও করেছে” মানে “সঠিক” নয় — এটা
এই module জুড়ে ফিরে আসবে।
৪. Bit নিজেই একটা perfectly reliable ধারণা নয়
আমরা শুরু করেছিলাম bit-কে “দ্বি-অবস্থার physical quantity” বলে। কিন্তু physical মানেই মাঝে মাঝে ভুল হয়।
মহাজাগতিক রশ্মি (cosmic ray) বা তেজস্ক্রিয় ক্ষয় থেকে আসা একটা
কণা যদি সঠিক মুহূর্তে সঠিক জায়গায় আঘাত করে, একটা DRAM
capacitor-এর charge অবস্থা উল্টে যেতে পারে — একটা 0 হয়ে যায়
1, কোনো software bug ছাড়াই। এটাকে বলে bit flip বা soft
error। বড় data center-এ, বিলিয়ন বিলিয়ন bit নিয়ে কাজ করার সময়,
এটা প্রতিদিনের বাস্তবতা — এই কারণেই সার্ভার-গ্রেড RAM-এ ECC
(Error-Correcting Code) থাকে, যা প্রতিটা byte-এর সাথে extra
bit রাখে শুধু ভুল ধরতে আর সংশোধন করতে (Level 7 আর ১০-এ error
correction নিয়ে বিস্তারিত)।
আরও অস্বস্তিকর একটা ঘটনা: Rowhammer (২০১৪ সালে আবিষ্কৃত)। একই memory row-এ বারবার দ্রুত access করলে পাশের row-এর capacitor-এ electrical interference হয়ে bit flip ঘটানো যায় — ইচ্ছাকৃতভাবে, শুধু access pattern দিয়ে, কোনো software bug ছাড়াই। এটা প্রমাণ করে bit-এর “দ্বি-অবস্থা” ধারণাটা একটা engineering guarantee, physical law নয় — আর যেকোনো guarantee-র মতো এটাও নির্দিষ্ট শর্তে ভাঙতে পারে।
সারকথা: এই লেসনের থিসিস শুধু “একই bit ভিন্নভাবে পড়া যায়” তা নয় — এটা আরও গভীর: পুরো representation ব্যবস্থাটাই একগুচ্ছ সম্মত convention আর engineering guarantee-র উপর দাঁড়িয়ে, আর প্রতিটা guarantee-র একটা ভাঙার শর্ত আছে। এই module জুড়ে আমরা বারবার এই প্যাটার্নে ফিরব: convention কী, কেন কাজ করে, আর কোথায় ভাঙে।
উদাহরণ
0x41 — চারটা পাঠ, বিস্তারিত হিসাব সহ
চলুন যাচাই করি উপরের দাবিগুলো নিছক গল্প নয়।
পাঠ ১ — Unsigned 8-bit integer:
Positional notation-এর সরাসরি প্রয়োগ (Level 0)।
পাঠ ২ — ASCII:
ASCII table-এ 65 = 'A'। এই mapping-টা কোনো গাণিতিক সত্য নয় —
এটা ১৯৬৩ সালে American Standards Association-এর একটা কমিটির
সিদ্ধান্ত। 'A' কেন 65 আর 0 নয়? কারণ প্রথম ৩২টা কোড
(0-31) control character-দের জন্য সংরক্ষিত ছিল (যেমন
\n, \t) — সেই ইতিহাস text-encoding লেসনে।
পাঠ ৩ — IEEE-754 float-এর অংশ:
12.0-কে ৩২-bit IEEE 754-এ এনকোড করলে (big-endian byte order):
sign(1) exponent(8) mantissa(23)
0 10000010 10000000000000000000000byte-এ ভাগ করলে: 0100 0001 0100 0000 0000 0000 0000 0000
= 41 40 00 00। প্রথম byte-টাই আমাদের 0x41। এই বিস্তারিত
এনকোডিং কীভাবে কাজ করে তা আমরা IEEE 754 লেসনে সম্পূর্ণ দেখব —
এখন শুধু লক্ষ্য করুন 0x41 এখানে একটা সম্পূর্ণ সংখ্যার
অংশমাত্র, নিজে কিছু না।
পাঠ ৪ — x86 machine instruction:
Intel-এর official opcode table-এ ৩২-bit mode-এ 0x41 মানে
INC ECX — ECX register-এর মান ১ বাড়ানো। কিন্তু x86-64
(“long mode”)-এ Intel এই একই byte range (0x40–0x4F)
পুনর্ব্যবহার করেছে REX prefix হিসেবে — একটা modifier byte
যা পরের instruction-কে ৬৪-bit register access দেয়।
এটাই সবচেয়ে জোরালো প্রমাণ: এমনকি “একই CPU পরিবার”-এর মধ্যেও
mode বদলালে একই byte-এর অর্থ বদলে যায়। 0x41 কোনো চিরস্থায়ী
সত্য বহন করে না — এটা শুধু একটা প্যাটার্ন যা প্রেক্ষাপট অনুযায়ী
ব্যাখ্যা পায়।
নিজে চালিয়ে দেখুন
একই ৩২ bit, চার রকম পাঠ — Python
import struct
# 12.0 -কে little-endian ৪-byte float হিসেবে এনকোড করি
raw = struct.pack('<f', 12.0)
print("raw bytes :", raw.hex(' ')) # 00 00 40 41
# এবার সেই একই bytes-কে ভিন্ন convention দিয়ে পড়ি
as_float = struct.unpack('<f', raw)[0]
as_uint32 = struct.unpack('<I', raw)[0]
as_int32 = struct.unpack('<i', raw)[0]
as_ascii = raw.decode('latin-1') # প্রতিটা byte-কে অক্ষর হিসেবে
print("as float :", as_float) # 12.0
print("as uint32 :", as_uint32) # 1092616192
print("as int32 :", as_int32) # 1092616192 (এখানে sign bit 0)
print("as ascii :", repr(as_ascii)) # '\x00\x00@A' — শেষ byte-টাই 0x41 = 'A'আউটপুট:
raw bytes : 00 00 40 41
as float : 12.0
as uint32 : 1092616192
as int32 : 1092616192
as ascii : '\x00\x00@A'একই চারটা byte — একটা 12.0, একটা 1092616192, একটা
বিচিত্র ৪-অক্ষরের string। struct.pack/unpack-এর format
string ('<f', '<I', '<i') হলো precisely সেই “convention”
যা আমরা এতক্ষণ কথা বলছিলাম — এটা বাদ দিলে raw bytes নিজে কিছুই
বলে না।
লক্ষ্য করুন uint32 আর int32-এর মান একই — কারণ sign bit
(সবচেয়ে গুরুত্বপূর্ণ bit) এখানে 0, তাই দুই পাঠেই মান positive।
পরের module-এর signed integer লেসনে দেখব sign bit 1 হলে এই
দুই পাঠ কতটা আলাদা হয়ে যায়।
একই raw bytes struct module দিয়ে integer বা float হিসেবে unpack করলে সম্পূর্ণ ভিন্ন সংখ্যা বেরোয় — bit pattern-এ কোনো 'built-in type tag' নেই, শুধু আপনি যে format string দেন সেটাই ব্যাখ্যা ঠিক করে।
C-তে type punning — কোনটা নিরাপদ, কোনটা নয়
#include <stdio.h>
#include <string.h>
int main(void) {
float f = 12.0f;
/* পথ ১ — union: defined behaviour (C99-এ স্পষ্ট অনুমোদিত) */
union { float f; uint32_t u; } pun;
pun.f = f;
printf("union : 0x%08x\n", pun.u);
/* পথ ২ — memcpy: সবচেয়ে নিরাপদ, সব compiler-এ সমানভাবে defined */
uint32_t via_memcpy;
memcpy(&via_memcpy, &f, sizeof(f));
printf("memcpy : 0x%08x\n", via_memcpy);
/* পথ ৩ — pointer cast: strict aliasing ভাঙে, undefined behaviour */
uint32_t *bad_ptr = (uint32_t *)&f;
printf("ptr cast : 0x%08x\n", *bad_ptr);
return 0;
}সাধারণ optimization level-এ (-O0, -O1) তিনটাই একই আউটপুট
দেয়:
union : 0x41400000
memcpy : 0x41400000
ptr cast : 0x41400000কিন্তু -O2/-O3-এ compiler ধরে নেয় float* আর uint32_t*
কখনো একই memory-কে alias করবে না (strict aliasing rule),
আর সেই অনুমানের ভিত্তিতে instruction reorder করতে পারে। বড়,
জটিল ফাংশনে এটা ভুল ফলাফল দিতে পারে — যদিও এই ছোট উদাহরণে
compiler-রা সাধারণত এখনো এটা “চালিয়ে দেয়”।
gcc -Wall -Wextra -fstrict-aliasing -O2 -o pun pun.c
gcc -Wall -Wextra -fstrict-aliasing -O2 -S -o - pun.c | grep -A3 "bad_ptr\|ptr cast"-Wstrict-aliasing flag চালু করলে GCC পথ ৩-এর জন্য একটা warning
দেয় — compiler নিজেই স্বীকার করছে এটা ঝুঁকিপূর্ণ।
একই bit reinterpret করার তিনটা পথের মধ্যে দুইটা defined behaviour দেয়, একটা (pointer cast) strict aliasing ভেঙে undefined behaviour ডেকে আনে — যা optimization level বদলালে ভিন্ন ফলাফল দিতে পারে।
নিজে বানান
Bit Pattern Inspector — একটা byte-কে সবরকমভাবে দেখান
- ব্যবহারকারীর কাছ থেকে একটা 8-bit মান নিন (decimal, hex, বা binary যেকোনো ফরম্যাটে)
- সেটাকে binary, hex, আর unsigned decimal — তিনভাবে print করুন
- সেটা printable ASCII হলে সংশ্লিষ্ট character দেখান, নয়তো "non-printable" লিখুন
- ৪টা এমন byte জোড়া করে সেটাকে একটা IEEE-754 float হিসেবেও দেখান
import struct
def inspect_byte(b: int) -> None:
assert 0 <= b <= 255, "একটা byte 0-255 এর মধ্যে হতে হবে"
print(f"binary : {b:08b}")
print(f"hex : 0x{b:02x}")
print(f"unsigned : {b}")
ch = chr(b)
if ch.isprintable() and ch != ' ':
print(f"ascii : {ch!r}")
else:
print("ascii : (non-printable)")
def inspect_as_float(bs: bytes, endian: str = '<') -> None:
assert len(bs) == 4, "float দেখাতে ঠিক ৪টা byte লাগবে"
(val,) = struct.unpack(f'{endian}f', bs)
(as_int,) = struct.unpack(f'{endian}I', bs)
print(f"bytes : {bs.hex(' ')}")
print(f"as float : {val}")
print(f"as uint32: {as_int}")
if __name__ == "__main__":
print("── একটা byte ──")
inspect_byte(0x41)
print("\n── চারটা byte, float হিসেবে ──")
inspect_as_float(bytes([0x00, 0x00, 0x40, 0x41]))নিজে বাড়ান:
- Command line argument নিন (
sys.argv), হার্ডকোড না করে - Signed interpretation যোগ করুন (
b - 256ifb >= 128) — পরের module-এর two’s complement লেসনের আগাম স্বাদ - Big-endian আর little-endian দুই ধরনের float পাঠ পাশাপাশি দেখান — পার্থক্যটা লক্ষ্য করুন
- একটা পুরো text file পড়ে প্রতিটা byte-এর জন্য এই তিন পাঠ একটা টেবিলে দেখান
বাস্তব সিস্টেমে
যেখানে “bit pattern-এর অর্থ আসলে কনভেনশন” প্রতিদিন দেখা যায়
-
File magic numbers। প্রতিটা ফাইল ফরম্যাটের প্রথম কয়েক byte একটা স্বাক্ষর — PNG
89 50 4E 47, ELF (Linux executable)7F 45 4C 46, ZIP50 4B 03 04, PDF25 50 44 46(%PDF)।fileকমান্ড extension নয়, এই byte দেখেই ফরম্যাট চেনে — extension মিথ্যা বলতে পারে, magic byte কম। -
Excel-এ CSV vs formula injection। একটা সংখ্যা যেমন
=1+1টাইপ করলে Excel সেটাকে formula হিসেবে interpret করে, plain text হিসেবে নয় — কারণ প্রথম character=convention অনুযায়ী “formula শুরু” বোঝায়। এই একই convention-এর অপব্যবহার করে CSV injection attack হয় — একটা spreadsheet export-এ malicious formula ঢুকিয়ে দেওয়া। -
Network protocol confusion। DNS আর অন্য কিছু UDP-based protocol একই port ব্যবহার করলে, বা একটা proxy TLS ClientHello-কে ভুল protocol বলে ভুল বুঝলে (protocol confusion attack), byte একই থাকে কিন্তু দুই প্রান্ত ভিন্ন convention ধরে নেয় — ফলাফল security bypass।
-
Endianness bug — Mars Climate Orbiter-এর কাজিন। যদিও সেই বিখ্যাত ব্যর্থতা ছিল unit confusion (metric বনাম imperial), একই শ্রেণির bug — “দুই সিস্টেম একই সংখ্যাকে ভিন্নভাবে পড়ছে” — network protocol-এ endianness ভুল হলে ঘটে। পরের module-এর একটা লেসন পুরোপুরি এই সমস্যার জন্য।
-
Instruction set retrofitting। আমরা দেখেছি
0x40–0x4Fx86-এ ৩২-বিট mode-এ ছিলINC/DEC, ৬৪-বিট mode-এ হয়ে গেছে REX prefix। ISA ডিজাইনাররা প্রতিবার নতুন feature যোগ করার সময় এই প্রশ্নের মুখোমুখি হন: পুরনো byte pattern পুনর্ব্যবহার করব, নাকি নতুন encoding space লাগবে? Level 3-এ ISA design-এ এই trade-off বিস্তারিত। -
JSON vs Protocol Buffers। JSON-এ ডেটা টেক্সট আকারে, নিজেই স্ব-বর্ণনামূলক (
{"age": 25})। Protocol Buffers-এ একই ডেটা raw bytes, আর একটা আলাদা.protoschema ফাইল ছাড়া সেই bytes সম্পূর্ণ অর্থহীন — এক্সট্রিম উদাহরণ যেখানে convention (schema) আর data সম্পূর্ণ আলাদা করা হয়েছে, compactness-এর বিনিময়ে। -
Rowhammer ও ECC memory। যেমন hood section-এ দেখেছি, DRAM-এ physical bit flip real। Google, Amazon-এর মতো cloud provider-রা সার্ভার RAM-এ বাধ্যতামূলক ECC ব্যবহার করে ঠিক এই কারণে — একটা bit-এর “guaranteed দুই অবস্থা” ধারণাটা free নয়, এর জন্য extra engineering লাগে।
যে ভুলগুলো সবাই করে
“কম্পিউটার 'ভেতরে ভেতরে বাইনারিতে চিন্তা করে', তাই বাইনারি একরকম বিশেষ বা মৌলিক।”
Binary “special” নয় — এটা একটা engineering choice, উপরে যেমন দেখলাম, reliability margin-এর কারণে জেতা একটা সিদ্ধান্ত।
গাণিতিকভাবে যেকোনো base সমানভাবে বৈধ। Ternary computer সত্যিই বানানো হয়েছিল (Setun) এবং কাজ করত। Decimal computer-ও হয়েছিল (IBM 650, 1950-এর দশক) — প্রতিটা digit BCD-তে এনকোড করে।
“কম্পিউটার বাইনারিতে চিন্তা করে” বলাটা প্রায় “গাড়ি পেট্রলে চিন্তা করে” বলার মতো — এটা জ্বালানি, চিন্তার মাধ্যম নয়। Binary হলো সবচেয়ে নির্ভরযোগ্য আর সস্তা encoding, যার উপর বাকি সবকিছু (integer, float, text) built হয়েছে — কিন্তু সেই “বাকি সবকিছু” নিজেরাই বিমূর্ত ধারণা, যেগুলোর binary encoding একটা choice, একমাত্র সম্ভাবনা নয়।
“একটা byte দেখেই বলে দেওয়া যায় এটা কী — কারণ 65 তো 65-ই, তাই না?”
এটাই এই লেসনের কেন্দ্রীয় ভুল ধারণা।
65 শুধুমাত্র তখনই “৬৫” যখন আপনি ইতিমধ্যে ধরে নিয়েছেন এটা একটা
unsigned integer, base-10-এ প্রকাশ করা হচ্ছে। ঐ একই bit pattern
'A', 12.0-এর অংশ, অথবা INC ECX-ও হতে পারে — উপরের উদাহরণেই
প্রমাণিত।
একটা debugger-এ raw memory দেখলে (যেমন xxd, gdb-র
x/4xb), আপনি শুধু hex bytes দেখেন — কোনো “টাইপ লেবেল” নেই।
টাইপ জানতে হলে source code, symbol table, বা আপনার নিজের
জ্ঞান লাগে যে ঐ address-এ কী রাখা আছে বলে আপনি আশা করেন।
এই ভুল ধারণাটাই debugging-কে কঠিন করে — একটা crash dump দেখে “এই সংখ্যাটা কী” বোঝা মানে আসলে প্রশ্ন করা “এই address-এ কোন convention প্রযোজ্য ছিল বলে আমার code ধরে নিয়েছিল”।
“একটা bit-এর দুইটা অবস্থা perfectly reliable — একটা bit কখনো 'ভুল' হয় না।”
Hood section-এ দেখেছি এটা সত্য নয়। DRAM-এর bit cosmic ray-তে flip হতে পারে, আর Rowhammer-এর মতো আক্রমণ ইচ্ছাকৃতভাবে সেটা ঘটাতে পারে।
“দুই-অবস্থার নির্ভরযোগ্যতা” একটা engineering guarantee, কোনো গাণিতিক সত্য নয়। এই guarantee বজায় রাখতে বাস্তব প্রকৌশল লাগে — ECC memory, error-detecting checksum, সঠিক voltage margin ডিজাইন। যখনই কেউ বলে “bit কখনো ভুল হয় না”, সেটা আসলে বলছে “এই নির্দিষ্ট সিস্টেমে আমরা যথেষ্ট প্রকৌশল বিনিয়োগ করেছি যাতে ভুলের সম্ভাবনা ব্যবহারিকভাবে উপেক্ষা করা যায়” — যা সবসময় সত্য নয়, বিশেষত সস্তা consumer hardware বা adversarial পরিবেশে (Rowhammer একটা attack, দুর্ঘটনা নয়)।
বুঝেছেন কি না দেখুন
1কেউ বলল, “একটা bit pattern দেখেই বলা যায় এটা signed নাকি unsigned
সংখ্যা, কারণ negative সংখ্যায় প্রথম bit সবসময় 1 থাকে।” এই
যুক্তিতে কোথায় ভুল?
যুক্তি
1 থাকে।” এই
যুক্তিতে কোথায় ভুল?যুক্তিটা উল্টো দিকে ভুল। “প্রথম bit 1 মানে negative”
— এটা সত্যিই two’s complement convention-এর একটা নিয়ম। কিন্তু
এখান থেকে “bit pattern নিজে বলে দেয় এটা signed” — এই সিদ্ধান্তে
যাওয়া ভুল।
কারণ: MSB (most significant bit) 1 থাকা একটা bit pattern
সমান বৈধভাবে একটা বড় unsigned সংখ্যাও হতে পারে।
1000 0001
signed (two's complement) পাঠ: -127
unsigned পাঠ: 129দুইটাই “সঠিক” — নির্ভর করে আপনি কোন convention প্রযোজ্য বলে
আগে থেকে স্থির করেছেন তার উপর। C-তে এটাই ঘটে যখন int8_t
আর uint8_t-এর মধ্যে cast করা হয়: bit একই থাকে, শুধু compiler
আপনাকে বলা convention অনুযায়ী পড়ে।
সঠিক বাক্য হওয়া উচিত: “যদি আমরা ধরে নিই এই bit pattern
two’s complement-এ encoded, তাহলে MSB 1 মানে এটা negative।”
Convention আগে আসে, উপসংহার পরে — কখনো উল্টো নয়। এটাই এই
লেসনের কেন্দ্রীয় থিসিসের সবচেয়ে সাধারণ ভুল প্রয়োগ, আর পরের
module-এর signed integer লেসনে আমরা এটা আরও গভীরে দেখব।
2নিচের ৪-byte hex sequence-টা দেখুন: 48 65 6c 6c। এটা ASCII
হিসেবে পড়ুন, তারপর little-endian unsigned 32-bit integer হিসেবে
পড়ুন (দুইটাই হিসাব দেখান)।
প্রয়োগ
48 65 6c 6c। এটা ASCII
হিসেবে পড়ুন, তারপর little-endian unsigned 32-bit integer হিসেবে
পড়ুন (দুইটাই হিসাব দেখান)।ASCII পাঠ:
| byte | hex | decimal | ASCII |
|---|---|---|---|
| ১ম | 48 | 72 | H |
| ২য় | 65 | 101 | e |
| ৩য় | 6c | 108 | l |
| ৪র্থ | 6c | 108 | l |
→ "Hell" (সম্পূর্ণ "Hello"-র প্রথম চার অক্ষর — এটা ইচ্ছাকৃতভাবে
বেছে নেওয়া উদাহরণ)।
Little-endian uint32 পাঠ: little-endian মানে সবচেয়ে কম গুরুত্বপূর্ণ byte সবার আগে (memory-তে সবচেয়ে ছোট address-এ) থাকে। তাই byte order উল্টে সংখ্যা বানাতে হয়:
হিসাব করলে:
দুইটা সম্পূর্ণ ভিন্ন ফলাফল, একই চারটা byte থেকে — একবার একটা পঠনযোগ্য শব্দ, একবার একটা প্রায়-এলোমেলো দেখতে বড় সংখ্যা। এই প্রশ্নটাই দেখায় কেন endianness (multi-byte সংখ্যা কোন দিক থেকে পড়া হবে) নিজেই একটা আলাদা convention, যেটার জন্য এই module-এর আলাদা একটা লেসন লাগবে।
3Buffer overflow attack-কে প্রায়ই বলা হয় “hacker কোড ইনজেক্ট করে
দিল”। এই লেসনের ভাষায়, ঠিক কী “ইনজেক্ট” হয়, আর কেন এটা সম্ভব
হয়?
যুক্তি
কিছুই “নতুনভাবে” ইনজেক্ট হয় না প্রচলিত অর্থে — যা ঘটে তা হলো attacker এমন byte পাঠায় যেগুলো data হিসেবে গৃহীত হয়, কিন্তু পরে CPU সেই একই address-এ পৌঁছে সেগুলোকে instruction হিসেবে পড়ে ফেলে।
ধাপে ধাপে:
- একটা program
buf-এ user input নেয়, কিন্তু bound check করে না। Attackerbuf-এর আকারের চেয়ে বড় input পাঠায়। - Overflow হওয়া data stack-এর পরের অংশ পর্যন্ত ছড়িয়ে পড়ে — বিশেষত return address (ফাংশন শেষে CPU কোথায় ফিরবে সেই ঠিকানা, নিজেও একটা bit pattern) overwrite করে দেয়।
- Function শেষ হলে CPU সেই (এখন attacker-এর দেওয়া) return address-এ jump করে — যা আসলে attacker-এর পাঠানো data-রই একটা অংশ (shellcode) নির্দেশ করে।
- CPU সেই ঠিকানায় পৌঁছে সেই bytes-কে instruction হিসেবে fetch করে চালানো শুরু করে।
কোনো নতুন data তৈরি হয়নি। যা ঘটেছে তা এই লেসনের থিসিসের সরাসরি প্রয়োগ: একই bytes ধাপ ১-এ “শুধু ডেটা” হিসেবে গৃহীত হয়েছিল, ধাপ ৪-এ “instruction” হিসেবে fetch হয়েছে — শুধু CPU-র Program Counter সেখানে পৌঁছেছে বলে। Convention (এখানে: “PC যেখানে পৌঁছায় সেটাই instruction”) পুরো পার্থক্যটা তৈরি করেছে, bit pattern নিজে না।
এই কারণেই আধুনিক প্রতিরক্ষা (NX bit, stack canary, ASLR) সবই এই থিসিসকেই লক্ষ্য করে কাজ করে — কোনো না কোনোভাবে “data” আর “instruction” অঞ্চলের মধ্যে convention-টা জোরদার করে, যাতে data হিসেবে চিহ্নিত memory কখনো fetch/execute করা না যায়। Level 10-এ এটা বিস্তারিত।
4আপনি একটা নতুন ফাইল ফরম্যাট ডিজাইন করছেন যেটাতে সংখ্যা, টেক্সট,
আর ছবি — তিন ধরনের ডেটা একসাথে থাকবে। শুধু raw bytes থাকলে একটা
reader কীভাবে বুঝবে কোনটা কী? কমপক্ষে দুইটা ভিন্ন পদ্ধতি প্রস্তাব
করুন, আর তাদের trade-off আলোচনা করুন।
ডিজাইন
এই প্রশ্নটা সরাসরি জিজ্ঞেস করছে: bit নিজে convention বহন করে না, তাহলে সেটা আলাদাভাবে কীভাবে যোগ করবেন? বাস্তব ফাইল ফরম্যাট দুইটা প্রধান কৌশল ব্যবহার করে।
পদ্ধতি ১ — Type tag + length prefix (self-describing)।
প্রতিটা অংশের আগে একটা ছোট header রাখুন যা বলে দেয় ধরন আর দৈর্ঘ্য:
[TYPE:1 byte][LENGTH:4 byte][DATA:LENGTH byte] [TYPE][LENGTH][DATA] ...TYPE = 0x01 মানে integer, 0x02 মানে text, 0x03 মানে
image — একটা আলাদা lookup table দিয়ে সংজ্ঞায়িত।
- সুবিধা: reader schema আগে থেকে না জেনেও ফাইলের গঠন বুঝতে পারে (self-describing) — নতুন type যোগ করা সহজ, backward compatible রাখা যায়।
- অসুবিধা: প্রতিটা অংশে overhead (৫ byte বা তার বেশি)। ছোট ছোট অনেক value থাকলে এই overhead উল্লেখযোগ্য হয়ে ওঠে। PNG, ZIP, RIFF (WAV/AVI) — এই কৌশলই ব্যবহার করে (chunk-ভিত্তিক ফরম্যাট)।
পদ্ধতি ২ — Fixed schema (external convention)।
ফাইলে কোনো type tag থাকবে না। বদলে, একটা আলাদা spec ডকুমেন্ট (বা কোড-এ hardcode করা struct definition) বলে দেয় ঠিক কোন byte range-এ কী থাকবে:
struct MyFile {
uint32_t magic; // byte 0-3
uint32_t num_records; // byte 4-7
float records[]; // byte 8 থেকে, প্রতিটা ৪ byte
};- সুবিধা: কোনো per-field overhead নেই — সবচেয়ে compact। Parsing দ্রুত (শুধু struct-এর উপর cast/memcpy)।
- অসুবিধা: reader-কে schema আগে থেকে জানতে হবে — schema বদলালে পুরনো reader নতুন ফাইল পড়তে পারবে না (backward compatibility ভেঙে যায়) যদি না version number আগে থেকে রাখা হয়। Protocol Buffers, বেশিরভাগ raw binary struct dump এই কৌশল ব্যবহার করে।
বাস্তব সিদ্ধান্তের নীতি: যদি ফরম্যাটটা সময়ের সাথে বিবর্তিত হবে, বহু independent reader/writer থাকবে (যেমন একটা public file format), তাহলে পদ্ধতি ১ (self-describing) নিরাপদ। যদি performance critical আর producer/consumer একই codebase-এর অংশ (যেমন একটা game engine-এর internal save format), পদ্ধতি ২ যথেষ্ট, দ্রুততর।
তৃতীয় বাস্তব বিকল্প (hybrid): একটা ছোট fixed header (magic number + version), তারপর version-নির্ভর schema। এটাই সবচেয়ে বেশি ব্যবহৃত বাস্তব প্যাটার্ন — PNG, ELF, class file — সবাই প্রথমে magic number রাখে যাতে reader অন্তত “এটা কোন ফরম্যাট” নিশ্চিত হতে পারে, তারপর নির্দিষ্ট parsing logic-এ যায়।
এরপর কী
এরপর কী
এই লেসনে আমরা প্রতিষ্ঠা করেছি: bit একটা physical, দ্বি-অবস্থার quantity; কম্পিউটার সেটা বাছে reliability-র কারণে, তত্ত্বীয় optimality-র জন্য নয়; আর সবচেয়ে গুরুত্বপূর্ণ — একটা bit pattern-এর অর্থ সম্পূর্ণভাবে নির্ভর করে কোন convention প্রয়োগ হচ্ছে তার উপর।
কিন্তু একটা প্রশ্ন এখনো বাকি — এমনকি সবচেয়ে সহজ convention-টাও
(একটা bit pattern-কে একটা সংখ্যা হিসেবে পড়া) ব্যাখ্যা করা হয়নি।
আমরা 0100 0001 = 65 লিখেছি কিন্তু সেই হিসাবটা কীভাবে কাজ করে
তা ধরে নিয়েছি — Level 0-এর number-systems লেসনের গাণিতিক ভিত্তির
উপর ভরসা করে।
পরের লেসনে আমরা সেই গাণিতিক ভিত্তিটাকে প্রকৌশলের ভাষায় নিয়ে
আসব: কেন hex specifically bit pattern পড়ার জন্য মানুষের সবচেয়ে
সুবিধাজনক shorthand (ঠিক ৪ bit = ১ হেক্স digit, কোনো অপচয় নেই),
কেন octal একসময় গুরুত্বপূর্ণ ছিল কিন্তু এখন প্রায় বিলুপ্ত (Unix
file permission ছাড়া), আর কীভাবে বাস্তব টুল (xxd, hexdump,
compiler literal parser) এই conversion গুলো হাতে-কলমে করে। এটাই
হবে আমাদের প্রথম সত্যিকারের hexdump experiment — একটা আসল
ফাইলের raw bytes চোখে দেখা।
আরও পড়ুন
- A Mathematical Theory of Communication — Claude E. Shannon (1948) · "bit" শব্দটার প্রথম প্রকাশিত সংজ্ঞা — John Tukey-র প্রস্তাবিত সংক্ষেপণ
- Computer Organization and Design, Chapter 1 — David Patterson, John Hennessy · কেন hardware সবসময় binary বাছে — reliability বনাম information density
- Setun: A Balanced Ternary Computer — Nikolay Brusentsov (MSU, 1958) · একমাত্র বাণিজ্যিকভাবে উৎপাদিত ternary computer — binary কেন জিতল বোঝার সবচেয়ে ভালো contrast