সরাসরি পর্বে যান
পর্ব ৬২ · ৬২-এর মধ্যে · মডিউল ১০

Low-level ইন্টারভিউ — bit manipulation, memory আর embedded প্রশ্ন

Low-Level Interview Questions — Bit Manipulation, Integer Pitfalls, Memory Layout, Embedded C & Reading Assembly

Taught in Bangla, this page covers low-level interview questions with ten runnable C programs on Linux x86-64: bit manipulation classics such as powers of two, popcount, rotate, reverse, parity and Gray code, endianness and byte swaps, integer pitfalls and undefined behaviour, struct padding, stack versus heap, ring buffers and memory pools, embedded C with volatile, interrupts and fixed-point, and reading RISC-V and x86-64 assembly.

এই পর্বে যা শিখবেন

  • Low-level কোডিং প্রশ্নে width, signedness আর edge case জিজ্ঞেস করে, হাতে উদাহরণ দেখিয়ে, test-সহ উত্তর দেওয়ার কাঠামো
  • ২-এর ঘাত, popcount, সবচেয়ে নিচের 1, rotate, bit উল্টানো, parity, Gray code, পরের ২-এর ঘাত আর “+” ছাড়া যোগ — প্রমাণ আর hardware instruction-সহ
  • Undefined, implementation-defined আর well-defined আচরণ আলাদা করে integer promotion, overflow, shift আর char-এর ফাঁদ সৎভাবে ব্যাখ্যা করা
  • Struct padding-এর হিসাব, stack বনাম heap, pointer alignment, ring buffer আর fixed-size memory pool নিজে লেখা
  • Embedded প্রশ্ন — volatile, register-এর macro, interrupt-এর নিয়ম, debouncing, watchdog, UART-I2C-SPI, Q15 আর Q16.16 — আর ছোট RISC-V ও x86-64 assembly পড়ে বলা সেটি কী হিসাব করে

১ · Low-level প্রশ্ন কীভাবে সামলাবেন — কাঠামো আর রিভিশনের মানচিত্র

Embedded বা firmware engineer, systems programmer, device driver, হার্ডওয়্যার-যাচাই (verification), এমনকি performance-নির্ভর backend পদের ইন্টারভিউতে প্রায়ই একটি ছোট কোডিং অংশ থাকে, যেখানে প্রশ্নগুলো দেখতে খুব ছোট: “একটি সংখ্যায় কয়টি বিট 1, বের করুন”, “এই struct-এর sizeof কত?”, “volatile কী করে?” এ ধরনের প্রশ্ন প্রায়ই আসে, তবে ধরন প্রতিষ্ঠান আর পদভেদে আলাদা। উত্তর দুই লাইনের, কিন্তু ইন্টারভিউয়ার দেখেন আপনি মেশিনটিকে কতটা চেনেন — বিটের width, signed-unsigned, ভাষার নিয়ম আর hardware-এর আচরণের পার্থক্য।

বাড়ির মেইন সুইচবোর্ডের মিস্ত্রির মতো ভাবুন। “তৃতীয় বাতিটা জ্বালিয়ে দিন” — ভালো মিস্ত্রি আগে জিজ্ঞেস করেন, বোর্ডে কয়টি সুইচ, কোনটি কোন দিক থেকে গোনা, আর বাকিগুলো কি যেমন আছে তেমন থাকবে। তারপর ঠিক একটি সুইচ টেপেন, আর শেষে বাতি জ্বলল কি না দেখে নেন। Low-level প্রশ্নেও তাই: মাপ আর নিয়ম জেনে নিন, ঠিক যে বিটটি দরকার কেবল সেটি বদলান, আর test দিয়ে প্রমাণ দিন।

ছয় ধাপের কাঠামো

  1. Width আর signedness জিজ্ঞেস করুন: “৩২-বিট unsigned ধরে নিচ্ছি — ঠিক আছে?” int-এর মাপ ISA আর ABI-ভেদে আলাদা হতে পারে; uint32_t-এর মতো নির্দিষ্ট-মাপের type বললে দ্ব্যর্থতা থাকে না।
  2. Edge case আগে বলুন: 0, 1, সব বিট 1 (0xFFFFFFFF), সবচেয়ে বড় আর সবচেয়ে ছোট signed মান (INT_MIN), shift-এর পরিমাণ 0 বা width-এর সমান, একই pointer দুবার (aliasing).
  3. হাতে একটি ছোট উদাহরণ: ৮ বিটের একটি মান নিয়ে বিট ধরে দেখান। এতে ভুল ধরা পড়ে, আর ইন্টারভিউয়ার আপনার ভাবনা অনুসরণ করতে পারেন।
  4. কোড লিখুন — unsigned-এ, mask 1u দিয়ে: বেশিরভাগ বিট-কাজ unsigned-এ নিরাপদ; signed-এ shift আর overflow-এর নিয়ম ফাঁদে ভরা (অংশ ৫)।
  5. Test আর জটিলতা: কয়েকটি ইনপুট-আউটপুট জোড়া বলুন, লুপ কতবার চলে বলুন (যেমন Kernighan-এর popcount চলে যতগুলো 1 ততবার), আর hardware-এ এক instruction আছে কি না (popcnt, cpop, rev8).
  6. Portability বলুন: কোন অংশ ভাষার মান (standard) নিশ্চিত করে, কোনটি compiler বা ISA-নির্ভর, আর কোনটি পুরো undefined — এই তিন ভাগ স্পষ্ট করলেই উত্তরটি পরিণত শোনায়।
তিনটি শব্দ যা ইন্টারভিউতে আলাদা রাখতে হবে
  • Well-defined: মান ফল নির্দিষ্ট করে দেয়, সব মেশিনে এক। যেমন unsigned-এর wrap-around: 0u - 1 = UINT_MAX.
  • Implementation-defined: ফল compiler-কে বেছে নিয়ে লিখে রাখতে হয় (documentation-এ), তাই এক compiler-এ স্থির কিন্তু অন্যটিতে আলাদা হতে পারে। যেমন ঋণাত্মক signed-এর ডান shift, plain char signed কি না।
  • Undefined behaviour (UB): মান কিছুই বলে না — compiler ধরে নিতে পারে এটি কখনো ঘটবে না, আর সেই অনুমানে কোড বদলে দিতে পারে। যেমন signed overflow, width-এর সমান বা বেশি shift, null pointer পড়া।

এই পাতার প্রশ্নগুলো কোন পর্বের জ্ঞান চায়, নিচের টেবিলে দেখুন। কোনো অংশে আটকে গেলে আগে সেই পর্বটি আবার পড়ে আসুন — পর্বগুলোই সত্যের উৎস, আর সংখ্যা ও নিয়ম এখানে হুবহু সেগুলো থেকেই নেওয়া।

অংশবিষয়রিভিশন
২২-এর ঘাত, popcount, নিচের 1, sign, abs০৫, ০৬, ২০
৩Field, rotate, reverse, parity, Gray, “+” ছাড়া যোগ০৪, ০৬, ১১, ১৩
৪Endianness আর byte swap০৬
৫Overflow, UB, promotion, shift, char০৫, ২০, ৩১
৬Padding, alignment, stack-heap, pool, ring buffer০৬, ৩০, ৪৫, ৫০
৭volatile, interrupt, debouncing, watchdog, bus, fixed-point১৮, ২১, ৫১, ৫২, ৫৬
৮RISC-V আর x86-64 assembly পড়া২৬, ২৯, ৩১

সব C প্রোগ্রাম চলে Wandbox-এ — gcc, Linux x86-64, glibc, little-endian; compile হয় warning চালু রেখে। ফলে যেখানে আচরণ implementation-defined (যেমন char-এর চিহ্ন), সেখানে আউটপুট এই পরিবেশের সত্য — অন্য ISA-তে আলাদা হতে পারে, আর ব্যাখ্যায় সেটি বলা আছে। কোনো প্রোগ্রামেই UB ঘটানো হয়নি; UB-র উদাহরণগুলো শুধু লেখা হিসেবে দেখানো।

২ · Bit manipulation-এর ক্লাসিক (১) — ২-এর ঘাত, popcount, নিচের 1, sign আর abs

রিভিশন: পর্ব ০৫ · two's complement, পর্ব ০৬ · bit manipulation, পর্ব ২০ · integer যোগ-বিয়োগ

প্রায় সব বিট-কৌশলের মূলে একটি পর্যবেক্ষণ: x − 1 করলে সবচেয়ে নিচের 1-বিটটি 0 হয়, আর তার নিচের সব 0 হয়ে যায় 1 (ধার নেওয়া, পর্ব ২০)। ওপরের বিটগুলো বদলায় না। তাই x & (x − 1) সবচেয়ে নিচের 1 মুছে দেয়। আর two's complement-এ −x = ~x + 1 (পর্ব ০৫), তাই x & −x শুধু সবচেয়ে নিচের 1-কে একা রাখে। এই দুটি সূত্র থেকেই নিচের অর্ধেক প্রশ্নের উত্তর আসে। নিচের stepper-এ popcount-এর Kernighan-কৌশল ধাপে ধাপে দেখুন।

শুরুn = 0
x       : 0110 1100   (0x6C = 108)
          set bits at 6, 5, 3, 2
n       : 0
ঘুর ১বিট ২ মুছল
x       : 0110 1100
x - 1   : 0110 1011
x & x-1 : 0110 1000   (0x68)
n       : 1
ঘুর ২বিট ৩ মুছল
x       : 0110 1000
x - 1   : 0110 0111
x & x-1 : 0110 0000   (0x60)
n       : 2
ঘুর ৩বিট ৫ মুছল
x       : 0110 0000
x - 1   : 0101 1111
x & x-1 : 0100 0000   (0x40)
n       : 3
ঘুর ৪x = 0, থামুন
x       : 0100 0000
x - 1   : 0011 1111
x & x-1 : 0000 0000   (0)
n       : 4   -> popcount(0x6C) = 4
  1. শুরু. x = 0x6C = 0110 1100; বিট ৬, ৫, ৩ আর ২-এ 1 — মোট চারটি। গণক n = ০। লুপের শর্ত: x শূন্য না হওয়া পর্যন্ত x &= x - 1; n++.
  2. প্রথম ঘুর. x − 1 = 0110 1011: সবচেয়ে নিচের 1 (বিট ২) 0 হলো, নিচের দুই বিট 1 হলো। AND করলে বিট ২ আর তার নিচের অংশ মুছে যায়: 0x68. n = ১।
  3. দ্বিতীয় ঘুর. এখন সবচেয়ে নিচের 1 বিট ৩-এ। x − 1 = 0110 0111, AND-এ ফল 0x60. n = ২। লক্ষ করুন, শূন্য বিটগুলোর জন্য কোনো ঘুর খরচ হয়নি।
  4. তৃতীয় ঘুর. 0x60 − 1 = 0x5F = 0101 1111; AND-এ 0x40 — বিট ৫ গেল। n = ৩।
  5. চতুর্থ ঘুর আর শেষ. 0x40 − 1 = 0x3F; AND-এ ০। n = ৪, লুপ থামল। লুপ চলেছে ঠিক 1-বিটের সংখ্যার সমান বার — ৮টি বিটের সবগুলো পরীক্ষা করার সরল লুপের চেয়ে কম। সবচেয়ে খারাপ অবস্থা (সব বিট 1) তবুও ৩২ ঘুর।

এবার পুরো toolkit একসাথে test-সহ চালান — is_pow2, Kernighan বনাম gcc-র __builtin_popcount, x & -x, x & (x - 1), branch ছাড়া sign আর abs, আর দুটি সংখ্যার চিহ্ন বিপরীত কি না:

#include <stdio.h>
#include <stdint.h>

static int is_pow2(uint32_t x) { return x && !(x & (x - 1)); }
static int pop_k(uint32_t x) { int n = 0; while (x) { x &= x - 1; n++; } return n; }
static int sign_of(int32_t x) { return (x > 0) - (x < 0); }
static uint32_t abs_nb(int32_t x) {           /* branchless |x| */
    uint32_t m = (uint32_t)(x >> 31);         /* gcc: arithmetic shift -> 0 or all ones */
    return ((uint32_t)x ^ m) - m;             /* unsigned math, so no overflow UB */
}
static int opposite(int32_t a, int32_t b) { return (a ^ b) < 0; }

int main(void) {
    uint32_t t[] = {0, 1, 6, 64, 96, 0x80000000u, 0xFFFFFFFFu};
    printf("x           pow2 popK builtin  x&-x        x&(x-1)\n");
    for (int i = 0; i < 7; i++) {
        uint32_t x = t[i];
        printf("0x%08X  %d    %2d   %2d       0x%08X  0x%08X\n", x, is_pow2(x),
               pop_k(x), __builtin_popcount(x), x & (0u - x), x & (x - 1));
    }
    int32_t v[] = {-7, 0, 42, INT32_MIN};
    for (int i = 0; i < 4; i++)
        printf("x=%11d  sign=%2d  |x|=%u\n", (int)v[i], sign_of(v[i]), abs_nb(v[i]));
    printf("opposite(5,-3)=%d  opposite(-5,-3)=%d  opposite(0,-1)=%d\n",
           opposite(5, -3), opposite(-5, -3), opposite(0, -1));
    return 0;
}

Wandbox-এর gcc (Linux x86-64) আউটপুট:

x           pow2 popK builtin  x&-x        x&(x-1)
0x00000000  0     0    0       0x00000000  0x00000000
0x00000001  1     1    1       0x00000001  0x00000000
0x00000006  0     2    2       0x00000002  0x00000004
0x00000040  1     1    1       0x00000040  0x00000000
0x00000060  0     2    2       0x00000020  0x00000040
0x80000000  1     1    1       0x80000000  0x00000000
0xFFFFFFFF  0    32   32       0x00000001  0xFFFFFFFE
x=         -7  sign=-1  |x|=7
x=          0  sign= 0  |x|=0
x=         42  sign= 1  |x|=42
x=-2147483648  sign=-1  |x|=2147483648
opposite(5,-3)=1  opposite(-5,-3)=0  opposite(0,-1)=1

২-এর ঘাত: ১, ৬৪ আর 0x80000000 পাস করল; ৬ আর ৯৬-তে দুটি করে 1, তাই না। ০-এর সারিটি লক্ষ করুন — x && অংশটি না থাকলে ০-কেও ২-এর ঘাত বলা হতো, কারণ 0 & (0 - 1) = ০। popcount: আমাদের লুপ আর built-in সব সারিতে মেলে, সব বিট 1-এ ৩২। x & -x ৬ (0110)-এ ২ দেয়, ৯৬-এ ৩২ — সবচেয়ে নিচের 1-এর মান; আর x & (x-1) সেটিকেই মুছে দেয় (৬ → ৪, ৯৬ → ৬৪)। unsigned-এ -x লিখেছি 0u - x হিসেবে — well-defined.

sign আর abs: (x > 0) - (x < 0) কোনো branch ছাড়াই −১, ০ বা ১ দেয়। branchless abs-এ m = x >> 31 হয় ০ বা সব বিট 1; (x ⊕ m) − m মানে ঋণাত্মক হলে “বিট উল্টে ১ যোগ” — অর্থাৎ two's complement negation. শেষ সারির INT32_MIN-এর |x| = 2147483648 এসেছে কারণ ফল unsigned; int-এ ফেরত নিলে এটি ধরত না। opposite: চিহ্ন-বিট আলাদা হলে XOR-এর চিহ্ন-বিট 1, অর্থাৎ ঋণাত্মক — ০ আর −১-এর ক্ষেত্রেও ১, কারণ ০-কে এখানে “ধনাত্মক চিহ্ন” ধরা হয়।

প্র ০১একটি ৩২-বিট unsigned সংখ্যা ২-এর ঘাত কি না, এক লাইনে বের করুন। ০-এর ক্ষেত্রে কী হবে? (সহজ)

উত্তর: return x != 0 && (x & (x - 1)) == 0; যুক্তি: ২-এর ঘাতে ঠিক একটি বিট 1; x & (x - 1) সবচেয়ে নিচের 1 মোছে, তাই একটিমাত্র 1 থাকলে ফল ০। ০-তে কোনো 1 নেই, কিন্তু 0 & 0xFFFFFFFF = ০, তাই আলাদা পরীক্ষা x != 0 না দিলে ০-কে ভুল করে ২-এর ঘাত বলা হবে। জটিলতা O(1), দুই-তিনটি instruction.

