Foundationপ্রথম নীতি থেকে
LEVEL 1মাঝারি~৬ ঘণ্টাPythonCযেকোনো

Bit Inspector CLI

Bit Inspector CLI

যেকোনো মান (integer বা float, decimal/hex/binary ইনপুট) নিয়ে তার raw bytes, পূর্ণ binary, two's complement আর IEEE 754 breakdown — সব একসাথে দেখানো একটা CLI টুল। একই bit pattern, ভিন্ন interpretation — এই মডিউলের মূল প্রশ্নটাই হাতে-কলমে দেখা।

মাইলস্টোন

আগে যা পড়া দরকার

কেন এই প্রজেক্ট

Signed Integers আর IEEE 754 লেসনে আমরা শিখেছি bit pattern কীভাবে সংখ্যায় রূপ নেয় — কিন্তু সেটা কাগজে-কলমে। এই প্রজেক্টে সেই একই নিয়মগুলো একটা প্রোগ্রামে বসাতে হবে, আর তখনই আসল প্রশ্নগুলো সামনে আসে: memory-তে byte-গুলো কোন ক্রমে থাকে (endianness)? overflow হলে হার্ডওয়্যার আসলে কী করে? একটা 32-bit pattern-কে int হিসেবে পড়লে এক জিনিস, float হিসেবে পড়লে সম্পূর্ণ অন্য জিনিস কেন বোঝায়?

এই টুলটা আসলে এই মডিউলের driving question-এরই উত্তর: “একটা bit pattern দেখে আমি কীভাবে জানব সেটা কী অর্থ বহন করছে?” — উত্তর হলো, জানা যায় না, শুধু ধরে নেওয়া যায়। এই প্রজেক্ট সেই ধরে-নেওয়াটাকে explicit করে দেখাবে।

লক্ষ্য

এই রকম কিছু চালাতে পারা:

$ python bitinspect.py 65