ফলো-আপ: (ক) signed int হলে? ঋণাত্মক সংখ্যা কখনো ২-এর ঘাত নয়, তাই x > 0 শর্ত দিন; আর INT_MIN-এ x - 1 overflow — UB, তাই আগে unsigned-এ রূপান্তর নিরাপদ। (খ) “৪-এর ঘাত?” — ২-এর ঘাত হতে হবে এবং একমাত্র 1-টি জোড় অবস্থানে: (x & 0x55555555u) != 0. (গ) C23-এ <stdbit.h>-এ এ ধরনের কাজের মানক ফাংশন যোগ হয়েছে (যেমন stdc_has_single_bit) — compiler সমর্থন দেখে নিন।

প্র ০২একটি সংখ্যায় কয়টি বিট 1 (popcount) — তিনটি পদ্ধতি বলুন, জটিলতা আর hardware সমর্থনসহ। (মাঝারি)

উত্তর: (১) সরল লুপ: প্রতি ঘুরে n += x & 1; x >>= 1; — সবসময় width পর্যন্ত, অর্থাৎ ৩২ ঘুর (বা সবচেয়ে উঁচু 1 পর্যন্ত)। (২) Kernighan: x &= x - 1 যতবার — লুপ চলে k বার, k = 1-এর সংখ্যা (ওপরের stepper). Sparse সংখ্যায় দ্রুত। (৩) Lookup table: ২৫৬ ঘরের টেবিল থেকে প্রতি byte-এর গণনা, চারটি lookup আর যোগ — মেমরি খরচ ২৫৬ byte, cache-এ থাকলে দ্রুত। (৪) SWAR (SIMD within a register): জোড়ায় জোড়ায় যোগ — x - ((x >> 1) & 0x55555555), তারপর ২-বিট, ৪-বিট গুচ্ছ — লুপ ছাড়াই প্রায় ১২টি instruction, ধ্রুব সময়।

Hardware: x86-এর popcnt (আলাদা CPUID বিট, ২০০৮-এর কাছাকাছি সময়ের CPU থেকে), RISC-V Zbb-এর cpop, পুরোনো ARM64-এ SIMD-এর cnt. gcc-র __builtin_popcount লিখলে compiler লক্ষ্য CPU অনুযায়ী instruction বা বিকল্প কোড বেছে নেয়। মজার তথ্য: gcc 13 -O2 -mpopcnt-এ আমাদের Kernighan লুপটিকেই চিনে এক popcnt-এ নামিয়ে আনে (অংশ ৮-এ listing).

ফলো-আপ: “০ থেকে n পর্যন্ত সবগুলোর popcount” — DP: bits[i] = bits[i >> 1] + (i & 1), O(n). “দুটি সংখ্যার মধ্যে কয়টি বিট আলাদা?” — popcount(a ^ b), অর্থাৎ Hamming distance (পর্ব ০৪)।

প্র ০৩তৃতীয় ভ্যারিয়েবল ছাড়া দুটি int অদলবদল করুন। আর বলুন, কেন বাস্তব কোডে এটি করবেন না। (মাঝারি)

উত্তর: XOR দিয়ে: a ^= b; b ^= a; a ^= b; — কারণ (a ⊕ b) ⊕ b = a. যোগ-বিয়োগ দিয়েও হয় (a = a + b; b = a - b; a = a - b;), কিন্তু signed-এ a + b overflow হলে UB — তাই সেটি আরও খারাপ।

কেন নয়: (১) Aliasing: দুটি pointer একই জায়গা দেখালে প্রথম লাইনেই মান ০ হয়ে যায় — অংশ ৩-এর প্রোগ্রামে xor_swap(&p,&p) সত্যিই ০ ছাপে। (২) গতি: তিনটি instruction একটির ওপর আরেকটি নির্ভরশীল, তাই out-of-order CPU (পর্ব ৪০) সমান্তরালে চালাতে পারে না; আর temp দিয়ে লেখা সাধারণ swap compiler প্রায়ই পরের কোডে দুই register-এর ভূমিকা অদলবদল করেই সেরে ফেলে (register allocation) — কোনো instruction ছাড়াই; mov লাগলেও অনেক আধুনিক core সেটি register renaming-এর ধাপেই মিটিয়ে দেয়। (৩) পড়ার সুবিধা নেই। মূল কথা: “register বাঁচানো”র যুক্তিটি ১৯৭০–৮০-এর ছোট মেশিনের, আজ প্রায় সবসময় অর্থহীন।

ফলো-আপ: “তাহলে এর ব্যবহার কোথায়?” — বিট-কৌশল বোঝার অনুশীলন, আর XOR-এর ধর্ম (নিজের সাথে XOR = ০, ০-এর সাথে XOR = নিজে) — যা RAID parity আর “একটি array-তে একবার থাকা সংখ্যাটি খুঁজুন”-ধরনের প্রশ্নে কাজে লাগে।

প্র ০৪Branch ছাড়া abs(x) লিখুন। কোন কোন জায়গায় এটি portable নয়? (কঠিন)

উত্তর: m = x >> 31; return (x ^ m) - m; — x ঋণাত্মক হলে m = −১ (সব বিট 1), তখন x ⊕ m = ~x, আর ~x − (−১) = ~x + ১ = −x. ধনাত্মক হলে m = ০, ফল x. gcc 13 -O2-এ এটি হয় sar, xor, sub — সাথে দুটি mov; অংশ ৮-এর listing C দেখুন। তবে লক্ষ করুন, সাধারণ abs()-ও compiler প্রায়ই branch ছাড়া বানায় — x86-এ neg আর cmov দিয়ে।

Portability-র ফাঁদ: (১) ঋণাত্মক signed সংখ্যার >> C-তে implementation-defined; gcc আর clang arithmetic shift করে, কিন্তু মান তা নিশ্চিত করে না। (২) 31 ধরে নিচ্ছে int ৩২ বিট — sizeof(int) * CHAR_BIT - 1 লিখুন। (৩) INT_MIN-এর abs int-এ ধরে না — (x ^ m) - m signed-এ overflow, অর্থাৎ UB. তাই নিরাপদ রূপ: হিসাব unsigned-এ, ফল unsigned (ওপরের প্রোগ্রামের মতো)। (৪) C23 থেকে signed integer-এর রূপ two's complement হওয়া বাধ্যতামূলক (C++20-এও), তবে C23-এ overflow এখনো UB আর ঋণাত্মক সংখ্যার ডান shift এখনো implementation-defined (C++20 সেটিকে arithmetic বলে নির্দিষ্ট করেছে) — অর্থাৎ C23 সব সমস্যা মেটায়নি।

ফলো-আপ: “branchless কেন?” — ডেটা-নির্ভর অনিশ্চিত branch-এ misprediction-এর খরচ (পর্ব ৩৭), আর constant-time cryptography-তে branch সময় দিয়ে গোপন তথ্য ফাঁস করতে পারে (পর্ব ৪১)।

৩ · Bit manipulation-এর ক্লাসিক (২) — field, rotate, reverse, parity, Gray, “+” ছাড়া যোগ

রিভিশন: পর্ব ০৪ · Gray code, পর্ব ০৬ · bit field, পর্ব ১১ · adder, পর্ব ১৩ · ALU আর rotate, পর্ব ১৮ · Gray pointer

দ্বিতীয় দলের প্রশ্নগুলো একটু লম্বা, কিন্তু সবই mask, shift আর XOR দিয়ে গড়া। Bit field-এর সূত্রটি পর্ব ০৬ থেকে মনে রাখুন: শুরু s, চওড়া w হলে mask = 2w − 1, বের করা (x >> s) & mask, বসানো (x & ~(mask << s)) | ((v & mask) << s). পরীক্ষার জন্য আবার সেই RGB565 রঙ 0x3DA9 নেওয়া হলো, যাতে পর্ব ০৬-এর সাথে মেলানো যায়।

#include <stdio.h>
#include <stdint.h>

static uint32_t get_field(uint32_t x, unsigned s, unsigned w) {   /* 1 <= w <= 31 */
    return (x >> s) & ((1u << w) - 1);
}
static uint32_t set_field(uint32_t x, unsigned s, unsigned w, uint32_t v) {
    uint32_t m = ((1u << w) - 1) << s;
    return (x & ~m) | ((v << s) & m);
}
static uint32_t rotl(uint32_t x, unsigned r) { r &= 31; return (x << r) | (x >> ((32 - r) & 31)); }
static uint32_t rotr(uint32_t x, unsigned r) { r &= 31; return (x >> r) | (x << ((32 - r) & 31)); }
static uint32_t rev32(uint32_t x) {                 /* swap 1-bit, 2-bit, 4-bit groups, then bytes */
    x = ((x >> 1) & 0x55555555u) | ((x & 0x55555555u) << 1);
    x = ((x >> 2) & 0x33333333u) | ((x & 0x33333333u) << 2);
    x = ((x >> 4) & 0x0F0F0F0Fu) | ((x & 0x0F0F0F0Fu) << 4);
    return __builtin_bswap32(x);
}
static int parity(uint32_t x) { x ^= x >> 16; x ^= x >> 8; x ^= x >> 4; x ^= x >> 2; x ^= x >> 1; return x & 1; }

int main(void) {
    uint32_t px = 0x3DA9;                            /* RGB565 pixel */
    printf("green field = %u, insert 63 -> 0x%04X\n", get_field(px, 5, 6), set_field(px, 5, 6, 63));
    printf("rotl(0x80000001,1) = 0x%08X  rotr(0x1,4) = 0x%08X  rotl(x,0) = 0x%08X\n",
           rotl(0x80000001u, 1), rotr(1u, 4), rotl(0x12345678u, 0));
    printf("rev32(0x00000001) = 0x%08X  rev32(0x12345678) = 0x%08X\n", rev32(1u), rev32(0x12345678u));
    printf("parity: 0x07 -> %d, 0xB4 -> %d, builtin(0xB4) -> %d\n",
           parity(0x07u), parity(0xB4u), __builtin_parity(0xB4u));
    return 0;
}

আসল রানের আউটপুট:

green field = 45, insert 63 -> 0x3FE9
rotl(0x80000001,1) = 0x00000003  rotr(0x1,4) = 0x10000000  rotl(x,0) = 0x12345678
rev32(0x00000001) = 0x80000000  rev32(0x12345678) = 0x1E6A2C48
parity: 0x07 -> 1, 0xB4 -> 0, builtin(0xB4) -> 0

Field: সবুজ ৪৫ আর ৬৩ বসানোর পরে 0x3FE9 — পর্ব ০৬-এর stepper-এর সাথে হুবহু। Rotate: 0x80000001 বাঁয়ে এক ঘর ঘোরালে ওপরের 1 নিচে চলে আসে, তাই 0x00000003; ১-কে ডানে ৪ ঘর ঘোরালে 0x10000000. rotl(x,0) অক্ষত — (32 - r) & 31 লেখার কারণ এটাই: r = ০ হলে নইলে x >> 32 হতো, যা UB. gcc এই ধাঁচ চিনে x86-এ এক rol বানায় (অংশ ৮)।

Reverse: প্রথমে পাশাপাশি বিট, তারপর ২-বিটের জোড়া, তারপর nibble অদলবদল — প্রতিটি byte-এর ভেতরে বিট উল্টে গেল; শেষে byte-ক্রম উল্টে (bswap) পুরো ৩২ বিট উল্টানো। 0x12345678 → 0x1E6A2C48 হাতেও মেলানো যায়: 0001 0010 … 1000 উল্টে 0001 1110 … 1000. Parity: XOR-এর ভাঁজ (fold) — ১৬, ৮, ৪, ২, ১ ঘর সরিয়ে XOR করলে সব বিটের XOR শেষ বিটে আসে। 0x07-এ তিনটি 1 (বিজোড়) → ১; 0xB4-এ চারটি → ০, built-in-ও তাই বলে।

এবার তিনটি ছোট কিন্তু প্রিয় প্রশ্ন — Gray code-এ যাওয়া-আসা, পরের ২-এর ঘাত, আর “+” অপারেটর ছাড়া যোগ — সাথে XOR swap-এর ফাঁদটি চোখে দেখা:

#include <stdio.h>
#include <stdint.h>

static uint32_t to_gray(uint32_t b) { return b ^ (b >> 1); }
static uint32_t from_gray(uint32_t g) { for (uint32_t s = g >> 1; s; s >>= 1) g ^= s; return g; }
static uint32_t next_pow2(uint32_t x) {        /* smallest 2^k >= x; valid for 1 .. 2^31 */
    x--; x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16;
    return x + 1;
}
static uint32_t add_noplus(uint32_t a, uint32_t b) {
    while (b) { uint32_t carry = (a & b) << 1; a ^= b; b = carry; }
    return a;
}
static void xor_swap(int *a, int *b) { *a ^= *b; *b ^= *a; *a ^= *b; }
static void bits3(uint32_t v) { for (int k = 2; k >= 0; k--) putchar('0' + (int)((v >> k) & 1u)); }

int main(void) {
    printf("n bin gray back\n");
    for (uint32_t n = 0; n < 8; n++) {
        printf("%u ", n); bits3(n); putchar(' '); bits3(to_gray(n));
        printf("  %u\n", from_gray(to_gray(n)));
    }
    printf("next_pow2: 1->%u 5->%u 64->%u 65->%u 0->%u (edge!)\n",
           next_pow2(1), next_pow2(5), next_pow2(64), next_pow2(65), next_pow2(0));
    printf("add_noplus(13,11)=%u  add_noplus(0xFFFFFFFF,1)=%u\n", add_noplus(13, 11), add_noplus(0xFFFFFFFFu, 1));
    int p = 3, q = 5;
    xor_swap(&p, &q); printf("xor_swap(3,5) -> %d %d\n", p, q);
    xor_swap(&p, &p); printf("xor_swap(&p,&p) -> p = %d (lost!)\n", p);
    return 0;
}

আসল রানের আউটপুট:

n bin gray back
0 000 000  0
1 001 001  1
2 010 011  2
3 011 010  3
4 100 110  4
5 101 111  5
6 110 101  6
7 111 100  7
next_pow2: 1->1 5->8 64->64 65->128 0->0 (edge!)
add_noplus(13,11)=24  add_noplus(0xFFFFFFFF,1)=0
xor_swap(3,5) -> 5 3
xor_swap(&p,&p) -> p = 0 (lost!)

Gray code: b ^ (b >> 1); টেবিলে পরপর দুটি Gray সংখ্যায় ঠিক একটি বিট বদলায় (000 → 001 → 011 → 010 → 110 …), পর্ব ০৪-এর মতো। উল্টো পথে প্রতিটি বিট = ওপরের সব Gray বিটের XOR, তাই লুপে g ^= g >> 1, >> 2, … — back কলামে আবার ০–৭ ফিরেছে। পরের ২-এর ঘাত: x−1 করে সবচেয়ে উঁচু 1-এর নিচের সব বিট “ভরিয়ে” (OR-এর ভাঁজ) ১ যোগ — ৫ → ৮, ৬৪ → ৬৪ (x−1 থাকায় নিজেই ঘাত হলে অক্ষত), ৬৫ → ১২৮। কিন্তু ০ → ০, আর 231-এর বেশি হলে ফল ধরে না — এই দুটি edge case বলুন।

“+” ছাড়া যোগ: ১৩ + ১১ = ২৪, আর 0xFFFFFFFF + 1 = ০ — ঠিক hardware adder-এর মতো wrap-around. নিচের stepper-এ ঘুরগুলো দেখুন। XOR swap: ৩ আর ৫ অদলবদল ঠিক হলো, কিন্তু একই ঠিকানা দুবার দিলে মান ০ হয়ে গেল — প্র ০৩-এর প্রথম কারণ।

শুরুa = 13, b = 11
a       : 0 1101   (13)
b       : 0 1011   (11)
rule    : sum = a ^ b      (add, ignore carries)
          carry = (a & b) << 1
ঘুর ১a = 6, b = 18
a & b   : 0 1001  -> << 1 = 1 0010  (18)
a ^ b   : 0 0110                (6)
now a = 6, b = 18
ঘুর ২a = 20, b = 4
a & b   : 0 0010  -> << 1 = 0 0100  (4)
a ^ b   : 1 0100                (20)
now a = 20, b = 4
ঘুর ৩a = 16, b = 8
a & b   : 0 0100  -> << 1 = 0 1000  (8)
a ^ b   : 1 0000                (16)
now a = 16, b = 8
ঘুর ৪b = 0, থামুন
a & b   : 0 0000  -> carry = 0
a ^ b   : 1 1000                (24)
b == 0  -> result 24 = 13 + 11
  1. দুই অংশে ভাঙা। একটি full adder-এর sum-বিট = a ⊕ b ⊕ cin, carry = ab + … (পর্ব ১১)। বহন বাদ দিলে প্রতিটি ঘরের যোগ শুধু XOR; যে ঘরে দুটোই 1, সেখান থেকে বহন যায় এক ঘর বাঁয়ে — তাই (a & b) << 1.
  2. প্রথম ঘুর. 1101 আর 1011-এর মিল বিট ৩ আর ০-এ (1001), বহন 10010 = ১৮। XOR 0110 = ৬। এখন সমস্যা দাঁড়াল ৬ + ১৮ — যোগফল একই (২৪), শুধু রূপ বদলাল।
  3. দ্বিতীয় ঘুর. 00110 আর 10010-এর মিল শুধু বিট ১-এ, বহন 00100 = ৪। XOR 10100 = ২০। এখন ২০ + ৪।
  4. তৃতীয় ঘুর. 10100 আর 00100-এর মিল বিট ২-এ, বহন 01000 = ৮; XOR 10000 = ১৬। বহন প্রতিবার অন্তত এক ঘর বাঁয়ে সরে — ripple carry-র মতো।
  5. শেষ. 10000 আর 01000-এর কোনো মিল নেই, বহন ০; XOR = 11000 = ২৪। লুপ সবচেয়ে বেশি width-সংখ্যক বার চলে (৩২-বিটে ৩২; ৩৩তম বহন ওপর দিয়ে পড়ে যায়, তাই wrap-around). Hardware-এ ঠিক এই ধীর পথ এড়াতেই carry-lookahead (পর্ব ১১)।
প্র ০৫একটি ৩২-বিট মানকে বাঁয়ে r ঘর rotate করুন। r = ০ বা r ≥ ৩২ হলে কী হবে? Hardware-এ কীভাবে হয়? (মাঝারি)

উত্তর: r &= 31; return (x << r) | (x >> ((32 - r) & 31)); সরল রূপ (x << r) | (x >> (32 - r))-এ r = ০ হলে x >> 32 হয় — width-এর সমান shift, অর্থাৎ UB (অংশ ৫)। & 31 দুই জায়গাতেই দিলে সব r-এ সংজ্ঞায়িত, আর rotate-এর অর্থও ঠিক থাকে (৩৩ ঘর ঘোরানো = ১ ঘর)। x অবশ্যই unsigned — signed হলে ডান shift-এ sign bit ঢুকে ফল নষ্ট।

Hardware: x86-এর rol/ror, ARM-এর ror (বাঁয়ে ঘোরানো = ডানে 32 − r), RISC-V-এর Zbb extension-এ rol/ror/rori; base RV32I-তে নেই, তাই সেখানে compiler কয়েকটি instruction বানায় (sll, srl, or — r চলক হলে সাথে একটি neg). ALU-তে rotate করে barrel shifter (পর্ব ১৩)। gcc আর clang ওপরের idiom চিনে এক instruction বানায় — অংশ ৮-এর listing-এ rol eax, cl.

ফলো-আপ: rotate কোথায় লাগে? Hash function, cryptography (যেমন ChaCha20, SHA-2-এ বহুবার), CRC-র কিছু রূপ, আর circular buffer-এর বিট-mask. C++20-এ std::rotl আছে; C-তে দীর্ঘদিন মানক rotate ছিল না, তাই এই idiom-ই প্রচলিত।

প্র ০৬একটি ৩২-বিট সংখ্যার বিটগুলো উল্টে দিন (বিট ০ ↔ বিট ৩১)। কয়টি পদ্ধতি জানেন? (মাঝারি)

উত্তর: (১) লুপ: ৩২ বার — r = (r << 1) | (x & 1); x >>= 1; — O(width). (২) ভাগ করে জয় (swap ধাপ): পাশাপাশি বিট অদলবদল (mask 0x5555…), তারপর জোড়া (0x3333…), nibble (0x0F0F…), byte, half-word — log2(৩২) = ৫ ধাপ, প্রতিটিতে দুটি shift, দুটি AND আর একটি OR; ওপরের প্রোগ্রামে শেষ দুই ধাপের বদলে এক __builtin_bswap32. (৩) Lookup table: ২৫৬ ঘরের টেবিলে প্রতিটি byte-এর উল্টো রূপ; চারটি lookup, তারপর byte-ক্রম উল্টে জোড়া।

Hardware: ARM-এর rbit এক instruction-এ পুরো বিট উল্টায়; RISC-V-এর Zbb-এর rev8 শুধু byte উল্টায়, আর প্রতি byte-এর ভেতরে বিট উল্টানোর brev8 আছে cryptography-র Zbkb extension-এ; x86-এ সাধারণ এক-instruction bit reverse নেই, byte-এর জন্য bswap. ব্যবহার: FFT-র bit-reversal ক্রম, কিছু CRC-র রূপ, ছবি উল্টানো।

ফলো-আপ: “অনেকবার ডাকা হবে” বললে টেবিল বা swap ধাপ; “মেমরি খুব কম (ছোট MCU)” বললে লুপ বা swap ধাপ — trade-off বলাটাই আসল উত্তর।

প্র ০৭Parity আর Gray code — কোথায় লাগে, আর Gray থেকে binary-তে কীভাবে ফেরেন? (মাঝারি)

উত্তর: Parity = সব বিটের XOR; even parity-তে একটি বাড়তি বিট যোগ করে মোট 1-এর সংখ্যা জোড় রাখা হয়, যাতে একটি বিট উল্টে গেলে ধরা পড়ে (UART-এর ঐচ্ছিক parity বিট, পুরোনো মেমরি, Computer Networks কোর্সের error detection). দুটি বিট উল্টালে ধরা পড়ে না — তাই ECC (Hamming, পর্ব ৪৯)। দ্রুত হিসাব: XOR-এর ভাঁজ (log2 w ধাপ) বা __builtin_parity; x86-এ শেষ ফলের নিচের byte-এর parity flag (PF) আছে।

Gray code = b ^ (b >> 1); পরপর দুই মানে ঠিক এক বিট বদলায়। ব্যবহার: rotary encoder আর যান্ত্রিক অবস্থান-sensor (দুই বিট একসাথে বদলানোর মাঝপথে ভুল পড়া এড়াতে, পর্ব ০৪), K-map-এর সারি-কলামের ক্রম (পর্ব ০৯), আর async FIFO-র pointer — clock domain পার হওয়ার সময় একবারে এক বিট বদলায় বলে synchronizer নিরাপদে পড়তে পারে (পর্ব ১৮)।

ফেরার পথ: binary-র ওপরের বিট = Gray-এর ওপরের বিট; তারপর প্রতিটি নিচের বিট = আগের binary বিট ⊕ এই Gray বিট। যেমন Gray 1011 → 1, 1⊕0 = 1, 1⊕1 = 0, 0⊕1 = 1 → 1101 = ১৩। কোডে: for (s = g >> 1; s; s >>= 1) g ^= s; — বা ভাঁজে g ^= g >> 16; g ^= g >> 8; … ৫ ধাপ।

প্র ০৮“+” বা “−” অপারেটর ছাড়া দুটি পূর্ণসংখ্যা যোগ করুন। বিয়োগ কীভাবে করবেন, আর লুপ সর্বোচ্চ কতবার চলে? (কঠিন)

উত্তর: while (b) { c = (a & b) << 1; a ^= b; b = c; } return a; — unsigned-এ লিখুন, কারণ signed-এ ঋণাত্মক মানের বাঁ shift UB. যুক্তি (ওপরের stepper): a + b = (a ⊕ b) + 2(a ∧ b) — প্রতিটি ঘরের যোগ = বহন ছাড়া যোগফল + বহন। প্রতি ঘুরে যোগফল অপরিবর্তিত থাকে, আর বহনের সবচেয়ে নিচের 1 অন্তত এক ঘর বাঁয়ে সরে, তাই সর্বোচ্চ w ঘুরের পরে বহন বাইরে পড়ে যায় — লুপ শেষ হবেই, ৩২-বিটে সর্বোচ্চ ৩২ ঘুর (যেমন ১ + 0xFFFFFFFF).

বিয়োগ: a − b = a + (~b + 1) — two's complement-এ বিয়োগ মানে উল্টো সংখ্যা যোগ (পর্ব ০৫, ২০); “+1”-টিও একই ফাংশন দিয়ে: add(a, add(~b, 1)). গুণ চাইলে shift-add (পর্ব ২১)।

ফলো-আপ: “Hardware কি এভাবে যোগ করে?” — না, এটি ripple-এর মতো, সবচেয়ে খারাপ অবস্থায় w ধাপ। ALU বহন আগে থেকে হিসাব করে — carry-lookahead-এর generate g = ab আর propagate p = a ⊕ b ঠিক এই দুই অংশই (পর্ব ১১)।

প্র ০৯x-এর চেয়ে বড় বা সমান সবচেয়ে ছোট ২-এর ঘাত বের করুন। কোন edge case আছে, আর hardware-এর কোন instruction সাহায্য করে? (মাঝারি)

উত্তর: x--; x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; return x + 1; সবচেয়ে উঁচু 1-এর নিচের সব বিট 1 করে (১, ২, ৪, ৮, ১৬ ঘরের OR-এ পুরো ৩২ বিট ঢাকা পড়ে), ১ যোগ করলে পরের ঘাত। শুরুতে x-- যাতে x নিজে ঘাত হলে অক্ষত থাকে।

Edge case: x = ০ → x−1 = 0xFFFFFFFF → ফল ০ (wrap-around) — প্রোগ্রামে 0->0 দেখেছেন; x > 231 হলে উত্তর ৩২ বিটে ধরে না, ফল ০। এ দুটি আলাদা করে বলুন বা চুক্তিতে লিখুন “১ ≤ x ≤ 231”.

Hardware: leading zero count — x86-এর lzcnt (বা পুরোনো bsr), ARM-এর clz, RISC-V Zbb-এর clz; gcc-তে __builtin_clz (০-তে undefined!)। সূত্র: x > 1 হলে 1u << (32 - __builtin_clz(x - 1)). ব্যবহার: hash table-এর আকার, buddy allocator, ring buffer-এর আকার ২-এর ঘাতে তোলা (অংশ ৬)।

প্র ১০একটি ৩২-বিট register-এর বিট s থেকে w বিট চওড়া field বের করা আর বসানোর ফাংশন লিখুন। C-র bit-field struct দিয়ে register-এর নকশা করা কেন ঝুঁকিপূর্ণ? (মাঝারি)

উত্তর: বের করা: (x >> s) & ((1u << w) - 1). বসানো: m = ((1u << w) - 1) << s; x = (x & ~m) | ((v << s) & m); — আগে জায়গা মোছা, তারপর নতুন মান, আর & m দিয়ে v-এর বাড়তি বিট ছেঁটে ফেলা, যাতে পাশের field নষ্ট না হয়। শর্ত: ১ ≤ w ≤ ৩১ আর s + w ≤ ৩২; w = ৩২ হলে 1u << 32 UB (প্র ১৫-এর ফাঁদ)। ওপরের প্রোগ্রামে RGB565-এর সবুজ (s = ৫, w = ৬) দিয়ে দুটোই যাচাই করা।

Bit-field struct-এর ঝুঁকি: struct { unsigned en : 1; unsigned mode : 3; } r; দেখতে সুন্দর, কিন্তু (১) field-গুলো word-এর কোন প্রান্ত থেকে বসবে, ফাঁক থাকবে কি না, কোন মাপের access হবে — সব implementation-defined, ABI আর endianness-ভেদে বদলায়; (২) একটি field লিখতে compiler পুরো word বা শুধু একটি byte পড়ে-লিখতে পারে — যন্ত্রের register-এ byte-access নিষিদ্ধ হতে পারে, আর read-modify-write-এ “পড়লেই মোছে” ধরনের status বিট হারায়; (৩) int-ভিত্তিক bit-field signed কি না সেটিও implementation-defined. তাই portable driver-এ সাধারণত shift-mask macro, আর নির্মাতার header-এ bit-field থাকলে সেটি নির্দিষ্ট compiler আর ABI-র জন্য লেখা।

ফলো-আপ: “Hardware-এ field বের করার খরচ?” — Verilog-এ শুধু তার টেনে নেওয়া, কোনো gate নয় (পর্ব ০৬); software-এ shift আর AND — দুটি instruction; x86-এর BMI2-এর pext ছড়ানো বিটও এক instruction-এ জড়ো করে।

৪ · Endianness আর byte swap — মেশিন কোন দিক থেকে byte সাজায়

রিভিশন: পর্ব ০৬ · endianness আর portable কোড, পর্ব ৫১ · bus আর device

পর্ব ০৬-এ দেখেছেন: little-endian-এ মানের হালকা byte (LSB) বসে সবচেয়ে ছোট ঠিকানায় — x86-64, RISC-V আর বাস্তবে প্রায় সব ARM সিস্টেম; big-endian-এ ভারী byte আগে — নেটওয়ার্ক protocol-এর header (network byte order), অনেক ফাইল-ফরম্যাট। ইন্টারভিউর তিনটি চিরচেনা প্রশ্ন: মেশিনের endianness কীভাবে জানবেন, byte কীভাবে উল্টাবেন, আর কোড কীভাবে লিখবেন যাতে endianness নিয়ে ভাবতেই না হয়। তিনটিই নিচের প্রোগ্রামে:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h>

static uint32_t bswap_manual(uint32_t x) {
    return (x >> 24) | ((x >> 8) & 0x0000FF00u) | ((x << 8) & 0x00FF0000u) | (x << 24);
}
static uint32_t read_be32(const unsigned char *p) {   /* portable on any machine */
    return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}

int main(void) {
    uint32_t v = 0x12345678u;
    unsigned char b[4];
    memcpy(b, &v, sizeof v);                        /* look at the bytes, no aliasing tricks */
    printf("bytes at increasing addresses: %02X %02X %02X %02X -> %s-endian\n",
           b[0], b[1], b[2], b[3], b[0] == 0x78 ? "little" : "big");
    printf("compile-time check: little = %d\n", __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__);
    printf("bswap manual 0x%08X, builtin 0x%08X\n", bswap_manual(v), __builtin_bswap32(v));
    printf("htonl(0x12345678) = 0x%08X, htons(0x1F90) = 0x%04X\n", htonl(v), htons(0x1F90));
    const unsigned char wire[4] = {0x12, 0x34, 0x56, 0x78};   /* network byte order */
    printf("read_be32(wire) = 0x%08X\n", read_be32(wire));
    return 0;
}

আসল রানের আউটপুট:

bytes at increasing addresses: 78 56 34 12 -> little-endian
compile-time check: little = 1
bswap manual 0x78563412, builtin 0x78563412
htonl(0x12345678) = 0x78563412, htons(0x1F90) = 0x901F
read_be32(wire) = 0x12345678

প্রথম লাইন: 0x12345678-এর byte বাড়তি ঠিকানায় 78 56 34 12 — সবচেয়ে হালকা byte আগে, তাই little-endian. byte দেখতে memcpy দিয়ে unsigned char array-তে কপি করা হয়েছে — unsigned char * দিয়ে পড়াও মানে বৈধ, তবে অন্য type-এর pointer cast করে পড়া strict aliasing ভাঙে। দ্বিতীয় লাইন: gcc আর clang-এর __BYTE_ORDER__ macro দিয়ে compile-এর সময়েই জানা যায় — runtime পরীক্ষার দরকারই নেই।

bswap: হাতে লেখা shift-mask আর __builtin_bswap32 দুটোই 0x78563412; compiler হাতে লেখা রূপটিও চিনে এক bswap (x86) বা rev (ARM) বানায়। htonl/htons: little-endian মেশিনে byte উল্টায় (8080 = 0x1F90 → 0x901F), big-endian মেশিনে কিছুই করে না। শেষ লাইন: read_be32 byte ধরে shift করে মান বানায়, তাই যেকোনো মেশিনে একই ফল 0x12345678 — portable কোডের নিয়ম এটাই।

প্র ১১C-তে মেশিন little- না big-endian, কীভাবে বের করবেন? আর একটি little-endian মেশিনে uint32_t 0xAABBCCDD-এর সবচেয়ে ছোট ঠিকানার byte কোনটি? (সহজ)

উত্তর: uint32_t v = 1; নিয়ে এর প্রথম byte দেখুন — unsigned char pointer, memcpy বা union দিয়ে। ১ হলে little-endian, ০ হলে big-endian. Compile-এর সময় জানতে __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ (gcc/clang), আর C23-এ <stdbit.h>-এর __STDC_ENDIAN_NATIVE__. 0xAABBCCDD-এর সবচেয়ে ছোট ঠিকানায় থাকে 0xDD (তারপর CC, BB, AA).