input       : '65'  →  parsed as integer 65
bit width   : 32
binary      : 00000000 00000000 00000000 01000001
raw bytes (little-endian, native=little): 41 00 00 00
unsigned    : 65
signed (2's complement): 65

--- একই 32-bit pattern-কে float32 হিসেবে reinterpret করলে ---
sign        : 0  (ধনাত্মক)
exponent    : 00000000  (raw=0, biased-127=-127)
mantissa    : 00000000000000001000001  (raw=65)
stored value: 9.108245e-44   (একটা subnormal — প্রায় শূন্য, কিন্তু আক্ষরিক অর্থে শূন্য নয়)
$ python bitinspect.py 300 --bits 8

input       : '300'  →  parsed as integer 300
bit width   : 8
binary      : 00101100
raw bytes (little-endian): 2c
unsigned    : 44
signed (2's complement): 44
⚠ overflow  : 300 8-bit-এ ধরে না, wrap হয়ে 44 হয়ে গেছে (হুবহু হার্ডওয়্যার wraparound)
$ python bitinspect.py 3.14

input       : '3.14'  →  parsed as float 3.14
bit width   : 32 (single-precision)
binary      : 01000000 01001000 11110101 11000011
raw bytes (little-endian): c3 f5 48 40
sign        : 0  (ধনাত্মক)
exponent    : 10000000  (raw=128, biased-127=1)
mantissa    : 10010001111010111000011  (raw=4781507)
stored value      : 3.140000104904175
intended (double) : 3.14
precision error    : ≈1.049e-07   (single precision-এ মাত্র ~৭টা দশমিক অঙ্ক নির্ভরযোগ্য)

ধাপে ধাপে

১. ইনপুট পার্স করা

ব্যবহারকারী decimal, hex, binary, বা float — যেকোনো ফরম্যাটে ইনপুট দিতে পারবে।

def parse_input(raw: str):
    """ইনপুট স্ট্রিং থেকে (value, is_float) বের করা।"""
    s = raw.strip()
    lower = s.lower()
    if lower.startswith('0x') or lower.startswith('-0x'):
        return int(s, 16), False
    if lower.startswith('0b') or lower.startswith('-0b'):
        return int(s, 2), False
    if '.' in s or 'e' in lower or lower in ('inf', '-inf', 'nan'):
        return float(s), True
    return int(s), False

লক্ষ্য করুন — int(s, 16) আর int(s, 2) নিজেরাই 0x/0b prefix আর - চিহ্ন বুঝে নেয়, তাই আলাদা করে parse করার দরকার নেই।

২. Bit pattern আর two’s complement wraparound

Python-এর int arbitrary-precision — তাই hardware-এর মতো একটা নির্দিষ্ট bit-width-এ wraparound হওয়াটা নিজেই simulate করতে হবে।

def analyze_int(value: int, bits: int):
    lo_signed = -(1 \<\< (bits - 1))
    hi_unsigned = (1 \<\< bits) - 1
    overflowed = value \< lo_signed or value > hi_unsigned

    mask = (1 \<\< bits) - 1
    pattern = value & mask                        # unsigned bit pattern, wraparound সহ

    sign_bit = 1 \<\< (bits - 1)
    signed_value = pattern - (1 \<\< bits) if pattern & sign_bit else pattern

    return pattern, signed_value, overflowed

value & mask লাইনটাই আসল কাজ — এটা Python-কে বাধ্য করছে হার্ডওয়্যারের মতো আচরণ করতে। 300 & 0xFF = 44 ঠিক তেমনই wraparound যেমন একটা 8-bit CPU register-এ ঘটত। এটাই Signed Integers লেসনের overflow আলোচনার প্রত্যক্ষ প্রমাণ।

৩. Raw bytes আর endianness

import sys

def to_raw_bytes(pattern: int, bits: int, endian: str) -> bytes:
    return pattern.to_bytes(bits // 8, byteorder=endian)

def format_binary(pattern: int, bits: int) -> str:
    raw_bits = format(pattern, f'0{bits}b')
    groups = [raw_bits[i:i + 8] for i in range(0, bits, 8)]
    return ' '.join(groups)

binary আউটপুট সবসময় most-significant-bit আগে দেখায় (সংখ্যা যেভাবে লেখা হয়, ঠিক সেভাবে) — কিন্তু raw bytes মেমরিতে যেভাবে সাজানো থাকে সেভাবে, যেটা endianness-এর উপর নির্ভর করে। এই দুইটার ক্রম যে আলাদা হতে পারে — সেটাই এন্ডিয়ানেস লেসনের মূল কথা। --endian flag দিয়ে little আর big দুইভাবেই দেখান, এবং sys.byteorder দিয়ে মেশিনের নিজের native order জানিয়ে দিন।

৪. IEEE 754 breakdown

import struct

def decode_ieee754_32(bits_pattern: int):
    sign = (bits_pattern >> 31) & 1
    exponent = (bits_pattern >> 23) & 0xFF
    mantissa = bits_pattern & 0x7FFFFF
    return sign, exponent, mantissa

def reconstruct_value(sign: int, exponent: int, mantissa: int) -> float:
    if exponent == 0 and mantissa == 0:
        return -0.0 if sign else 0.0
    if exponent == 0:                                   # subnormal
        frac = mantissa / (1 \<\< 23)
        return ((-1) ** sign) * frac * (2.0 ** -126)
    if exponent == 0xFF:
        if mantissa == 0:
            return float('-inf') if sign else float('inf')
        return float('nan')
    frac = 1 + mantissa / (1 \<\< 23)                    # normal — implicit leading 1
    return ((-1) ** sign) * frac * (2.0 ** (exponent - 127))

এখন হার্ডওয়্যারের decode-এর সাথে নিজের ম্যানুয়াল reconstruction মিলিয়ে দেখা যায়, struct মডিউল ব্যবহার করে:

def print_float_breakdown(bits_pattern: int, original: float = None):
    sign, exponent, mantissa = decode_ieee754_32(bits_pattern)
    stored = struct.unpack('\<f', bits_pattern.to_bytes(4, 'little'))[0]
    reconstructed = reconstruct_value(sign, exponent, mantissa)

    print(f"sign        : {sign}  ({'ঋণাত্মক' if sign else 'ধনাত্মক'})")
    print(f"exponent    : {exponent:08b}  (raw={exponent}, biased-127={exponent - 127})")
    print(f"mantissa    : {mantissa:023b}  (raw={mantissa})")
    print(f"stored value: {stored!r}")
    print(f"manual reconstruct == hardware decode: {reconstructed == stored}")

    if original is not None:
        error = stored - original
        print(f"intended (double) value: {original!r}")
        print(f"precision error (stored - intended): {error!r}")

reconstructed == stored লাইনটা আসলে একটা assertion-এর কাজ করছে — যদি এটা False দেখায়, বুঝবেন reconstruct_value-এ ভুল আছে (একটা ব্যতিক্রম: NaN কখনো নিজের সমান হয় না, তাই NaN ইনপুটে এটা False দেখাবে — সেটাও IEEE 754-এর নিজের একটা বিখ্যাত নিয়ম)।

stored - original লাইনটাই আসল শিক্ষা — original হলো Python-এর double precision-এ পড়া মান (3.14), আর stored হলো সেটাকে single precision-এ round করার পর যা টেকে। এই দুইয়ের ফারাকই single precision-এর precision loss।

৫. সব জোড়া লাগিয়ে CLI বানানো

import argparse

def analyze(raw_input: str, bits: int, endian: str):
    value, is_float_input = parse_input(raw_input)
    kind = 'float' if is_float_input else 'integer'
    print(f"input       : {raw_input!r}  →  parsed as {kind} {value}")

    if not is_float_input:
        pattern, signed_value, overflowed = analyze_int(value, bits)
        raw = to_raw_bytes(pattern, bits, endian)

        print(f"bit width   : {bits}")
        print(f"binary      : {format_binary(pattern, bits)}")
        print(f"raw bytes ({endian}-endian, native={sys.byteorder}): {raw.hex(' ')}")
        print(f"unsigned    : {pattern}")
        print(f"signed (2's complement): {signed_value}")
        if overflowed:
            print(f"⚠ overflow  : {value} {bits}-bit-এ ধরে না, wrap হয়ে {signed_value} হয়ে গেছে")

        if bits >= 32:
            print()
            print("--- একই 32-bit pattern-কে float32 হিসেবে reinterpret করলে ---")
            print_float_breakdown(pattern & 0xFFFFFFFF)
    else:
        raw = struct.pack('\<f', value)
        if endian == 'big':
            raw = raw[::-1]
        float_bits = int.from_bytes(struct.pack('\<f', value), 'little')

        print(f"bit width   : 32 (single-precision)")
        print(f"binary      : {format_binary(float_bits, 32)}")
        print(f"raw bytes ({endian}-endian): {raw.hex(' ')}")
        print_float_breakdown(float_bits, original=value)


def main():
    parser = argparse.ArgumentParser(description='যেকোনো মানের bit-level breakdown')
    parser.add_argument('value', help='decimal / hex (0x..) / binary (0b..) / float')
    parser.add_argument('--bits', type=int, choices=[8, 16, 32, 64], default=32)
    parser.add_argument('--endian', choices=['little', 'big'], default=sys.byteorder)
    args = parser.parse_args()
    analyze(args.value, args.bits, args.endian)


if __name__ == '__main__':
    main()

নিজেকে চ্যালেঞ্জ করুন

  1. 64-bit double সাপোর্ট যোগ করুন — sign(1)/exponent(11)/mantissa(52) নিয়ে একই breakdown
  2. ASCII/Unicode reinterpretation — একই raw bytes-কে UTF-8 text হিসেবে পড়ার চেষ্টা করে দেখান (invalid হলে বলুন কেন)
  3. --compare flag — দুইটা মান দিয়ে তাদের raw bytes-এর মধ্যে ঠিক কয়টা bit আলাদা (Hamming distance) সেটা highlight করুন
  4. NaN payload inspection — NaN-এর mantissa bit-এ payload/signaling-bit encode থাকে, সেটাও দেখান
  5. Memory dump mode — একগুচ্ছ মান পাশাপাশি রেখে আসল hexdump-এর মতো output বানান

এটা যেখানে গিয়ে মিশবে

এখানে যা শিখলেনপরে কোথায় লাগবে
Two’s complement wraparound simulate করাLevel 2 — ALU-তে ওভারফ্লো ফ্ল্যাগ, adder সার্কিট
Endianness-এর raw byte orderLevel 3 — CPU architecture, instruction encoding, memory dump পড়া
IEEE 754 sign/exponent/mantissa breakdownLevel 2 — FPU/floating-point unit ডিজাইন
একই bit pattern-এর ভিন্ন interpretationLevel 5 — Compiler-এর constant literal encoding, type punning
Raw bit manipulation, memory inspectionLevel 4 — Debugger, core dump, /proc/pid/mem পড়া
Bit-pattern-নির্ভর bug/exploit বোঝাLevel 10 — Type confusion, buffer overflow-এর মূল কারণ