ফলো-আপ: (ক) “union দিয়ে পড়া কি বৈধ?” — C-তে union-এর এক member লিখে অন্যটি পড়া (type punning) অনুমোদিত, C++-এ নয়; সবচেয়ে নিরাপদ memcpy, যা compiler পুরোপুরি মুছে দিয়ে একটি load বানায়। (খ) “Bi-endian কী?” — কিছু ISA (ARM, MIPS, Power, RISC-V-ও মানে অনুমতি দেয়) দুই ক্রমেই চলতে পারে; বাস্তবে Linux-এর প্রায় সব ARM আর RISC-V সিস্টেম little-endian. (গ) Endianness byte-এর ক্রম — একটি byte-এর ভেতরের বিটের ক্রম নয়; C-র shift সবসময় মানের ওপর কাজ করে, মেমরির ক্রমের ওপর নয়।

প্র ১২একটি sensor big-endian ক্রমে ৬-byte packet পাঠায়: ২ byte তাপমাত্রা (signed, 0.01°C একক), ৪ byte timestamp. Little-endian MCU-তে কীভাবে পড়বেন? struct cast করা কেন বিপজ্জনক? (কঠিন)

উত্তর: byte ধরে মান বানান: uint16_t raw = (uint16_t)((p[0] << 8) | p[1]); int16_t t = (int16_t)raw; আর uint32_t ts = read_be32(p + 2);. তাপমাত্রা = t / 100.0, বা fixed-point-এ t রেখেই হিসাব। (int16_t)raw রূপান্তরটি 32767-এর বেশি মানে implementation-defined ছিল; C23-এ two's complement বাধ্যতামূলক হলেও out-of-range রূপান্তর এখনো implementation-defined — gcc modulo নিয়ম মানে। পুরো portable চাইলে: raw < 32768 ? raw : (int)raw - 65536.

Struct cast কেন নয়: (১) byte-ক্রম ভুল — little-endian-এ সব field উল্টো পড়বে; (২) padding — struct {int16_t t; uint32_t ts;}-এ t-র পরে ২ byte ফাঁকা, মোট ৮, অথচ packet ৬ byte (অংশ ৬); (৩) alignment — buffer-এর ঠিকানা থেকে ts misaligned হতে পারে, কিছু ISA-তে exception বা খুব ধীর (পর্ব ০৬); (৪) strict aliasing ভাঙে। __attribute__((packed)) দিলে (২) মেটে কিন্তু (১) মেটে না।

ফলো-আপ: “লেখার সময়?” — উল্টো: p[0] = v >> 8; p[1] = v & 0xFF;. Linux kernel আর অনেক library-তে এর জন্য cpu_to_be32/be32_to_cpu-ধরনের ফাংশন থাকে, যা টাইপ দিয়ে ভুল ধরতেও সাহায্য করে।

৫ · Integer-এর ফাঁদ — overflow, UB, promotion, shift আর char

রিভিশন: পর্ব ০৫ · overflow আর wrap-around, পর্ব ২০ · status flag, পর্ব ৩১ · UB আর optimizer

Hardware-এর কাছে signed আর unsigned যোগ একই — একই adder, একই বিট (পর্ব ২০); পার্থক্য শুধু flag-এ: unsigned-এর overflow ধরা পড়ে carry flag-এ, signed-এর overflow flag-এ। কিন্তু C ভাষা দুটিকে খুব আলাদা চোখে দেখে, আর ইন্টারভিউর “এটা কী ছাপবে?”-ধরনের প্রশ্নের বেশিরভাগ ফাঁদ এখানেই। নিচের টেবিলে নিয়মগুলো এক জায়গায়:

ঘটনাC-র চোখেx86-64 gcc-তে সাধারণত
unsigned-এর overflow (UINT_MAX + 1)well-defined: modulo 2N০
signed-এর overflow (INT_MAX + 1)UBhardware-এ wrap, কিন্তু optimizer “ঘটে না” ধরে নিতে পারে
INT_MIN / -1, INT_MIN % -1UBx86-এর idiv exception দেয় (SIGFPE)
shift-এর পরিমাণ ঋণাত্মক বা ≥ widthUBx86 shift-এর পরিমাণের নিচের ৫ বিট নেয় — runtime-এ প্রায়ই 1 << 32 = ১
ঋণাত্মক signed-কে বাঁয়ে shiftUBবিট-স্তরে সাধারণ shift
ঋণাত্মক signed-কে ডানে shiftimplementation-definedarithmetic shift (sar)
সীমার বাইরের মান signed type-এ রূপান্তরimplementation-definedmodulo 2N (নিচের বিটগুলো থাকে)
plain char signed কি নাimplementation-definedsigned (ARM আর RISC-V Linux-এ সাধারণত unsigned)
ছোট type-এর হিসাব (uint8_t, short)আগে int-এ উন্নীত (integer promotion)৩২-বিট হিসাব
signed বনাম unsigned তুলনাusual arithmetic conversions: signed → unsigned-1 < 1u মিথ্যা

এবার এগুলো চোখে দেখুন — কিন্তু UB না ঘটিয়ে। Overflow-এর জন্য gcc-র __builtin_add_overflow (যা hardware-এর overflow flag-এর মতো একটি বিট দেয়), shift-এর জন্য & 31, আর signed-unsigned তুলনা স্পষ্ট cast দিয়ে:

#include <stdio.h>
#include <stdint.h>
#include <limits.h>

int main(void) {
    unsigned u = 0; u -= 1;                              /* unsigned wrap: well defined */
    printf("0u - 1 = %u (UINT_MAX = %u)\n", u, UINT_MAX);
    int r;
    int ovf = __builtin_add_overflow(INT_MAX, 1, &r);    /* checked add, no UB */
    printf("INT_MAX + 1: overflow flag = %d, wrapped value = %d\n", ovf, r);
    unsigned char a = 200, b = 100;
    printf("uchar 200 + 100 = %d (int after promotion), stored in uchar = %u\n",
           a + b, (unsigned)(unsigned char)(a + b));
    uint8_t x = 0x0F;
    printf("~x = %d, (uint8_t)~x = %u\n", ~x, (unsigned)(uint8_t)~x);
    int neg = -1; unsigned one = 1;
    printf("(unsigned)-1 = %u, so -1 < 1u is %d\n", (unsigned)neg, (unsigned)neg < one);
    printf("CHAR_MIN = %d -> plain char is %s here\n", CHAR_MIN, CHAR_MIN < 0 ? "signed" : "unsigned");
    char c = (char)0xE0;
    printf("(char)0xE0 as int = %d\n", c);
    unsigned sh = 32;
    printf("1u << (32 & 31) = %u   (1u << 32 itself is UB)\n", 1u << (sh & 31));
    printf("sizeof('a') = %zu in C\n", sizeof('a'));
    return 0;
}

আসল রানের আউটপুট:

0u - 1 = 4294967295 (UINT_MAX = 4294967295)
INT_MAX + 1: overflow flag = 1, wrapped value = -2147483648
uchar 200 + 100 = 300 (int after promotion), stored in uchar = 44
~x = -16, (uint8_t)~x = 240
(unsigned)-1 = 4294967295, so -1 < 1u is 0
CHAR_MIN = -128 -> plain char is signed here
(char)0xE0 as int = -32
1u << (32 & 31) = 1   (1u << 32 itself is UB)
sizeof('a') = 4 in C

লাইন ১–২: unsigned-এ ০ − ১ = 4294967295 — মান-নির্দিষ্ট wrap-around. signed-এ INT_MAX + 1 সরাসরি লিখলে UB হতো; built-in ফাংশনটি overflow flag = ১ আর wrap হওয়া মান −2147483648 ফেরত দিল, কোনো UB ছাড়াই। লাইন ৩: দুটি unsigned char-এর যোগ ৩০০ — কারণ যোগের আগে দুটোই int-এ উন্নীত হয়; কেবল আবার unsigned char-এ রাখলে ৩০০ − ২৫৬ = ৪৪। লাইন ৪: ~x-ও int-এ হয়: 0x0000000F-এর উল্টো 0xFFFFFFF0 = −১৬, আর তাই ~x == 0xF0 তুলনা মিথ্যা; cast করে নিচের ৮ বিট নিলে ২৪০। এটি embedded কোডে mask-এর খুব পরিচিত বাগ — gcc -Wextra-এ এমন তুলনায় “promoted bitwise complement” নামে warning দেয়, তাই warning চালু রেখে compile করার অভ্যাস করুন।

লাইন ৫: −১-কে unsigned-এ নিলে 4294967295, তাই “−১ < ১u” মিথ্যা (০) — সরাসরি -1 < 1u লিখলেও compiler ঠিক এটাই করে, আর -Wall -Wextra-এ “different signedness” warning দেয় (তাই প্রোগ্রামে cast স্পষ্ট করে লেখা)। লাইন ৬–৭: এই পরিবেশে CHAR_MIN = −১২৮, অর্থাৎ plain char signed; তাই (char)0xE0 = −৩২ (implementation-defined রূপান্তর, gcc-তে modulo). একই কোড ARM বা RISC-V Linux-এ ২২৪ ছাপবে। লাইন ৮–৯: & 31 করায় shift ০ ঘর, ফল ১; আর C-তে 'a'-র type int, তাই sizeof ৪ (C++-এ ১)।

প্র ১৩Signed overflow কেন UB আর unsigned-এর wrap কেন well-defined? Optimizer এর সুযোগ কীভাবে নেয়, আর overflow কীভাবে নিরাপদে ধরবেন? (মাঝারি)

উত্তর: ঐতিহাসিক কারণ: C যখন মানক হয়, তখন sign-magnitude আর ones' complement মেশিনও ছিল (পর্ব ০৫), আর কিছু মেশিন overflow-এ trap দিত — তাই ভাষা কোনো ফল বেঁধে দেয়নি। Unsigned-এর জন্য modulo 2N সব মেশিনে স্বাভাবিক, তাই সেটি নির্দিষ্ট। আজ optimizer এই স্বাধীনতা কাজে লাগায়: x + 1 > x-কে সবসময় সত্য ধরে, for (int i = 0; i <= n; i++)-এ i কখনো wrap করবে না ধরে লুপের ঘুর-সংখ্যা হিসাব করে, ৬৪-বিট pointer হিসাবে sign extension বাদ দেয় — পর্ব ৩১-এ gcc -O2-এর grows ফাংশন সরাসরি mov eax, 1 হয়ে গিয়েছিল।

নিরাপদে ধরা: (১) আগে পরীক্ষা: if (b > 0 && a > INT_MAX - b) /* overflow */. (২) gcc/clang-এর __builtin_add_overflow, __builtin_mul_overflow — x86-এ এক add আর jo/seto; C23-এ মানক রূপ <stdckdint.h>-এর ckd_add. (৩) পরীক্ষার সময় -fsanitize=undefined (UBSan). (৪) ইচ্ছাকৃত wrap চাইলে unsigned, বা পুরো প্রোগ্রামে -fwrapv (ফলে কিছু optimization হারাবেন)।

ফলো-আপ: “Hardware কীভাবে জানে?” — একই adder; signed overflow = দুই operand-এর চিহ্ন এক কিন্তু ফলের চিহ্ন আলাদা, অর্থাৎ carry-in ⊕ carry-out of MSB (পর্ব ২০)। RISC-V-তে কোনো flag register নেই, তাই overflow ধরতে compiler বাড়তি তুলনা-instruction বসায় (পর্ব ২৬)।

প্র ১৪uint16_t a = 65535; uint32_t p = a * a; — p কত? সব ঠিক আছে তো? (কঠিন)

উত্তর: এটি একটি লুকানো UB. uint16_t দুটোই আগে int-এ (৩২-বিট signed) উন্নীত হয়, কারণ int সব uint16_t মান ধরতে পারে। তারপর গুণ হয় signed int-এ: ৬৫৫৩৫ × ৬৫৫৩৫ = ৪,২৯৪,৮৩৬,২২৫, যা INT_MAX (২,১৪৭,৪৮৩,৬৪৭)-এর বেশি — signed overflow, অর্থাৎ UB. বাস্তবে x86-এ প্রায়ই সঠিক বিট (4294836225) আসে, তাই বাগটি পরীক্ষায় ধরা পড়ে না — কিন্তু optimizer চাইলে অন্য ফলও দিতে পারে, আর UBSan এটিকে ধরে।

ঠিক রূপ: uint32_t p = (uint32_t)a * a; — একটি operand আগে unsigned ৩২-বিটে নিলে পুরো গুণ unsigned-এ, well-defined. একই কারণে uint8_t দুটির গুণ কখনো overflow করে না (সর্বোচ্চ ৬৫০২৫, int-এ ধরে), কিন্তু uint16_t-এর করে।

ফলো-আপ: “১৬-বিট int-এর MCU হলে?” — সেখানে uint16_t উন্নীত হয় unsigned int-এ (কারণ int সব মান ধরতে পারে না), ফলে গুণ unsigned আর ফল ১৬ বিটে wrap — একই কোড দুই প্ল্যাটফর্মে দুই রকম আচরণ। তাই embedded কোডে ছোট type-এর হিসাবে সবসময় স্পষ্ট cast লিখুন।

প্র ১৫1 << 32 আর 1u << 31 — কোনটি বৈধ? x86, ARM আর RISC-V-তে runtime-এ shift-এর পরিমাণ ৩২ হলে কী ঘটে? (কঠিন)

উত্তর: 1u << 31 বৈধ, ফল 0x80000000. 1 << 31 (signed) ফলটি int-এ ধরে না — C-তে UB (C++20 থেকে C++-এ সংজ্ঞায়িত)। 1 << 32 বা 1u << 32 — shift-এর পরিমাণ width-এর সমান বা বেশি — দুটোই UB; constant হলে gcc warning দেয়।

Hardware কেন একমত নয়: x86-এর ৩২-বিট shift পরিমাণের নিচের ৫ বিট নেয় (৬৪-বিটে ৬ বিট), তাই runtime-এ 1 << 32 প্রায়ই ১। RISC-V RV32-এর sll-ও নিচের ৫ বিট নেয় → ১। AArch64-এর register shift পরিমাণকে width দিয়ে modulo করে → ১। কিন্তু ৩২-বিট ARM-এ register-নির্দিষ্ট shift-এ নিচের পুরো byte দেখা হয়, তাই ৩২ ঘর shift করলে ০। এক ISA-তে ১, আরেকটিতে ০ — ভাষা তাই কোনোটিই বেঁধে দেয়নি। আর constant হলে compiler নিজে হিসাব করে যেকোনো ফল বসাতে পারে: Wandbox-এর gcc-তে printf("%d", 1 << 32) “shift count >= width” warning দিয়ে ০ ছাপল — অথচ একই shift runtime-এ x86-এ করলে ১ আসত।

ফলো-আপ: “নিরাপদ ‘w-বিট mask’?” — w == 32 ? 0xFFFFFFFFu : (1u << w) - 1, বা ৬৪-বিটে হিসাব করে কেটে নেওয়া। Rotate-এ & 31 কেন দিয়েছিলাম, এখন পরিষ্কার।

প্র ১৬char c; while ((c = getchar()) != EOF) … — এই লুপে কোন বাগ আছে, আর ISA-ভেদে তা কীভাবে আলাদা রূপ নেয়? (মাঝারি)

উত্তর: getchar ফেরত দেয় int — ০ থেকে ২৫৫ পর্যন্ত byte, বা EOF (সাধারণত −১)। char-এ রাখলে ২৫৭টি সম্ভাব্য মান ২৫৬টি ঘরে ঢোকানো হলো। char signed হলে (x86-64): byte 0xFF পড়লে c = −১, যা EOF-এর সমান — লুপ ফাইলের মাঝেই থেমে যায় (যেমন বাইনারি ফাইল বা কিছু encoding-এ)। char unsigned হলে (ARM আর RISC-V Linux): EOF (−১) c-তে রাখলে হয় ২৫৫, যা কখনো −১-এর সমান নয় — লুপ চিরকাল চলে। সমাধান: int c;.

ফলো-আপ: (ক) char, signed char আর unsigned char — তিনটি আলাদা type; বিট-কাজ আর byte-buffer-এর জন্য সবসময় unsigned char বা uint8_t. (খ) gcc-তে -fsigned-char/-funsigned-char দিয়ে আচরণ বদলানো যায় — পুরোনো কোড এক প্ল্যাটফর্ম থেকে আরেকটিতে নেওয়ার সময় কাজে লাগে। (গ) isdigit(c)-এর মতো <ctype.h> ফাংশনে ঋণাত্মক char দেওয়া UB — আগে (unsigned char) cast.

৬ · মেমরি — struct padding, alignment, stack বনাম heap, memory pool আর ring buffer

রিভিশন: পর্ব ০৬ · alignment আর padding, পর্ব ৩০ · stack frame, পর্ব ৪৫ · cache line, পর্ব ৫০ · page

পর্ব ০৬-এর তিন নিয়ম (x86-64 System V ABI): প্রতিটি member বসে নিজের alignment-এর গুণিতক offset-এ; struct-এর alignment = সবচেয়ে বড় member-alignment; আর sizeof সেই alignment-এর গুণিতক পর্যন্ত বাড়ে (tail padding), যাতে array-র প্রতিটি উপাদান aligned থাকে। ইন্টারভিউতে প্রায়ই একটি struct দিয়ে বলা হয় “sizeof কত?” — নিচের stepper-এ একটি নেটওয়ার্ক-header-এর মতো struct ধাপে ধাপে সাজানো হলো।

typeoffset ০, ১ byte
offset : 0  1  2  3  4  5  6  7  8  9 10 11
         T  .  .  .  .  .  .  .  .  .  .  .
T = type (uint8_t, align 1)
len৪-এর গুণিতকে যেতে হবে
offset : 0  1  2  3  4  5  6  7  8  9 10 11
         T  p  p  p  L  L  L  L  .  .  .  .
p = padding (3 bytes), L = len (uint32_t, align 4)
port আর flag৮ আর ১০-এ
offset : 0  1  2  3  4  5  6  7  8  9 10 11
         T  p  p  p  L  L  L  L  P  P  F  .
P = port (uint16_t, align 2) at 8, F = flag at 10
tail paddingsizeof = ১২
offset : 0  1  2  3  4  5  6  7  8  9 10 11
         T  p  p  p  L  L  L  L  P  P  F  p
struct align = 4 -> size rounded up to 12
data 8 bytes, padding 4 bytes
নতুন ক্রমবড় থেকে ছোট: ৮ byte
struct { uint32_t len; uint16_t port; uint8_t type; uint8_t flag; }
offset : 0  1  2  3  4  5  6  7
         L  L  L  L  P  P  T  F      size 8, no padding
packed (attribute): size 8 too, but len at offset 1 (misaligned)
  1. প্রথম member. type এক byte, alignment ১ — offset ০-তে বসল। এখন পরের খালি জায়গা offset ১।
  2. len-এর জন্য ফাঁক. uint32_t-এর alignment ৪, তাই offset ১ চলবে না; ১, ২, ৩ — তিনটি padding byte, len বসে ৪–৭-এ।
  3. port আর flag. offset ৮ দুইয়ের গুণিতক, তাই port সরাসরি ৮–৯-এ; flag এক byte, ১০-এ। এখানে কোনো নতুন ফাঁক লাগেনি।
  4. Tail padding. ডেটা শেষ হলো offset ১১-তে (মোট ১১ byte জায়গা), কিন্তু struct-এর alignment ৪ (সবচেয়ে বড় member len-এর)। ১১-কে ৪-এর পরের গুণিতকে তুললে ১২ — শেষে একটি padding byte. মোট ৮ byte ডেটার জন্য ১২ byte.
  5. পুনর্বিন্যাস. Member-দের alignment-এর বড় থেকে ছোট ক্রমে সাজালে কোনো ফাঁক লাগে না — ৮ byte. packed দিয়েও ৮, কিন্তু তখন len offset ১-এ misaligned: x86-এ চলে, কিছু ISA-তে ধীর বা exception, আর &s.len pointer অন্য ফাংশনে দেওয়া বিপজ্জনক।

এবার আটটি “sizeof কত?” ধাঁধা একসাথে compiler-কে জিজ্ঞেস করুন — উত্তর দেখার আগে নিজে খাতায় লিখে নিন:

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <stdalign.h>

struct P1 { char a; int b; char c; };
struct P2 { int b; char a; char c; };
struct P3 { char a; double d; };
struct P4 { char a; char b[3]; short s; };
struct P5 { uint8_t type; uint32_t len; uint16_t port; uint8_t flag; };
struct __attribute__((packed)) P6 { uint8_t type; uint32_t len; uint16_t port; uint8_t flag; };
struct P7 { char c; struct P2 in; };
union  U  { char c[5]; int i; };

#define SHOW(T) printf("%-10s size %2zu  align %zu\n", #T, sizeof(T), alignof(T))

int main(void) {
    SHOW(struct P1); SHOW(struct P2); SHOW(struct P3); SHOW(struct P4);
    SHOW(struct P5); SHOW(struct P6); SHOW(struct P7); SHOW(union U);
    printf("P5 offsets: type@%zu len@%zu port@%zu flag@%zu\n", offsetof(struct P5, type),
           offsetof(struct P5, len), offsetof(struct P5, port), offsetof(struct P5, flag));
    printf("P6 offsets: type@%zu len@%zu port@%zu flag@%zu\n", offsetof(struct P6, type),
           offsetof(struct P6, len), offsetof(struct P6, port), offsetof(struct P6, flag));
    struct P1 arr[3];
    printf("struct P1 arr[3]: %zu bytes, &arr[1] - &arr[0] = %zu bytes\n",
           sizeof arr, (size_t)((char *)&arr[1] - (char *)&arr[0]));
    return 0;
}

Wandbox-এর gcc (Linux x86-64, System V ABI) আউটপুট:

struct P1  size 12  align 4
struct P2  size  8  align 4
struct P3  size 16  align 8
struct P4  size  6  align 2
struct P5  size 12  align 4
struct P6  size  8  align 1
struct P7  size 12  align 4
union U    size  8  align 4
P5 offsets: type@0 len@4 port@8 flag@10
P6 offsets: type@0 len@1 port@5 flag@7
struct P1 arr[3]: 36 bytes, &arr[1] - &arr[0] = 12 bytes

P1 বনাম P2: একই তিনটি member — char, int, char ক্রমে ১২ (প্রথম char-এর পরে ৩, শেষে ৩ padding), কিন্তু int আগে দিলে ৮। P3: double-এর alignment ৮, তাই char-এর পরে ৭ byte ফাঁক, মোট ১৬। P4: char-এর array-র alignment ১, তাই a আর b[3] মিলে ৪ byte, short বসে ৪-এ — মোট ৬, alignment ২। P5 ঠিক ওপরের stepper: ১২, offset ০/৪/৮/১০। P6 (packed): ৮ byte, alignment ১, len offset ১-এ।

P7: ভেতরের struct (P2, alignment ৪) offset ৪-এ বসে, তাই ১ + ৩ + ৮ = ১২ — nested struct তার নিজের alignment নিয়েই আসে। union U: মাপ = সবচেয়ে বড় member (৫) কে alignment ৪-এর গুণিতকে তোলা = ৮। শেষ লাইন: struct P1 arr[3] = ৩৬ byte, আর উপাদানগুলোর দূরত্ব ঠিক sizeof — tail padding কেন লাগে তার প্রমাণ। এই সংখ্যাগুলো x86-64 Linux-এর; ৩২-বিট x86 Linux-এ double-এর struct-এর ভেতরের alignment ৪, তাই P3 সেখানে ১২ হতো।

প্র ১৭Stack আর heap-এর পার্থক্য বলুন — কোথায় থাকে, কে ব্যবস্থাপনা করে, গতি, আকারের সীমা আর সাধারণ বাগ। Embedded-এ heap এড়ানো হয় কেন? (মাঝারি)

উত্তর: Stack: প্রতিটি ফাংশন-ডাকে একটি stack frame — local ভ্যারিয়েবল, ফেরার ঠিকানা, save করা register (পর্ব ৩০)। বরাদ্দ মানে শুধু stack pointer সরানো (RISC-V-এ addi sp, sp, -32) — এক instruction, আর মুক্তি ফাংশন ফেরার সময় আপনা থেকেই। LIFO, তাই খুব দ্রুত আর cache-বান্ধব (উপরের অংশ বারবার ব্যবহার হয়)। সীমা ছোট: অনেক Linux সিস্টেমে মূল thread-এর জন্য সাধারণত ৮ MiB, অন্য thread-এ আরও কম, MCU-তে হয়তো কয়েকশো byte থেকে কয়েক KB. বাগ: অনেক গভীর recursion বা বড় local array → stack overflow; ফাংশন শেষে local-এর ঠিকানা ফেরত দেওয়া (dangling pointer).

Heap: malloc/free-এর এলাকা — যেকোনো ক্রমে বরাদ্দ ও মুক্তি, আকার runtime-এ ঠিক হয়, জীবনকাল প্রোগ্রামার ঠিক করেন। খরচ বেশি: allocator-কে free list খুঁজতে হয়, metadata রাখতে হয়, multi-thread-এ lock; glibc-তে x86-64-এ প্রতিটি বরাদ্দ ১৬ byte-এ aligned. বাগ: memory leak, double free, use-after-free, fragmentation.

Embedded-এ: ছোট RAM-এ fragmentation-এর কারণে সপ্তাহ চলার পর হঠাৎ malloc ব্যর্থ হতে পারে; বরাদ্দের সময় অনিশ্চিত — real-time-এ WCET প্রমাণ কঠিন (পর্ব ৫৬); আর ব্যর্থ হলে কী করবেন, তারও ভালো উত্তর থাকে না। তাই অনেক নিরাপত্তা-সংবেদনশীল কোডিং নির্দেশিকা চালু হওয়ার পরে dynamic allocation নিষেধ করে; বদলে static buffer আর fixed-size memory pool (নিচের প্রোগ্রাম)।

প্র ১৮একটি pointer ১৬-byte aligned কি না কীভাবে দেখবেন? ঠিকানা 0x1003-কে ৮-এর পরের গুণিতকে তুলুন। (সহজ)

উত্তর: ((uintptr_t)p & 15) == 0 (বা % 16) — alignment ২-এর ঘাত হলে নিচের log2(a) বিট শূন্য কি না দেখলেই হয়। ওপরে তোলা (align up): (x + a - 1) & ~(a - 1). 0x1003 আর a = ৮: 0x1003 + 7 = 0x100A, নিচের ৩ বিট মুছলে 0x1008. নিচে নামানো (align down): x & ~(a - 1) → 0x1000.

ফলো-আপ: (ক) uintptr_t কেন? pointer-কে integer-এ নিয়ে বিট-কাজের জন্য এটিই সঠিক type; int-এ নিলে ৬৪-বিট ঠিকানা কেটে যায়। (খ) aligned মেমরি চাইতে C11-এর aligned_alloc(16, n) (n আগে alignment-এর গুণিতক হওয়া ভালো), বা POSIX posix_memalign; struct-এ alignas(64) — যেমন false sharing এড়াতে cache line ধরে আলাদা রাখা (পর্ব ৪৮)। (গ) কেন দরকার? SIMD load (পর্ব ৪২), DMA buffer (অনেক controller নির্দিষ্ট alignment চায়, পর্ব ৫২), আর page-aligned বরাদ্দ (৪ KiB, পর্ব ৫০)।

এবার এমবেডেড আর systems ইন্টারভিউর দুটি প্রিয় “ছোট ডেটা-স্ট্রাকচার লিখুন” প্রশ্ন। প্রথমটি ring buffer (circular buffer): একটি নির্দিষ্ট মাপের array, যেখানে producer (যেমন UART-এর interrupt) লেখে আর consumer (মূল লুপ) পড়ে। এখানে index দুটি কখনো কাটা হয় না — শুধু বাড়ে, আর array-তে বসানোর সময় & (SIZE - 1); গণনা = head - tail, যা unsigned wrap-এর কারণে 232 পার হলেও ঠিক থাকে। প্রমাণ করতে index শুরু করা হয়েছে 232-এর ঠিক তিন ঘর আগে থেকে।

#include <stdio.h>
#include <stdint.h>

#define RB_SIZE 8u                                /* must be a power of two */
typedef struct {
    uint8_t buf[RB_SIZE];
    volatile uint32_t head;                       /* written only by the producer (e.g. ISR) */
    volatile uint32_t tail;                       /* written only by the consumer (main loop) */
} ring;
static int rb_put(ring *r, uint8_t v) {
    if (r->head - r->tail == RB_SIZE) return 0;   /* full: unsigned difference survives wrap */
    r->buf[r->head & (RB_SIZE - 1)] = v; r->head++; return 1;
}
static int rb_get(ring *r, uint8_t *v) {
    if (r->head == r->tail) return 0;             /* empty */
    *v = r->buf[r->tail & (RB_SIZE - 1)]; r->tail++; return 1;
}
int main(void) {
    ring r = { .head = 0xFFFFFFFDu, .tail = 0xFFFFFFFDu };   /* start just before 2^32 */
    int ok = 0; uint8_t c;
    for (int i = 0; i < 10; i++) ok += rb_put(&r, (uint8_t)('A' + i));
    printf("put A..J: accepted %d, count = %u\nget 3: ", ok, (unsigned)(r.head - r.tail));
    for (int i = 0; i < 3; i++) if (rb_get(&r, &c)) putchar(c);
    ok = rb_put(&r, 'X') + rb_put(&r, 'Y');
    printf("\nput X,Y: accepted %d\ndrain: ", ok);
    while (rb_get(&r, &c)) putchar(c);
    printf("\nhead = %u, tail = %u (both wrapped past 2^32)\n", (unsigned)r.head, (unsigned)r.tail);
    return 0;
}

আসল রানের আউটপুট:

put A..J: accepted 8, count = 8
get 3: ABC
put X,Y: accepted 2
drain: DEFGHXY
head = 7, tail = 7 (both wrapped past 2^32)

দশটি byte দিতে চাইলে buffer নিল আটটি (A–H), বাকি দুটি “full”-এ ফেরত — কোনো লেখা পুরোনো ডেটার ওপর পড়েনি। তিনটি পড়ার পর (ABC) জায়গা হলো, X আর Y ঢুকল; শেষে বাকি সব ক্রম মেনে বেরোল: DEFGHXY. head আর tail দুটোই 232 পার হয়ে ৭-এ — শুরু 0xFFFFFFFD থেকে ১০ বার বাড়লে (৮ + ২টি সফল লেখা) 232 + ৭। তবু প্রতিটি মুহূর্তে head - tail সঠিক গণনা দিয়েছে। এই কৌশলের শর্ত একটিই: SIZE ২-এর ঘাত, যাতে 232-কে নিখুঁতভাবে ভাগ করে।

দ্বিতীয়টি fixed-size memory pool: সমান মাপের কয়েকটি block, আর খালি block-গুলো একটি linked list-এ বাঁধা — link-টি রাখা হয় খালি block-এর ভেতরেই, তাই বাড়তি মেমরি লাগে না। বরাদ্দ = list-এর মাথা খুলে নেওয়া, মুক্তি = মাথায় বসানো — দুটোই O(1), fragmentation নেই, সময় নিশ্চিত।

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <stdalign.h>

#define BLOCK 32
#define NBLK  4
typedef union blk {                         /* a free block stores the link inside itself */
    union blk *next;
    alignas(max_align_t) unsigned char data[BLOCK];
} blk;
static blk pool[NBLK];
static blk *free_list;
static void pool_init(void) {
    for (int i = 0; i < NBLK - 1; i++) pool[i].next = &pool[i + 1];
    pool[NBLK - 1].next = NULL; free_list = &pool[0];
}
static void *pool_alloc(void) { blk *b = free_list; if (b) free_list = b->next; return b; }
static void pool_free(void *p) { blk *b = p; b->next = free_list; free_list = b; }
static int idx(const void *p) { return p ? (int)((const blk *)p - pool) : -1; }
int main(void) {
    pool_init();
    void *a = pool_alloc(), *b = pool_alloc(), *c = pool_alloc(), *d = pool_alloc(), *e = pool_alloc();
    printf("alloc x5 -> blocks %d %d %d %d %d (last one: pool empty)\n", idx(a), idx(b), idx(c), idx(d), idx(e));
    pool_free(b);
    printf("free(1) then alloc -> block %d (LIFO free list, O(1))\n", idx(pool_alloc()));
    printf("sizeof(blk) = %zu, alignof(blk) = %zu, block 2 aligned: %d\n", sizeof(blk), alignof(blk),
           (uintptr_t)c % alignof(max_align_t) == 0);
    return 0;
}

আসল রানের আউটপুট:

alloc x5 -> blocks 0 1 2 3 -1 (last one: pool empty)
free(1) then alloc -> block 1 (LIFO free list, O(1))
sizeof(blk) = 32, alignof(blk) = 16, block 2 aligned: 1

চারটি block ক্রমে ০–৩ পাওয়া গেল, পঞ্চম অনুরোধে NULL (−১) — pool খালি, আর এটি সামলানো কলারের দায়িত্ব। block ১ মুক্ত করে আবার চাইলে সেই block ১-ই ফেরত এল: free list LIFO, আর সদ্য ব্যবহৃত block cache-এ থাকার সম্ভাবনা বেশি — একটি ছোট সুবিধা। union-এ alignas(max_align_t) দেওয়ায় প্রতিটি block ১৬ byte-এ aligned (x86-64-এ max_align_t-এর alignment ১৬), তাই যেকোনো type রাখা নিরাপদ; ঠিকানা পরীক্ষাও ১ দিল। ঠিকানা নিজে ছাপা হয়নি, কারণ ASLR-এর জন্য প্রতি রানে বদলায় — কিন্তু alignment-এর ফল বদলায় না।

প্র ১৯Ring buffer-এ “full” আর “empty” আলাদা করবেন কীভাবে? একটি interrupt লেখে আর মূল লুপ পড়ে — lock লাগবে কি? Multicore-এ কী বাড়তি লাগে? (কঠিন)

উত্তর: তিনটি প্রচলিত উপায়: (১) অবিরাম বাড়তে থাকা index (ওপরের প্রোগ্রাম): empty = head == tail, full = head - tail == SIZE; সব ঘর ব্যবহার হয়, শর্ত SIZE ২-এর ঘাত। (২) একটি ঘর খালি রাখা: index সবসময় % SIZE-এ; empty = head == tail, full = (head + 1) % SIZE == tail — SIZE যেকোনো, কিন্তু একটি ঘর নষ্ট। (৩) আলাদা count ভ্যারিয়েবল — কিন্তু তখন দুই পক্ষই একই ভ্যারিয়েবল লেখে, ফলে race.

Lock: single-producer single-consumer (SPSC) হলে সাধারণত না — head লেখে শুধু producer, tail শুধু consumer, আর প্রতিটি index একটি aligned word, যা একবারে পড়া-লেখা হয়। শর্ত: ক্রম ঠিক রাখা — producer আগে ডেটা লিখবে, তারপর head বাড়াবে; consumer আগে head পড়বে, তারপর ডেটা। এক core-এর MCU-তে volatile index আর compiler barrier প্রায়ই যথেষ্ট।

Multicore বা DMA-সহ: volatile CPU-র memory ordering ঠিক করে না (পর্ব ৫৩) — অন্য core head-এর নতুন মান দেখতে পারে ডেটার আগে। তাই C11 <stdatomic.h>: producer-এ atomic_store_explicit(&head, h + 1, memory_order_release), consumer-এ memory_order_acquire দিয়ে পড়া (acquire-release). আর head ও tail আলাদা cache line-এ রাখলে false sharing কমে (পর্ব ৪৮)। একাধিক producer হলে CAS বা lock লাগবেই।

প্র ২০৩২ byte-এর block-এর একটি memory pool নকশা করুন। malloc-এর তুলনায় সুবিধা-অসুবিধা কী? Double free কীভাবে ধরবেন? (মাঝারি)

উত্তর: ওপরের নকশা: static array, প্রতিটি খালি block-এর প্রথম কয়েক byte-এ পরের খালি block-এর pointer (intrusive free list); alloc আর free দুটোই O(1), কয়েকটি instruction. সুবিধা: নিশ্চিত সময় (real-time-এ জরুরি), external fragmentation নেই, metadata খরচ নেই, মোট মেমরি compile-এর সময়েই জানা। অসুবিধা: এক মাপ — ছোট বস্তুতে ভেতরের অপচয় (internal fragmentation); বিভিন্ন মাপের জন্য কয়েকটি pool (যেমন ১৬, ৬৪, ২৫৬ byte) — slab allocator-এর ধারণা; pool ফুরালে কী করবেন, আগেই ঠিক করতে হয়।

Double free ধরা: (১) প্রতি block-এ একটি “ব্যবহৃত” বিট রাখুন (আলাদা bitmap-এ — popcount আর ctz দিয়ে দ্রুত খালি block খোঁজাও যায়); (২) ঠিকানা pool-এর সীমার ভেতরে আর block-এর সীমানায় কি না পরীক্ষা: ((char *)p - (char *)pool) % sizeof(blk) == 0; (৩) debug build-এ মুক্ত block-এ একটি নির্দিষ্ট নকশা (যেমন 0xDEADBEEF) লিখে পরে use-after-free ধরা।

ফলো-আপ: “interrupt থেকেও alloc হবে?” — তখন alloc/free-কে critical section-এ রাখুন (interrupt বন্ধ করে) বা lock-free CAS ব্যবহার করুন; নইলে free list-এর মাথা দুই পক্ষ একসাথে বদলে list ভেঙে ফেলবে। C কোর্সের memory allocator পর্বে সাধারণ malloc-এর ভেতরটা দেখুন।

৭ · Embedded প্রশ্ন — volatile, register, interrupt, debouncing, watchdog, bus আর fixed-point

রিভিশন: পর্ব ১৮ · debouncing, পর্ব ২১ · Q15, পর্ব ৩৮ · trap, পর্ব ৫১ · memory-mapped I/O, পর্ব ৫২ · interrupt আর DMA, পর্ব ৫৬ · microcontroller

একটি microcontroller-এর peripheral — GPIO, timer, UART — নিয়ন্ত্রণ হয় নির্দিষ্ট ঠিকানায় বসানো register দিয়ে (memory-mapped I/O, পর্ব ৫১)। C-তে সেই ঠিকানাকে একটি volatile pointer বানিয়ে পড়া-লেখা হয়, আর প্রতিটি বিট বা field-এর জন্য set, clear, toggle-এর macro লেখা হয়। নিচের প্রোগ্রামে আসল hardware নেই, তাই একটি volatile ভ্যারিয়েবল register-এর ভূমিকা নিয়েছে; শেষ অংশে একটি read-modify-write race হাতে হাতে ঘটিয়ে দেখানো হয়েছে।

#include <stdio.h>
#include <stdint.h>

#define BIT(n)            (1u << (n))
#define REG_SET(r, m)     ((r) |= (m))
#define REG_CLR(r, m)     ((r) &= ~(m))
#define REG_TGL(r, m)     ((r) ^= (m))
#define FIELD_MASK(s, w)  ((((1u << (w)) - 1u)) << (s))
#define REG_WRF(r, s, w, v) ((r) = ((r) & ~FIELD_MASK(s, w)) | (((uint32_t)(v) << (s)) & FIELD_MASK(s, w)))

/* On a real MCU this would be: #define GPIO_OUT (*(volatile uint32_t *)0x40001014u)
   (address made up here; take it from the datasheet). A variable stands in for it. */
static volatile uint32_t GPIO_OUT;

int main(void) {
    REG_SET(GPIO_OUT, BIT(5) | BIT(0)); printf("set 5,0      : 0x%08X\n", (unsigned)GPIO_OUT);
    REG_TGL(GPIO_OUT, BIT(5));          printf("toggle 5     : 0x%08X\n", (unsigned)GPIO_OUT);
    REG_CLR(GPIO_OUT, BIT(0));          printf("clear 0      : 0x%08X\n", (unsigned)GPIO_OUT);
    REG_WRF(GPIO_OUT, 8, 4, 0xA);       printf("field[11:8]=A: 0x%08X\n", (unsigned)GPIO_OUT);

    /* read-modify-write race, replayed by hand */
    uint32_t copy = GPIO_OUT;           /* 1. main loop reads             */
    GPIO_OUT |= BIT(3);                 /* 2. interrupt fires, sets bit 3 */
    copy |= BIT(7);                     /* 3. main modifies its old copy  */
    GPIO_OUT = copy;                    /* 4. main writes back            */
    printf("after race   : 0x%08X  bit3 = %u (ISR's write lost), bit7 = %u\n",
           (unsigned)GPIO_OUT, (unsigned)((GPIO_OUT >> 3) & 1u), (unsigned)((GPIO_OUT >> 7) & 1u));
    return 0;
}

আসল রানের আউটপুট:

set 5,0      : 0x00000021
toggle 5     : 0x00000001
clear 0      : 0x00000000
field[11:8]=A: 0x00000A00
after race   : 0x00000A80  bit3 = 0 (ISR's write lost), bit7 = 1

বিট ৫ আর ০ চালু → 0x21; বিট ৫ উল্টে → 0x01; বিট ০ বন্ধ → ০; বিট ১১..৮-এ 0xA বসানো → 0xA00 — প্রতিটি macro অন্য বিট অক্ষত রেখেছে। Macro-গুলোতে প্রতিটি argument বন্ধনীতে, আর mask 1u দিয়ে — যাতে BIT(31) signed overflow না হয়। শেষ লাইনটিই আসল শিক্ষা: মূল লুপ register পড়ে কপি নিল (0xA00), এর মধ্যে “interrupt” বিট ৩ চালু করল (0xA08), তারপর মূল লুপ নিজের পুরোনো কপিতে বিট ৭ যোগ করে লিখে দিল — 0xA80. ফলে interrupt-এর লেখা বিট ৩ হারিয়ে গেল। আসল REG_SET-ও ভেতরে এই তিন ধাপই (load, or, store), তাই একই race ঘটতে পারে।

প্র ২১Register-এর বিট বদলানোর macro লেখার সময় কোন তিনটি ভুল সবচেয়ে বেশি হয়? Macro না static inline ফাংশন? (সহজ)

উত্তর: (১) বন্ধনী না দেওয়া: #define BIT(n) 1u << n হলে BIT(2) | BIT(3) ঠিক থাকলেও ~BIT(i + 1) ভেঙে যায় — ~1u << i + 1; প্রতিটি argument আর পুরো রাশি বন্ধনীতে রাখুন। (২) 1 বনাম 1u: 1 << 31 signed int-এ UB, আর ৬৪-বিট register-এ 1u << 40-ও UB — সেখানে 1ull বা UINT64_C(1). (৩) Argument দুবার ব্যবহার: REG_WRF-এর মতো macro-তে r দুবার আসে; REG_WRF(*p++, …) লিখলে pointer দুবার বাড়বে — side effect-সহ argument কখনো নয়।

Macro না ফাংশন: আধুনিক compiler static inline ফাংশনকে macro-র মতোই নিখরচায় বসিয়ে দেয়, আর তাতে type যাচাই হয়, argument একবারই মূল্যায়িত হয়, debugger-এ দেখা যায়। Macro তখনো কাজে লাগে যখন compile-এর সময়ের constant দরকার (যেমন #if-এ, বা static initializer-এ mask). বহু-statement macro হলে do { … } while (0) দিয়ে মোড়ানো — যাতে if-else-এর ভেতরে নিরাপদ থাকে।

ফলো-আপ: “Toggle কেন বিপজ্জনক হতে পারে?” — ^= একটি RMW; যদি register-এর অন্য বিট hardware নিজে বদলায় (status বিট), বা কিছু বিট “write-1-to-clear” হয়, তবে পড়া মান আবার লিখে দিলে অনিচ্ছায় সেই বিটগুলো মুছে যেতে পারে — তাই datasheet-এ প্রতিটি বিটের পড়া-লেখার ধরন (R/W, RO, W1C) দেখে নিন।

প্র ২২volatile কী করে, আর কী করে না? তিনটি জায়গা বলুন যেখানে লাগে। (মাঝারি)

উত্তর: volatile compiler-কে বলে: এই বস্তুর প্রতিটি পড়া আর লেখা একটি “দেখা যায় এমন ঘটনা” — বাদ দেওয়া যাবে না, একত্র করা যাবে না, register-এ রেখে দেওয়া যাবে না, আর volatile-গুলোর মধ্যে ক্রম বদলানো যাবে না। পর্ব ৩১-এ দেখেছেন, volatile ছাড়া clang একটি UART status-এর polling লুপকে একবার পড়ে চিরকালের লুপ বানিয়েছিল। যেখানে লাগে: (১) memory-mapped hardware register; (২) interrupt handler বা signal handler আর মূল লুপের মধ্যে ভাগ করা flag (signal-এ volatile sig_atomic_t); (৩) setjmp/longjmp-এর পরে ব্যবহৃত local ভ্যারিয়েবল।

যা করে না: (১) atomicity নয় — volatile int c; c++; এখনো load, add, store তিনটি আলাদা instruction; মাঝে interrupt এলে বা অন্য core লিখলে আপডেট হারায় (ওপরের race). (২) CPU-র memory ordering নয় — compiler ক্রম রাখে, কিন্তু out-of-order CPU বা store buffer অন্য core-এর চোখে ক্রম বদলাতে পারে (পর্ব ৫৩); তার জন্য fence বা atomic. (৩) non-volatile ভ্যারিয়েবলের ক্রম volatile-এর চারপাশে স্থির থাকে না। (৪) cache coherence নয় — DMA buffer-এর জন্য cache clean/invalidate আলাদা কাজ (পর্ব ৫২)।

ফলো-আপ: “Java-র volatile কি একই?” — না; Java-তে volatile মানে thread-গুলোর মধ্যে visibility আর ordering-এর নিশ্চয়তা (Java memory model অনুযায়ী) — C-র মতো শুধু compiler-এর ওপর নিষেধ নয়। “Thread-এর মধ্যে flag-এ volatile যথেষ্ট?” — না, C11 atomic ব্যবহার করুন।

প্র ২৩Interrupt service routine (ISR) লেখার নিয়মগুলো কী? ISR আর মূল লুপ একটি ভ্যারিয়েবল ভাগ করলে কীভাবে নিরাপদ রাখবেন? (মাঝারি)

উত্তর — ISR-এর নিয়ম: (১) ছোট আর দ্রুত — ডেটা তুলে ring buffer-এ রাখুন বা একটি flag দিন, ভারী কাজ মূল লুপে; লম্বা ISR অন্য interrupt-এর latency বাড়ায়। (২) কখনো অপেক্ষা নয় — delay(), busy-wait, mutex নেওয়া, blocking I/O নিষেধ; ISR-এ ঘুমালে কেউ জাগানোর নেই। (৩) সাধারণত printf, malloc নয় — এগুলো reentrant নয়, lock নিতে পারে, সময় অনিশ্চিত। (৪) interrupt-এর উৎস clear করুন (peripheral-এর flag) — নইলে ফেরামাত্র আবার ঢুকবে (interrupt storm, পর্ব ৫২)। (৫) floating point এড়িয়ে চলুন, অথবা জানুন FPU-র register কে save করে — এতে latency বাড়ে।

ভাগ করা ভ্যারিয়েবল: (১) volatile — যাতে মূল লুপ প্রতিবার মেমরি থেকে পড়ে। (২) পড়া-লেখা যদি একটি aligned word-এর বেশি হয় (৬৪-বিট counter ৩২-বিট MCU-তে, বা একাধিক field), বা read-modify-write হয় — মূল লুপে সেই অংশে interrupt বন্ধ করুন: critical section. RISC-V-তে csrci mstatus, 8 (MIE বিট বন্ধ) আর পরে আগের অবস্থা ফেরত; ARM Cortex-M-এ cpsid i/cpsie i; x86-এ cli/sti. বন্ধ রাখার সময় যত ছোট সম্ভব। (৩) বা atomic instruction: RISC-V-এর amoor.w, C11 atomic_fetch_or. (৪) অনেক MCU-তে আলাদা “set” আর “clear” register থাকে (write-1-to-set/clear) — এক store-এই একটি বিট বদলায়, RMW-ই লাগে না।

ফলো-আপ: “Nested interrupt থাকলে?” — উচ্চ priority-র ISR নিম্নটিকে থামাতে পারে, তাই ISR-গুলোর মধ্যেও একই সাবধানতা; আর “interrupt বন্ধ করে আগের অবস্থা ফেরত” — সরাসরি চালু নয়, কারণ ডাকার আগে হয়তো বন্ধই ছিল।

প্র ২৪একটি বোতাম একবার চাপলে কাউন্টার কয়েকবার বাড়ে। কেন? Software-এ debouncing লিখুন, আর hardware সমাধান বলুন। (মাঝারি)

উত্তর: যান্ত্রিক সুইচের পাত চাপ দেওয়ার পরে কয়েক মিলিসেকেন্ড লাফায় — contact bounce — সংযোগ হয়, ছেঁড়ে, আবার হয় (পর্ব ১৮)। প্রতি ধারে interrupt বা গণনা হলে একবারের চাপ কয়েকবার গোনা হয়। Software debouncing: একটি timer interrupt-এ (ধরা যাক প্রতি ৫ ms) pin পড়ুন; নতুন মান পরপর N বার (যেমন ৪ বার = ২০ ms) একই এলে তবেই অবস্থা বদল মেনে নিন। কোডে: if (raw != stable) { if (++cnt >= 4) { stable = raw; cnt = 0; on_change(); } } else cnt = 0;. বিকল্প: প্রতি নমুনা একটি ৮-বিট shift register-এ ঢুকিয়ে hist = (hist << 1) | raw; — hist == 0xFF হলে চাপা, 0x00 হলে ছাড়া। Edge-interrupt-এ সরাসরি গোনা এড়ান, বা প্রথম ধারের পর কিছুক্ষণ interrupt বন্ধ রাখুন।

Hardware: RC low-pass filter আর Schmitt trigger ইনপুট (hysteresis ধীর ধারকে পরিষ্কার করে), বা SR latch-সহ দুই-পায়ের (SPDT) সুইচ (পর্ব ১৫)। FPGA বা ASIC-এ একই counter-ভিত্তিক debouncer, আগে two-flip-flop synchronizer (পর্ব ১৮)।

ফলো-আপ: “সময় কত ধরবেন?” — সুইচের datasheet বা অসিলোস্কোপে মেপে; প্রায়ই কয়েক থেকে ২০ ms-এর মধ্যে ধরা হয়, তবে যন্ত্রভেদে আলাদা। মানুষের চোখে ৫০ ms-এর কম দেরি সাধারণত টের পাওয়া যায় না।

প্র ২৫Watchdog timer কী? কোথায় “খাওয়াবেন” (kick) — timer interrupt-এ খাওয়ালে সমস্যা কী? (মাঝারি)

উত্তর: Watchdog একটি স্বাধীন counter, যা নিজের clock-এ গুনতে থাকে; software নির্দিষ্ট সময়ের মধ্যে একটি বিশেষ মান লিখে তাকে reset না করলে পুরো chip reset হয় (পর্ব ৫৬)। লক্ষ্য: software অনন্ত লুপে বা deadlock-এ আটকে গেলে, বা বিদ্যুতের ঝাঁকুনিতে গোলমাল হলে, মানুষ ছাড়াই যন্ত্র নিজেকে উদ্ধার করে।

কোথায় খাওয়াবেন: মূল লুপে — আর কেবল তখনই, যখন প্রতিটি জরুরি কাজ সত্যিই এগিয়েছে বলে প্রমাণ আছে (প্রতিটি task নিজের “আমি বেঁচে আছি” বিট দেয়, মূল লুপ সব বিট দেখে তবেই kick আর বিটগুলো মোছে)। Timer ISR-এ খাওয়ানো ভুল: মূল লুপ আটকে গেলেও timer interrupt নিয়মিত আসতে থাকে, তাই watchdog কখনো কাজ করবে না — নিরাপত্তা-জালটি অর্থহীন। ইন্টারভিউতে এটিই আসল প্রশ্ন।

ফলো-আপ: “Window watchdog কী?” — খুব আগে খাওয়ালেও reset হয়; এতে “লুপ ভুল করে খুব দ্রুত ঘুরছে” ধরনের বাগও ধরা পড়ে। “Reset-এর পর কী?” — কারণ-register পড়ে (watchdog reset ছিল কি না) log করা, আর বারবার হলে নিরাপদ অবস্থায় চলা।

ইন্টারভিউর আরেকটি নিশ্চিত প্রশ্ন: তিনটি serial bus-এর পার্থক্য। পর্ব ৫৬-এর টেবিলটি সংক্ষেপে:

বিষয়UARTI2CSPI
তার (GND ছাড়া)২: TX, RX২: SDA, SCL৩ + প্রতি device-এ ১টি CS
Clockনেই — দুই পক্ষ একই baud rate মানেcontroller দেয়controller দেয়
Device বাছাইএক-এক (point-to-point)ঠিকানা (সাধারণত ৭-বিট), একই দুই তারে অনেক deviceCS তার দিয়ে
Duplexfull (দুই তার আলাদা)half (এক ডেটা-তার)full (MOSI আর MISO একসাথে)
প্রাপ্তি নিশ্চিতনেই (ঐচ্ছিক parity)প্রতি byte-এর পর ACK বিটনেই
গতি (মোটামুটি)৯৬০০–১১৫২০০ baud প্রচলিত১০০ / ৪০০ kbit/s, দ্রুত mode-ও আছেকয়েক থেকে কয়েক ডজন MHz
বৈদ্যুতিকpush-pullopen-drain, pull-up লাগেpush-pull
প্র ২৬UART, I2C আর SPI-এর মধ্যে কোনটি কখন বাছবেন? ১১৫২০০ baud-এ 8N1-এ সেকেন্ডে সর্বোচ্চ কত byte? I2C-তে pull-up কেন লাগে? (মাঝারি)

উত্তর: UART — দুই যন্ত্রের মধ্যে সরল সংযোগ, clock তার নেই: debug console, GPS module, modem, অন্য MCU. I2C — কম তারে অনেক ধীর device (তাপমাত্রা sensor, EEPROM, RTC, ছোট display); ঠিকানা আর ACK আছে, কিন্তু ধীর। SPI — দ্রুত বড় ডেটা (flash memory, SD card, display, ADC); সরল, full-duplex, কিন্তু প্রতি device-এ একটি বাড়তি CS তার আর কোনো ACK নেই।

হিসাব (পর্ব ৫৬): 8N1-এ প্রতি byte = ১ start + ৮ data + ১ stop = ১০ বিট। ১১৫২০০ ÷ ১০ = ১১,৫২০ byte/s — ৮ দিয়ে ভাগ করা (১৪,৪০০) প্রচলিত ভুল। ৯৬০০ baud-এ ৯৬০ byte/s. I2C-তে প্রতি byte ৯টি clock (৮ ডেটা + ACK).

Pull-up: I2C-র লাইন open-drain — প্রতিটি device শুধু লাইনকে ০-তে টানতে পারে, ১ করতে পারে না; resistor লাইনকে ১-এ তোলে। ফলে দুজন একসাথে চালালে short circuit হয় না, আর “wired-AND” আচরণে arbitration আর clock stretching (ধীর device clock ধরে রাখে) সম্ভব হয়। Pull-up-এর মান বেশি হলে ধার ধীর (RC), কম হলে বেশি বিদ্যুৎ খরচ।

ফলো-আপ: “UART-এ baud rate-এর অমিল কত সহ্য হয়?” — receiver প্রতি বিটের মাঝখানে নমুনা নেয় আর প্রতি frame-এ start বিটে আবার মেলায়, তাই কয়েক শতাংশ অমিল সাধারণত চলে; ঠিক সীমা oversampling আর frame দৈর্ঘ্যের ওপর নির্ভর করে।

শেষ embedded বিষয়: fixed-point গণিত। পর্ব ২১-এর নিয়ম: দুটি Q15 গুণ করলে ৩২-বিটে Q30, ১৫ ঘর ডানে shift করে ফেরা, আর কাছের মানে rounding-এর জন্য shift-এর আগে 214 যোগ। Q16.16-এ (১৬ বিট পূর্ণ অংশ, ১৬ বিট ভগ্নাংশ) একই নিয়ম ৬৪-বিট গুণফলে, ১৬ ঘর shift:

#include <stdio.h>
#include <stdint.h>

static int16_t q15_mul(int16_t a, int16_t b) {
    int32_t p = (int32_t)a * b;          /* Q15 x Q15 = Q30 */
    p += 1 << 14;                        /* add half an LSB: round to nearest */
    p >>= 15;                            /* back to Q15 (gcc: arithmetic shift) */
    if (p > INT16_MAX) p = INT16_MAX;    /* only -1 x -1 can overflow: saturate */
    return (int16_t)p;
}
static int32_t q16_mul(int32_t a, int32_t b) {
    int64_t p = (int64_t)a * b;          /* Q16.16 x Q16.16 = Q32.32 in 64 bits */
    p += (int64_t)1 << 15;               /* round */
    return (int32_t)(p >> 16);
}
static int32_t to_q16(double x) { return (int32_t)(x * 65536.0 + (x >= 0 ? 0.5 : -0.5)); }

int main(void) {
    int16_t a = 9830, b = 22938;                       /* 0.3 and 0.7 in Q15 */
    int16_t c = q15_mul(a, b);
    printf("Q15  0.3 x 0.7 = %d -> %.7f (exact 0.21)\n", c, c / 32768.0);
    printf("Q15 -1 x -1    = %d (saturated, +1 does not fit)\n", q15_mul(INT16_MIN, INT16_MIN));
    int32_t x = to_q16(3.25), y = to_q16(-1.5), z = q16_mul(x, y);
    printf("Q16.16 3.25 x -1.5 = 0x%08X -> %.6f\n", (unsigned)z, z / 65536.0);
    int32_t pi = to_q16(3.14159265), r = to_q16(2.5);
    printf("area pi*r*r (r=2.5): fixed %.6f, double %.6f\n",
           q16_mul(q16_mul(pi, r), r) / 65536.0, 3.14159265 * 2.5 * 2.5);
    printf("Q16.16 range %.1f .. %.5f, step %.8f\n", INT32_MIN / 65536.0, INT32_MAX / 65536.0, 1 / 65536.0);
    return 0;
}

আসল রানের আউটপুট:

Q15  0.3 x 0.7 = 6881 -> 0.2099915 (exact 0.21)
Q15 -1 x -1    = 32767 (saturated, +1 does not fit)
Q16.16 3.25 x -1.5 = 0xFFFB2000 -> -4.875000
area pi*r*r (r=2.5): fixed 19.634933, double 19.634954
Q16.16 range -32768.0 .. 32767.99998, step 0.00001526

Q15: ০.৩ → ৯৮৩০, ০.৭ → ২২৯৩৮; গুণফল ৬৮৮১ = ০.২০৯৯৯১৫ — সঠিক ০.২১ থেকে এক LSB-এর (≈ ০.০০০০৩) কম ভুল, পর্ব ২১-এর হিসাবের সাথে হুবহু। −১ × −১ = +১ Q15-এ ধরে না, তাই ৩২৭৬৭-এ আটকানো (saturation) — না করলে wrap হয়ে −১ হতো! Q16.16: ৩.২৫ × −১.৫ = −৪.৮৭৫ ঠিক এল (0xFFFB2000 — ঋণাত্মক, তাই ওপরের বিটগুলো 1). বৃত্তের ক্ষেত্রফলে fixed-point ১৯.৬৩৪৯৩৩, double-এ ১৯.৬৩৪৯৫৪ — পার্থক্য প্রায় ০.০০০০২, কারণ π নিজেই 2−16 ধাপে গোল হয়েছে আর প্রতিটি গুণে rounding জমেছে। শেষ লাইন: পরিসর −৩২৭৬৮ থেকে প্রায় ৩২৭৬৮, ধাপ ≈ ০.০০০০১৫ — float-এর মতো নমনীয় নয়, কিন্তু সব জায়গায় সমান নির্ভুলতা।

প্র ২৭কিছু MCU-তে floating point এড়িয়ে fixed-point কেন ব্যবহার করা হয়? Q-format বাছবেন কীভাবে? (মাঝারি)

উত্তর: (১) FPU নেই: ছোট MCU-তে (যেমন ARM Cortex-M0/M0+/M3) hardware FPU নেই; float লিখলে compiler software floating point-এর library ডাকে — প্রতিটি গুণ-যোগে কয়েক ডজন থেকে শতাধিক cycle, আর কোডের আকারও কয়েক KB বাড়ে। M4F-এ সাধারণত single-precision FPU থাকে, কিন্তু double তখনো software-এ — 3.14 লিখলে সেটি double, তাই 3.14f লেখার অভ্যাস জরুরি। (২) শক্তি আর সময়: integer গুণ কম শক্তি নেয় (পর্ব ৫৬), আর সময় নির্ধারিত — real-time-এ WCET সহজ। (৩) নির্ধারিত ফল: fixed-point-এ একই ইনপুটে সব প্ল্যাটফর্মে বিট-হুবহু একই ফল; float-এ compiler, FMA (পর্ব ২৪) আর optimization-ভেদে শেষ বিট বদলাতে পারে। (৪) ISR-এ FPU register save করার খরচ এড়ানো।

Q-format বাছাই: সর্বোচ্চ মান দেখে পূর্ণ অংশের বিট, প্রয়োজনীয় নির্ভুলতা দেখে ভগ্নাংশের বিট। যেমন ADC-র ভোল্টেজ ০–৩.৩ V, mV নির্ভুলতা → ২ বিট পূর্ণ অংশ যথেষ্ট, Q2.13 বা Q3.12 ১৬ বিটে; অডিও আর DSP-তে −১ থেকে ১ — Q15/Q31. সবসময় ভাবুন: মধ্যবর্তী গুণফল চওড়া register-এ (৩২ বা ৬৪ বিট), rounding আর saturation কোথায়, আর ভাগে কী হবে।

ফলো-আপ: “Q16.16-এ ১.৫-কে কী লিখবেন?” — ১.৫ × ৬৫৫৩৬ = ৯৮৩০৪ = 0x00018000. “দুটি Q16.16 ভাগ?” — ((int64_t)a << 16) / b, ভাগের আগে বাঁয়ে shift, b = ০ পরীক্ষা।

আরও গভীরে: বাস্তব firmware-এ register-এর macro প্রায়ই নির্মাতার দেওয়া header থেকে আসে, যেখানে প্রতিটি peripheral একটি volatile struct হিসেবে লেখা — member-এর offset datasheet-এর register map-এর সাথে মেলে, আর ARM-এর CMSIS-এর মতো মানক স্তর interrupt চালু-বন্ধের মতো কাজের অভিন্ন ফাংশন দেয়। কিছু Cortex-M3/M4-এ “bit-banding” নামে একটি ব্যবস্থা ছিল, যেখানে প্রতিটি বিটের নিজের একটি alias ঠিকানা থাকে, ফলে এক store-এ একটি বিট atomically বদলায়। নিরাপত্তা-সংবেদনশীল গাড়ি বা চিকিৎসা-যন্ত্রের কোডে MISRA C-এর মতো নির্দেশিকা অংশ ৫-এর অনেক ফাঁদ (implicit conversion, signed shift) নিয়ম করে নিষেধ করে, আর static analysis tool দিয়ে তা যাচাই হয়।

৮ · Assembly পড়া — RISC-V আর x86-64-এর ছোট listing দেখে বলুন কী হিসাব হচ্ছে

রিভিশন: পর্ব ২৬ · RISC-V instruction, পর্ব ২৯ · শাখা আর লুপ, পর্ব ৩০ · calling convention, পর্ব ৩১ · C থেকে assembly

Firmware, compiler, performance আর security পদের ইন্টারভিউতে প্রায়ই একটি ছোট listing দিয়ে বলা হয়: “এটি কী করে?” কৌশল পর্ব ৩১-এর মতো: (১) calling convention থেকে ইনপুট-আউটপুট চিনুন — RISC-V-এ argument a0, a1…, ফল a0; x86-64 System V-এ argument edi/rdi, esi/rsi…, ফল eax/rax. (২) লুপ খুঁজুন — পেছনের দিকে লাফ। (৩) প্রতিটি register-কে একটি C ভ্যারিয়েবলের নাম দিন। (৪) ছোট একটি ইনপুট নিয়ে হাতে চালান। RISC-V listing-গুলো RV32I-এ compiler-এর ধাঁচে হাতে লেখা; x86-64-এরগুলো Wandbox-এর gcc 13 -O2 -masm=intel-এর আসল আউটপুট (comment আমাদের)।

f1:
    li    a1, 0             # ?
    beqz  a0, .L2
.L1:
    addi  a2, a0, -1
    and   a0, a0, a2
    addi  a1, a1, 1
    bnez  a0, .L1
.L2:
    mv    a0, a1
    ret
প্র ২৮Listing A (RISC-V) কী ফেরত দেয়? x = 0x6C হলে লুপ কতবার ঘোরে? (সহজ)

উত্তর: এটি Kernighan-এর popcount — x-এ কয়টি বিট 1. নাম দিন: a0 = x, a1 = n (০ দিয়ে শুরু)। x = ০ হলে সরাসরি .L2-এ, ফল ০। লুপে a2 = x - 1, x = x & a2, n++, আর x ≠ ০ হলে আবার — অর্থাৎ while (x) { x &= x - 1; n++; }. শেষে mv a0, a1 — ফল n. x = 0x6C-তে চারটি 1, তাই লুপ ৪ বার (অংশ ২-এর stepper).

ফলো-আপ: (ক) “লুপের শর্ত শেষে কেন?” — compiler লুপকে “rotate” করে: প্রথমে একবার শর্ত দেখে, তারপর নিচে শর্ত রেখে do-while বানায় — প্রতি ঘুরে একটি লাফ কম (পর্ব ২৯)। (খ) Zbb থাকলে পুরো ফাংশন cpop a0, a0; ret. (গ) x86-এ একই C কোড gcc -O2-এ (popcnt ছাড়া) সাধারণত এই কাঠামোই দেয় — lea দিয়ে x − 1, তারপর and, add, jne — আর -mpopcnt দিলে নিচের listing D-এর মতো এক instruction.

f2:
    lw    a2, 0(a0)
    li    a3, 1
    bge   a3, a1, .L4
.L3:
    slli  a4, a3, 2
    add   a4, a0, a4
    lw    a4, 0(a4)
    bge   a2, a4, .L5
    mv    a2, a4
.L5:
    addi  a3, a3, 1
    blt   a3, a1, .L3
.L4:
    mv    a0, a2
    ret
প্র ২৯Listing B (RISC-V) কী হিসাব করে? bge বদলে bgeu লিখলে কী বদলাবে? (মাঝারি)

উত্তর: Array-র সবচেয়ে বড় মান (signed). a2 = m = a[0], a3 = i = ১। n ≤ ১ হলে সরাসরি ফল a[0]. লুপে slli a4, a3, 2 = i × ৪ (int ৪ byte), add দিয়ে &a[i], lw দিয়ে a[i]; bge a2, a4 — m ≥ a[i] হলে বাদ, নইলে m = a[i]. তারপর i++ আর i < n হলে আবার। C-তে: int m = a[0]; for (int i = 1; i < n; i++) if (a[i] > m) m = a[i]; return m;

bgeu দিলে তুলনা unsigned হবে — তখন −১ (0xFFFFFFFF) সবচেয়ে বড় ধরা হবে, অর্থাৎ ফাংশনটি হবে unsigned-এর max. RISC-V-তে signed আর unsigned-এর জন্য আলাদা branch (blt/bltu, bge/bgeu) — কারণ এখানে কোনো flag নেই (পর্ব ২৬); x86-এ একই cmp-এর পরে jl বনাম jb.

ফলো-আপ: “gcc -O2 x86-এ কী বানায়?” — সাধারণত branch-এর বদলে cmp আর cmovl — অর্থাৎ branchless max, যাতে এলোমেলো ডেটায় misprediction না হয়; আর index-এর বদলে চলমান pointer. সংস্করণ আর flag-ভেদে খুঁটিনাটি বদলায়, তাই নিজে -S দিয়ে দেখে নিন।

c1:                         c2:
    mov   eax, edi              mov   edx, edi
    neg   eax                   sar   edx, 31
    and   eax, edi              xor   edi, edx
    ret                         mov   eax, edi
                                sub   eax, edx
                                ret
প্র ৩০Listing C-র দুটি ফাংশন (x86-64) কী করে? c2-কে RISC-V-তে লিখুন। (মাঝারি)

উত্তর: c1: eax = x, তারপর neg (eax = −x), তারপর and eax, edi — অর্থাৎ x & -x: সবচেয়ে নিচের 1-বিটটি একা রাখা (অংশ ২)। ১২ (1100) দিলে ৪। c2: edx = x sar ৩১ — ০ বা −১ (চিহ্নের mask m); তারপর (x ^ m) - m — branchless abs (প্র ০৪)। −৭ দিলে: m = −১, x ⊕ m = ৬, ৬ − (−১) = ৭। লক্ষ করুন sar = arithmetic shift — gcc ঋণাত্মক signed-এর >>-কে এভাবেই অনুবাদ করে।

RISC-V-তে c2: srai a1, a0, 31; xor a0, a0, a1; sub a0, a0, a1; ret — তিনটি instruction, কারণ RISC-V-এ তিন-operand রূপে আলাদা mov লাগে না; x86-এ দুই-operand instruction উৎসকে মুছে দেয় বলে বাড়তি mov (পর্ব ২৫)।

ফলো-আপ: “c1 x = ০ হলে?” — ০ & ০ = ০; “x = 0x80000000?” — −x একই মান, তাই ফল 0x80000000 — unsigned হিসেবে এটি ঠিক।

d1:                       # int d1(int x)
    test   edi, edi
    lea    eax, [rdi+7]
    cmovns eax, edi
    sar    eax, 3
    ret

d2:                       # unsigned d2(unsigned x, unsigned r)
    mov    eax, edi
    mov    ecx, esi
    rol    eax, cl
    ret

d3:                       # same C as listing A, compiled with -mpopcnt
    xor    eax, eax
    xor    edx, edx
    popcnt eax, edi
    test   edi, edi
    cmove  eax, edx
    ret
প্র ৩১Listing D-র d1 কী হিসাব করে? x = −৯ দিয়ে হাতে চালান। শুধু sar eax, 3 দিলে কী ভুল হতো? (কঠিন)

উত্তর: d1 = x / 8 (signed ভাগ)। lea eax, [rdi+7] = x + ৭; test edi, edi চিহ্ন দেখে, আর cmovns — x ঋণাত্মক না হলে eax = x (অর্থাৎ ৭ যোগ বাদ)। তারপর ৩ ঘর arithmetic shift = ৮ দিয়ে ভাগ। x = −৯: −৯ + ৭ = −২, ঋণাত্মক বলে cmov হয় না, −২ sar ৩ = −১। আর C-তে −৯ / ৮ = −১ (শূন্যের দিকে গোল) ✓।

শুধু shift দিলে: −৯ >> ৩ = −২ — কারণ arithmetic shift −∞-এর দিকে গোল করে (floor), আর C-র / শূন্যের দিকে (truncation, পর্ব ২২)। ঋণাত্মক হলে আগে (2k − ১) যোগ করলে দুটি মেলে। Wandbox-এ যাচাই: -7 / 2 = −৩ কিন্তু -7 >> 1 = −৪। x unsigned হলে compiler শুধু shr eax, 3 দিত — তাই “২-এর ঘাতে ভাগ হলে unsigned দ্রুত” কথাটির ভিত্তি এটাই।

d2 ওপরের rotate idiom-এর ফল — এক rol, shift-এর পরিমাণ cl-এ। d3-এ gcc লুপটিকে চিনে একটি popcnt বানিয়েছে; তবু x = ০-র জন্য test/cmove রেখে দিয়েছে, যদিও popcnt নিজেই ০-তে ০ দেয় — compiler-এর আউটপুট সবসময় নিখুঁত নয়। আর শুরুর xor eax, eax কিছু Intel CPU-র popcnt-এর destination register-এর ওপর “মিথ্যা নির্ভরতা” ভাঙতে বলে ব্যাপকভাবে জানা যায় — microarchitecture-এর এমন খুঁটিনাটি পর্ব ৪০-এর register renaming-এর সাথে জড়িত।

৯ · AI-এর যুগে — low-level প্রস্তুতিতে AI-কে কাজে লাগানো আর যাচাই করা

AI-কে “একটি ring buffer লিখে দিন” বা “এই struct-এর sizeof কত” জিজ্ঞেস করলে উত্তর মুহূর্তে আসে, আর প্রায়ই প্রায়-ঠিক। কিন্তু low-level কোডে “প্রায়-ঠিক” মানেই বাগ — একটি 1-এর জায়গায় 1u না লেখা, বা একটি volatile বাদ পড়া মাঠে বসানো যন্ত্রে কয়েক মাস পরে ধরা পড়ে। আর ইন্টারভিউতে AI পাশে থাকবে না: তাই AI-কে উত্তরদাতা নয়, প্রশ্নকর্তা আর পরীক্ষক হিসেবে ব্যবহার করুন — আর তার প্রতিটি দাবি compiler, sanitizer আর datasheet দিয়ে মিলিয়ে নিন।

AI প্রায়ই যা করেসত্যিটাযাচাইয়ের উপায়
mask-এ 1 << 31, signed-এ shiftC-তে UB; 1u লিখতে হয়-fsanitize=undefined দিয়ে test
struct-এর মাপ = member-দের যোগফল, বা অন্য ABI-র উত্তরpadding যোগ হয়, ABI-ভেদে আলাদাsizeof/offsetof ছাপুন, _Static_assert
“volatile দিলে thread-safe”atomicity বা CPU ordering দেয় নাC11 atomic, ThreadSanitizer
char-কে সবসময় signed (বা unsigned) ধরাimplementation-defined; ARM/RISC-V Linux-এ unsignedলক্ষ্য প্ল্যাটফর্মে CHAR_MIN ছাপুন
Q15 গুণে rounding বা saturation বাদ, বা ১৬-বিট গুণফল৩২-বিট গুণফল, 214 যোগ, −১ × −১-এ saturationসীমার মান দিয়ে test (0x8000, 0x7FFF)
বানানো register-ঠিকানা বা bit-অবস্থানchip-এর datasheet বা reference manual-ই সত্যdatasheet-এর register map মেলান
RISC-V আর MIPS-এর instruction মেশানো, বা x86-এর AT&T আর Intel ক্রম গুলিয়ে ফেলাISA manual আর syntax আলাদানিজে compile করে listing দেখুন (Compiler Explorer, -S)

AI-কে mock interviewer বানাতে নিচের কাঠামোটি কাজে লাগবে — সে প্রশ্ন করবে, আপনি উত্তর দেবেন, আর সে আপনার কোডের edge case খুঁজবে:

তুমি একজন embedded firmware ইন্টারভিউয়ার। আমাকে একটি একটি করে প্রশ্ন করো, উত্তর নিজে দিয়ে দিও না। বিষয়: bit manipulation, C-র integer promotion ও undefined behaviour, struct padding (x86-64 System V ABI), volatile, ISR, ring buffer, fixed-point (Q15). নিয়ম: ১. প্রতিটি প্রশ্নের পরে আমার উত্তরের অপেক্ষা করো। ২. আমার C কোড পেলে অন্তত তিনটি edge case বলো (০, সব বিট 1, INT_MIN, shift 0 বা 32, aliasing) যেখানে কোডটি ভুল হতে পারে। ৩. কোনো আচরণ UB না implementation-defined, তা আলাদা করে বলো, আর কোন অনুচ্ছেদ বা নিয়মের ভিত্তিতে বলছ তা উল্লেখ করো। ৪. "এটা কী ছাপবে" ধরনের প্রশ্নে তোমার উত্তর যাচাই করার জন্য একটি ছোট C প্রোগ্রাম দাও — আমি gcc -Wall -Wextra -fsanitize=undefined দিয়ে নিজে চালিয়ে মেলাব। ৫. নিশ্চিত না হলে বলো "নিশ্চিত নই" — অনুমান করে register-এর ঠিকানা বা chip-এর তথ্য বানিয়ো না।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

নিজেকে যাচাই করুন

প্রতিটি “এটা কী ছাপবে” প্রশ্নের উত্তর Wandbox-এর gcc (Linux x86-64)-এ চালিয়ে যাচাই করা। ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি microcontroller-এ UART-এ একটি byte এলে interrupt-চালিত ring buffer-এ কী কী ঘটে — সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. UART-এ একটি byte পৌঁছায়, peripheral তার “data ready” flag তোলে
  2. Interrupt controller CPU-কে জানায়, CPU চলতি instruction শেষে ISR-এ লাফ দেয়
  3. ISR data register পড়ে (তাতে flag মোছে) আর byte-টি ring buffer-এ রেখে head বাড়ায়
  4. ISR ফেরত যায়, CPU থেমে থাকা কাজ আবার শুরু করে
  5. মূল লুপ head ≠ tail দেখে byte-টি তুলে নিয়ে প্রক্রিয়া করে

(সহজ) unsigned x = 0x58; হলে x & (x - 1)-এর মান কত?

  • 0x57
  • 0x50
  • 0x08
  • 0x48

(মাঝারি) gcc, Linux x86-64-এ unsigned char a = 250, b = 10; printf("%d\n", a + b); কী ছাপবে?

  • 4
  • 255
  • Undefined behaviour
  • 260

(মাঝারি) x86-64 Linux (System V ABI)-এ struct S { char a; double b; char c; };-এর sizeof কত?

  • 24
  • 10
  • 16
  • 12

(কঠিন) gcc, Linux x86-64-এ int8_t c = -8; printf("%d %d\n", c >> 1, (uint8_t)c >> 1); কী ছাপবে?

  • 124 124
  • -4 -4
  • -4 124
  • -4 252

(কঠিন) Q15 ফরম্যাটে (১৬-বিট, মান = N / 215) a = 0x4000 আর b = 0x6000. ৩২-বিট গুণফলে 214 যোগ করে ১৫ ঘর ডানে shift করলে Q15 ফল কত?

  • 0x1800
  • 0x6000
  • 0x3000
  • 0x0C00

(মাঝারি) একটি interrupt handler আর মূল লুপ একটি volatile uint32_t count ভাগ করে; দুজনেই count++ করে। কোন কথাটি সত্য?

  • প্রতিটি পড়া-লেখা সত্যিই মেমরিতে হবে, কিন্তু count++ atomic নয় — আপডেট হারাতে পারে
  • volatile থাকায় count++ atomic, কোনো সমস্যা নেই
  • volatile একটি পূর্ণ memory barrier বসায়
  • volatile ভ্যারিয়েবল cache এড়িয়ে সরাসরি RAM-এ যায়

(মাঝারি) একটি UART ৯৬০০ baud-এ 8N1 ফরম্যাটে (১ start, ৮ data, কোনো parity নেই, ১ stop বিট) বিরতিহীন পাঠালে সেকেন্ডে সর্বোচ্চ কত byte যায়?

  • 1200
  • 9600
  • 1066
  • 960

(মাঝারি) একটি rotary encoder ৪-বিট Gray code 1011 পাঠাল। সাধারণ binary-তে মানটি দশমিকে কত?

  • 11
  • 13
  • 14
  • 8

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে একসাথে:

মনে রাখুন

  1. কাঠামো আগে: width আর signedness জিজ্ঞেস, edge case (০, সব বিট 1, INT_MIN, shift ০ বা width), হাতে একটি উদাহরণ, unsigned-এ কোড, test, জটিলতা আর hardware instruction, শেষে portability.
  2. দুটি সূত্রে অর্ধেক বিট-প্রশ্ন: x & (x − 1) নিচের 1 মোছে (২-এর ঘাত, Kernighan popcount), x & −x নিচের 1 রাখে; rotate-এ & 31, “+” ছাড়া যোগে XOR আর বহন, Gray = b ^ (b >> 1).
  3. তিন শ্রেণি আলাদা রাখুন: unsigned wrap well-defined; ঋণাত্মক signed-এর ডান shift আর char-এর চিহ্ন implementation-defined; signed overflow, width-এর সমান shift, 1 << 31 — UB. ছোট type আগে int-এ উন্নীত হয় — uint16_t-এর গুণেও UB লুকিয়ে থাকে।
  4. মেমরি: struct-এর মাপ alignment-এর তিন নিয়মে (x86-64-এ {char; int; char} = ১২, বড় থেকে ছোট সাজালে কম); stack দ্রুত কিন্তু ছোট, heap নমনীয় কিন্তু অনিশ্চিত; ring buffer-এ ২-এর ঘাত আর unsigned index, pool-এ O(1) free list.
  5. Embedded: volatile পড়া-লেখা বাদ পড়তে দেয় না, কিন্তু atomic বা ordering দেয় না; ISR ছোট, অপেক্ষাহীন, ভাগ করা ডেটা critical section বা atomic-এ; watchdog মূল লুপ থেকে খাওয়ান; 8N1-এ byte = ১০ বিট; Q15-এ ৩২-বিট গুণফল, rounding আর saturation.

এরপর: এটিই কোর্সের শেষ পর্ব — অভিনন্দন! ৬২টি পর্বে আপনি transistor আর logic gate থেকে শুরু করে adder, flip-flop, ISA, pipeline, cache, virtual memory, multicore আর embedded সিস্টেম পর্যন্ত পুরো পথটি হেঁটেছেন। কোর্স হোম-এ ফিরে পুরো রোডম্যাপ দেখুন, ড্যাশবোর্ডে মডিউল টেস্ট আর কোর্সের সমাপনী পরীক্ষা দিন, আর ধারণাভিত্তিক প্রশ্নের জন্য পর্ব ৬১ আবার দেখে নিন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান