এই পর্বে যা শিখবেন
- n বিটে কতগুলো মান রাখা যায়, unsigned-এর সীমা আর wrap-around কেন modular হিসাব
- ঋণাত্মক সংখ্যা রাখার চার পদ্ধতি — sign-magnitude, one's complement, two's complement, excess-K — আর তাদের তুলনা
- Two's complement কেন সঠিক, আর একই adder কেন দুই অর্থেই কাজ করে — প্রমাণসহ
- Sign extension, zero extension, truncation; carry বনাম overflow flag চেনা
- C-র signed-unsigned তুলনার ফাঁদ, signed overflow-এর undefined behaviour আর নিরাপদ বিকল্প; Ariane 5 আর Year 2038-এর শিক্ষা
১ · বিটের সারি নিজে কিছু বলে না — fixed width আর interpretation
পর্ব ০৪-এ (সংখ্যা পদ্ধতি) আপনি দেখেছেন, যেকোনো ধনাত্মক পূর্ণসংখ্যা binary-তে লেখা যায়। কিন্তু কাগজে লেখা আর CPU-র ভেতরে রাখার মধ্যে একটি বড় পার্থক্য আছে। কাগজে আপনি যত খুশি অঙ্ক লিখতে পারেন। CPU-র register-এর ঘর কিন্তু গোনা — ৮, ১৬, ৩২ বা ৬৪টি বিট। একে বলে fixed-width integer.
দোকানের সস্তা ক্যালকুলেটরের কথা ভাবুন। এর পর্দায় ধরুন ৮টি ঘর। আপনি ৯৯,৯৯৯,৯৯৯-এর সাথে ১ যোগ করলে উত্তর ৯ অঙ্কের হয় — পর্দায় আর জায়গা নেই, তাই “E” দেখায়। CPU-র register-ও এমন একটি সীমিত পর্দা। পার্থক্য হলো, CPU সাধারণত “E” দেখায় না; বাড়তি অংশটি চুপচাপ ফেলে দেয়। এই পর্বের অর্ধেক বিষয়ই এই চুপচাপ ফেলে দেওয়ার ফল।
n বিটে প্রতিটি বিট ০ অথবা ১, তাই মোট 2nটি আলাদা bit pattern সম্ভব। ৮ বিটে ২৫৬টি, ১৬ বিটে ৬৫,৫৩৬টি, ৩২ বিটে প্রায় ৪৩০ কোটি। এর বেশি একটিও নয়। কোন প্যাটার্ন কোন সংখ্যা বোঝাবে, সেটা আমরা ঠিক করি — hardware শুধু প্যাটার্ন রাখে। একই ৮টি বিট 1101 0011 নিচের প্রতিটি নিয়মে আলাদা অর্থ দেয় (প্রতিটি নিয়ম পরের অংশগুলোতে বিস্তারিত দেখবেন):
| যে নিয়মে পড়া হচ্ছে | 1101 0011-এর মান | কীভাবে |
|---|---|---|
| Unsigned | 211 | 128 + 64 + 16 + 2 + 1 |
| Sign-magnitude | −83 | MSB 1 মানে ঋণাত্মক, বাকি 101 0011 = 83 |
| One's complement | −44 | সব বিট উল্টে 0010 1100 = 44, তাই −44 |
| Two's complement | −45 | −128 + 64 + 16 + 2 + 1 |
| Excess-128 | 83 | 211 − 128 |
সবচেয়ে বাঁয়ের বিটকে বলে MSB (most significant bit — সবচেয়ে বেশি ওজনের বিট), আর সবচেয়ে ডানেরটিকে LSB. নিচের ছকে প্রচলিত width আর তাদের সীমা দেওয়া হলো — এই সংখ্যাগুলো বিশ্ববিদ্যালয় ও চাকরির পরীক্ষায় বারবার আসে:
| n | প্যাটার্ন (2n) | Unsigned সীমা | Two's complement সীমা |
|---|---|---|---|
| ৪ | 16 | 0 … 15 | −8 … 7 |
| ৮ | 256 | 0 … 255 | −128 … 127 |
| ১৬ | 65,536 | 0 … 65,535 | −32,768 … 32,767 |
| ৩২ | 4,294,967,296 | 0 … 4,294,967,295 | −2,147,483,648 … 2,147,483,647 |
| ৬৪ | ≈ 1.8 × 1019 | 0 … 264 − 1 | −263 … 263 − 1 (≈ ±9.2 × 1018) |
Type হলো পড়ার চশমা। মেমরিতে শুধু বিট থাকে; int, unsigned, char, float — এগুলো compiler-কে বলে কোন চশমায় পড়তে হবে আর কোন instruction ব্যবহার করতে হবে। ভুল চশমায় পড়লে বিট ঠিক থাকলেও অর্থ ভুল হয়।
২ · Unsigned — 0 থেকে 2n − 1, আর ঘুরে আসা (wrap-around)
সবচেয়ে সরল নিয়ম হলো unsigned integer — চিহ্নহীন সংখ্যা। বিটগুলো bn−1 … b1 b0 হলে মান
সবচেয়ে ছোট মান ০ (সব বিট ০)। সবচেয়ে বড় মান সব বিট ১ হলে: 1 + 2 + 4 + … + 2n−1. এটি একটি গুণোত্তর ধারা, যোগফল 2n − 1. সহজ প্রমাণ: এই সংখ্যার সাথে ১ যোগ করলে সব ঘরে carry ছড়িয়ে পড়ে আর পাওয়া যায় 1 000…0 (১-এর পরে n টি শূন্য), যা ঠিক 2n. তাই ৮ বিটে সর্বোচ্চ 255, ৩২ বিটে 4,294,967,295.
সীমা পেরোলে কী হয় — মিটারের মতো ঘুরে আসা
পুরোনো মোটরসাইকেল বা CNG অটোরিকশার কিলোমিটার মিটারে ৫টি ঘর থাকলে ৯৯৯৯৯-এর পরে আসে ০০০০০। গাড়ি থামেনি, শুধু মিটারের ঘর ফুরিয়েছে — ষষ্ঠ অঙ্কটি রাখার জায়গা নেই। দেয়াল ঘড়িও তাই: ১১টার ৩ ঘণ্টা পরে ২টা, ১৪টা নয়। ঘড়ি mod 12 হিসাব করে।
n বিটের hardware ঠিক এভাবে modular arithmetic করে — প্রতিটি যোগ, বিয়োগ আর গুণের ফল থেকে শুধু নিচের n বিট রাখে, অর্থাৎ ফল mod 2n. ৮ বিটে 255 + 1 = 0, 200 + 100 = 44 (কারণ 300 − 256 = 44), আর 0 − 1 = 255. একে বলে wrap-around. Unsigned যোগে আসল ফল 2n-এর সমান বা বেশি হলে MSB থেকে একটি carry বেরিয়ে যায় — সেই বেরিয়ে যাওয়া carry-ই বলে দেয় যে unsigned overflow হয়েছে। x86 বা ARM-এর মতো CPU সেটি carry flag (C)-এ লিখে রাখে; RISC-V-এ flag নেই, সেখানে তুলনা করে ধরতে হয় (flag-এর circuit পর্ব ১৩ আর ২০-এ)।
Wrap-around সবসময় বাগ নয়। Hash function, checksum, random number generator আর cryptography ইচ্ছা করেই mod 232 বা mod 264 হিসাব ব্যবহার করে, কারণ hardware এটি বিনা খরচে করে দেয়। সমস্যা হয় তখনই, যখন প্রোগ্রামার ধরে নেন সংখ্যাটি “সাধারণ গণিতের” মতো বাড়তেই থাকবে।
৩ · ঋণাত্মক সংখ্যার প্রথম দুই চেষ্টা — sign-magnitude আর one's complement
এবার ঋণাত্মক সংখ্যা, অর্থাৎ signed integer. কাগজে আমরা সামনে একটি “−” চিহ্ন বসাই। কিন্তু বিটের জগতে “−” বলে কিছু নেই, আছে শুধু ০ আর ১। তাই কোনো একটি নিয়মে কিছু প্যাটার্নকে ঋণাত্মক মান হিসেবে ধরতে হয়। ইতিহাসে তিনটি প্রধান নিয়ম চেষ্টা করা হয়েছে, আর একটি চতুর্থ নিয়ম বিশেষ কাজে টিকে আছে।
Sign-magnitude — মানুষের লেখার হুবহু নকল
Sign-magnitude-এ MSB চিহ্ন (০ = ধনাত্মক, ১ = ঋণাত্মক), বাকি n − 1 বিট পরম মান। ৪ বিটে +5 = 0101, −5 = 1101. সীমা −(2n−1 − 1) থেকে +(2n−1 − 1); ৪ বিটে −7 … +7. বোঝা সহজ, কিন্তু দুটি সমস্যা:
- দুটি শূন্য:
0000= +0 আর1000= −0. তুলনার circuit-কে মনে রাখতে হয় যে দুটি আলাদা প্যাটার্ন সমান। - যোগ ঝামেলার: সাধারণ binary যোগে +3 + (−2) =
0011+1010=1101= −5, যা ভুল। ঠিক উত্তর পেতে আগে চিহ্ন দেখতে হয়, চিহ্ন ভিন্ন হলে বড় মান থেকে ছোট মান বিয়োগ করতে হয়, তারপর বড়টির চিহ্ন বসাতে হয় — মানে আলাদা subtractor আর তুলনার circuit.
তবু sign-magnitude মরেনি: IEEE 754 floating point-এ চিহ্নের বিট আলাদা, যা আসলে sign-magnitude চিন্তা (পর্ব ২৩)।
One's complement — সব বিট উল্টে দেওয়া
One's complement-এ −x পেতে x-এর প্রতিটি বিট উল্টানো হয়: −x = ~x (এই পর্বে ~ মানে bitwise NOT, অর্থাৎ প্রতিটি বিট উল্টানো)। ৪ বিটে +5 = 0101, −5 = 1010. সীমা sign-magnitude-এর মতোই −7 … +7, আর এখানেও দুটি শূন্য: 0000 আর 1111.
যোগ প্রায় সাধারণ binary যোগ, শুধু একটি বাড়তি নিয়ম: MSB থেকে carry বেরোলে সেটি ফেলে না দিয়ে আবার LSB-তে যোগ করতে হয় — একে বলে end-around carry. উদাহরণ: (−2) + 5 = 1101 + 0101 = 1 0010. বেরিয়ে যাওয়া ১-টি ঘুরিয়ে এনে 0010 + 1 = 0011 = +3. ঠিক উত্তর, কিন্তু carry-কে দুবার ঘুরতে হয় বলে hardware ধীর আর জটিল।
One's complement আজও প্রতিদিন কাজ করছে — Internet-এর IPv4, TCP আর UDP header-এর checksum one's complement যোগ দিয়ে বানানো, end-around carry-সহ। কম্পিউটার নেটওয়ার্ক কোর্সের Error detection পর্বে এর হাতে-কলমে হিসাব দেখতে পাবেন। এর একটি সুবিধা হলো, byte-এর ক্রম (endianness, পর্ব ০৬) উল্টে গেলেও এই যোগফলের গঠন ঠিক থাকে।
৪ · Two's complement — আজকের প্রায় সব CPU-র নিয়ম (প্রমাণসহ)
Two's complement-এর মূল চিন্তাটি এক লাইনের: MSB-এর ওজন ধনাত্মক নয়, ঋণাত্মক। n বিটে MSB-এর ওজন −2n−1, বাকি সব আগের মতো:
৮ বিটে ওজনগুলো: −128, 64, 32, 16, 8, 4, 2, 1. MSB ০ হলে সংখ্যাটি unsigned-এর মতোই ০ … 127. MSB ১ হলে −128-এর সাথে বাকি সাত বিটের (০ … 127) যোগ, তাই −128 … −1. মোট সীমা −2n−1 … 2n−1 − 1, আর শূন্য মাত্র একটি (0000 0000). দুটি শূন্য না থাকায় একটি প্যাটার্ন বেঁচে যায়, সেটিই বাড়তি ঋণাত্মক সংখ্যা −128 — এই অসমতার কথা অংশ ৭-এ আবার আসবে।
−x = ~x + 1 কেন ঠিক — ছোট্ট প্রমাণ
যেকোনো n বিটের x আর তার উল্টো ~x পাশাপাশি যোগ করুন। প্রতিটি ঘরে একটি ০ আর একটি ১, তাই কোনো carry হয় না, ফল সব-১: x + ~x = 2n − 1. দুই পাশে ১ যোগ করলে x + (~x + 1) = 2n. n বিটের hardware mod 2n হিসাব করে, আর 2n ≡ 0 (mod 2n). অর্থাৎ ~x + 1 এমন একটি সংখ্যা, যা x-এর সাথে যোগ করলে n বিটে ঠিক ০ পাওয়া যায় — এটাই −x-এর সংজ্ঞা। আরেকভাবে বললে, −x রাখা হয় 2n − x প্যাটার্নে; “two's complement” নামটিও এখান থেকে — 2n-এর পরিপূরক।
নিচের stepper-এ −45-এর ৮ বিট রূপ ধাপে ধাপে বের করা হলো। “পরে” চাপুন:
45 = 32 + 8 + 4 + 1 = 101101 (binary)
weight -128 64 32 16 8 4 2 1
+45 0 0 1 0 1 1 0 1 = 0010 1101
x 0010 1101 ~x 1101 0010 (one's complement = -45 there)
~x 1101 0010
+ 1
---------
-45 1101 0011 = 0xD3
weight -128 64 32 16 8 4 2 1
bits 1 1 0 1 0 0 1 1
value -128 +64 +16 +2 +1 = -45
0010 1101 (+45) + 1101 0011 (-45) ----------- 1 0000 0000 carry out is dropped -> 0
- 45-কে binary-তে লেখা। 45 = 32 + 8 + 4 + 1, তাই binary 101101. এটি ৬ বিটের, কিন্তু আমাদের register ৮ বিটের।
- ৮ বিটে পূর্ণ করা। বাঁয়ে দুটি ০ বসিয়ে +45 =
0010 1101. MSB ০, তাই সংখ্যাটি ধনাত্মক — এখানে two's complement আর unsigned একই অর্থ দেয়। - সব বিট উল্টানো। ~x =
1101 0010. One's complement নিয়মে এটিই −45 হতো; two's complement-এ এর সাথে আরও ১ যোগ করতে হবে। - ১ যোগ করা।
1101 0010+ 1 =1101 0011. LSB ০ ছিল বলে কোনো carry ছড়ায়নি। Hex-এ এটি0xD3; unsigned হিসেবে পড়লে 211, আর 256 − 45 = 211 — অর্থাৎ −x রাখা হয়েছে 28 − x প্যাটার্নে। - ওজন দিয়ে মিলিয়ে দেখা। MSB-এর ওজন −128. −128 + 64 + 16 + 2 + 1 = −45. হিসাব মিলেছে।
- চূড়ান্ত পরীক্ষা: x + (−x) = ০। দুটি যোগ করলে ৯ নম্বর বিটে একটি carry বেরোয়, ৮ বিট সব ০। ৮ বিটের register বেরোনো carry রাখে না, তাই ফল ঠিক ০. শর্টকাটও মনে রাখুন: ডান দিক থেকে প্রথম ১ পর্যন্ত বিটগুলো হুবহু রেখে বাকি সব উল্টালেও একই ফল আসে।
একই adder কেন signed আর unsigned দুটোতেই চলে
এটিই two's complement-এর জয়ের আসল কারণ, আর পরীক্ষায় এর প্রমাণ চাওয়া হয়। যেকোনো মান v-এর জন্য তার n বিটের প্যাটার্ন P(v) হলো v mod 2n — unsigned সংখ্যার জন্য এটি সরাসরি সত্য, আর ঋণাত্মক −x-এর প্যাটার্ন 2n − x, যা −x mod 2n. এবার adder শুধু দুটি প্যাটার্ন যোগ করে নিচের n বিট রাখে:
মাঝের সমতাটি modular গণিতের মৌলিক নিয়ম: (a mod m + b mod m) mod m = (a + b) mod m. তাই a আর b-কে unsigned ধরুন বা signed — যতক্ষণ আসল যোগফল a + b সেই নিয়মের সীমার ভেতরে থাকে, adder-এর দেওয়া প্যাটার্নই সঠিক উত্তরের প্যাটার্ন। Hardware-কে জানতেই হয় না সংখ্যাগুলো কোন চশমায় পড়া হবে। বিয়োগও একই adder-এ হয়: a − b = a + ~b + 1 (adder-subtractor circuit পর্ব ২০-এ)। শুধু একটি জিনিস আলাদা — ফল সীমার বাইরে গেল কি না, সেটি চেনার নিয়ম (অংশ ৭)। RISC-V-তে তাই add instruction একটিই, signed আর unsigned-এর আলাদা যোগ নেই।
নিচের simulator ৪ বিটের সব ১৬টি প্যাটার্নকে পাঁচ নিয়মে পড়ে, তারপর কয়েকটি ৮ বিট সংখ্যায় ~x + 1 ধাপে ধাপে দেখায়। N বা তালিকার সংখ্যা বদলে নিজে চালিয়ে দেখুন — Run চাপুন:
// 4-bit patterns under five interpretations, then 8-bit negation -x = ~x + 1.
const N = 4, M = 1 << N; // 16 patterns
const bin = (v, n) => v.toString(2).padStart(n, "0");
const signMag = p => (p >> (N - 1)) ? -(p & (M / 2 - 1)) : p;
const onesC = p => (p >> (N - 1)) ? -(~p & (M - 1)) : p;
const twosC = p => (p >> (N - 1)) ? p - M : p;
const excess = p => p - M / 2; // bias K = 2^(N-1) = 8
const f = v => (Object.is(v, -0) ? "-0" : String(v)).padStart(6);
console.log("bits uns s-mag 1'sC 2'sC ex-8");
for (let p = 0; p < M; p++)
console.log(`${bin(p, N)}${f(p)}${f(signMag(p))}${f(onesC(p))}` +
`${f(twosC(p))}${f(excess(p))}`);
console.log("\n8-bit negation: -x = ~x + 1");
for (const x of [45, 1, 0, 127, -128]) {
const p = x & 0xFF, inv = ~p & 0xFF, r = (inv + 1) & 0xFF;
const s = r >= 128 ? r - 256 : r;
console.log(`${String(x).padStart(4)}: ${bin(p, 8)} ~ ${bin(inv, 8)}` +
` +1 ${bin(r, 8)} = ${s}`);
}আউটপুট (নির্ধারিত — প্রতিবার একই):
bits uns s-mag 1'sC 2'sC ex-8
0000 0 0 0 0 -8
0001 1 1 1 1 -7
0010 2 2 2 2 -6
0011 3 3 3 3 -5
0100 4 4 4 4 -4
0101 5 5 5 5 -3
0110 6 6 6 6 -2
0111 7 7 7 7 -1
1000 8 -0 -7 -8 0
1001 9 -1 -6 -7 1
1010 10 -2 -5 -6 2
1011 11 -3 -4 -5 3
1100 12 -4 -3 -4 4
1101 13 -5 -2 -3 5
1110 14 -6 -1 -2 6
1111 15 -7 -0 -1 7
8-bit negation: -x = ~x + 1
45: 00101101 ~ 11010010 +1 11010011 = -45
1: 00000001 ~ 11111110 +1 11111111 = -1
0: 00000000 ~ 11111111 +1 00000000 = 0
127: 01111111 ~ 10000000 +1 10000001 = -127
-128: 10000000 ~ 01111111 +1 10000000 = -128
তিনটি জিনিস খেয়াল করুন। এক, ওপরের আট সারিতে (MSB ০) চারটি নিয়মের মান একই, পার্থক্য শুধু MSB ১ হলে। দুই, -0 দেখা যাচ্ছে sign-magnitude-এর 1000 আর one's complement-এর 1111-এ — দুটি শূন্যের সমস্যা চোখের সামনে; two's complement কলামে −8 … 7, শূন্য একবারই। তিন, ০-কে negate করলে ~0 + 1-এ carry বেরিয়ে আবার ০ আসে (ঠিক), কিন্তু −128-কে negate করলে আবার −128 আসে — +128 ৮ বিটে ধরে না।
৫ · Biased বা excess-K, আর চার পদ্ধতির পাশাপাশি তুলনা
চতুর্থ নিয়ম excess-K (biased representation). প্যাটার্নকে unsigned হিসেবে পড়ে তা থেকে একটি স্থির সংখ্যা K — যাকে বলে bias — বাদ দেওয়া হয়: মান = U − K. ৪ বিটে K = ৮ নিলে 0000 = −8, 1000 = 0, 1111 = +7.
এর বিশেষ সুবিধা: প্যাটার্নের unsigned ক্রম আর মানের ক্রম হুবহু এক। −8 থেকে +7 পর্যন্ত মানগুলো 0000 থেকে 1111 পর্যন্ত সোজা বাড়ে। তাই দুটি সংখ্যা তুলনা করতে সাধারণ unsigned comparator-ই যথেষ্ট। এজন্য IEEE 754 floating point-এর exponent excess-K রূপে রাখা হয় — single precision-এ K = 127, double-এ 1023 (পর্ব ২৩-এ বিস্তারিত)। খেয়াল করুন: K = 2n−1 হলে excess-K আর two's complement-এর পার্থক্য শুধু MSB-তে — একটির MSB উল্টালেই অন্যটি পাওয়া যায়। ADC (analog-to-digital converter)-এর “offset binary” আউটপুটও আসলে এই নিয়ম।
৪ বিটে −8 থেকে +7 প্রতিটি মান কোন নিয়মে কোন প্যাটার্ন — পরীক্ষার জন্য সবচেয়ে কাজের ছক:
| মান | Sign-magnitude | One's complement | Two's complement | Excess-8 |
|---|---|---|---|---|
| +7 | 0111 | 0111 | 0111 | 1111 |
| +6 | 0110 | 0110 | 0110 | 1110 |
| +5 | 0101 | 0101 | 0101 | 1101 |
| +4 | 0100 | 0100 | 0100 | 1100 |
| +3 | 0011 | 0011 | 0011 | 1011 |
| +2 | 0010 | 0010 | 0010 | 1010 |
| +1 | 0001 | 0001 | 0001 | 1001 |
| +0 | 0000 | 0000 | 0000 | 1000 |
| −0 | 1000 | 1111 | ||
| −1 | 1001 | 1110 | 1111 | 0111 |
| −2 | 1010 | 1101 | 1110 | 0110 |
| −3 | 1011 | 1100 | 1101 | 0101 |
| −4 | 1100 | 1011 | 1100 | 0100 |
| −5 | 1101 | 1010 | 1011 | 0011 |
| −6 | 1110 | 1001 | 1010 | 0010 |
| −7 | 1111 | 1000 | 1001 | 0001 |
| −8 | নেই | নেই | 1000 | 0000 |
| বৈশিষ্ট্য (n বিট) | Sign-magnitude | One's | Two's | Excess-K |
|---|---|---|---|---|
| সীমা | ±(2n−1 − 1) | ±(2n−1 − 1) | −2n−1 … 2n−1 − 1 | −K … 2n − 1 − K |
| শূন্য কয়টি | ২ | ২ | ১ | ১ |
| Negate | MSB উল্টানো | সব বিট উল্টানো | ~x + 1 | 2K − U |
| যোগ | চিহ্ন দেখে আলাদা পথ | end-around carry | সাধারণ adder | যোগের পরে K বিয়োগ |
| আজকের ব্যবহার | float-এর চিহ্ন | Internet checksum | সব integer | float-এর exponent |
৬ · Width বদলানো — sign extension, zero extension আর truncation
প্রোগ্রামে প্রায়ই ছোট width থেকে বড় width-এ যেতে হয়: মেমরি থেকে একটি ৮ বিটের char এনে ৩২ বা ৬৪ বিটের register-এ রাখা, বা int-কে long বানানো। প্রশ্ন হলো, নতুন বাঁয়ের বিটগুলোতে কী বসবে?
- Zero extension: নতুন সব বিটে ০. Unsigned সংখ্যার জন্য ঠিক, কারণ নতুন ঘরগুলোর ওজন যোগ হয় না।
- Sign extension: নতুন সব বিটে MSB-র কপি। Two's complement সংখ্যার জন্য এটিই ঠিক।
MSB কপি করলে মান কেন একই থাকে — প্রমাণ
একবারে মাত্র একটি বিট বাড়ানোর কথা ভাবুন; বাকিটা একই যুক্তি বারবার প্রয়োগ (induction). MSB ০ হলে নতুন বিটও ০, কিছুই বদলায় না। MSB ১ হলে n বিটে মান −2n−1 + R, যেখানে R নিচের n − 1 বিটের মান। n + 1 বিটে নতুন MSB ১ (ওজন −2n), আর পুরোনো MSB-টি এখন সাধারণ ধনাত্মক ঘর (ওজন +2n−1). নতুন মান = −2n + 2n−1 + R = −2n−1 + R. হুবহু আগের মান। অর্থাৎ, একটি −2n−1 ওজনকে ভেঙে “−2n + 2n−1” লেখা হয়েছে মাত্র।
bit 7 6 5 4 3 2 1 0
1 1 0 1 0 0 1 1 = 0xD3 = -45
^ MSB = 1 (negative)
1111 1111 1101 0011 = 0xFFD3
as unsigned: 0xFFD3 = 65491 65491 - 65536 = -45 by weights : -32768 + (16384+...+128) = -32768 + 32640 = -128 (same as old MSB)
0000 0000 1101 0011 = 0x00D3 = 211 (not -45!)
8-bit : 0010 1101 16-bit: 0000 0000 0010 1101 = 45 (sign = zero ext.)
- শুরুর অবস্থা। ৮ বিটে −45 =
1101 0011(প্রথম stepper থেকে)। MSB ১, অর্থাৎ ঋণাত্মক। একে ১৬ বিটের register-এ নিতে হবে। - MSB কপি করা। নতুন ৮টি বাঁয়ের ঘরে MSB-র মান ১ বসানো হলো:
1111 1111 1101 0011=0xFFD3. পুরোনো ৮টি বিট একদম অপরিবর্তিত। - মান একই কি না যাচাই। ১৬ বিট two's complement-এ মান = unsigned মান − 216 = 65491 − 65536 = −45. ওজনের হিসাবেও: নতুন MSB −32768 আর তার পরের আটটি ১-এর যোগ 32640, মিলে −128 — পুরোনো MSB-এর ওজনের সমান। প্রমাণের কথাটিই সংখ্যায় দেখা গেল।
- ভুল করে zero extension করলে। বাঁয়ে ০ বসালে
0x00D3= 211 পাওয়া যায়। −45 হয়ে গেল +211 — signed সংখ্যায় এটি মারাত্মক বাগ। এজন্য CPU-তে দুই রকম load আছে: RISC-V-এlb(sign-extend) আরlbu(zero-extend), x86-এmovsxআরmovzx. - ধনাত্মক সংখ্যায় দুটোই এক। +45-এর MSB ০, তাই sign extension আর zero extension দুটিতেই বাঁয়ে ০ বসে, মান 45 থাকে। পার্থক্য দেখা দেয় শুধু MSB ১ হলে — তাই বাগটিও ধরা পড়ে শুধু ঋণাত্মক বা ১২৮-এর বেশি মানে, যা পরীক্ষায় সহজে বাদ পড়ে যায়।
উল্টো দিক — truncation
বড় width থেকে ছোট width-এ নিলে ওপরের বিটগুলো কেটে ফেলা হয় — একে বলে truncation. ফল হয় মূল মান mod 2k (k = নতুন width). ১৬ বিটের 300 (0000 0001 0010 1100) ৮ বিটে নিলে থাকে 0010 1100 = 44. আর −129 (১৬ বিটে 1111 1111 0111 1111) ৮ বিটে নিলে থাকে 0111 1111 = +127 — চিহ্নই বদলে গেল! নিয়ম: মান নতুন width-এর সীমায় ধরলে truncation মান বদলায় না; না ধরলে বদলাবেই। C-তে ছোট unsigned type-এ রূপান্তর নিয়মমতো mod 2k; ছোট signed type-এ সীমার বাইরের মান রূপান্তর মানদণ্ডে implementation-defined — GCC নথিতে বলে সে mod 2k করে।
৭ · Overflow — unsigned-এর carry বনাম signed-এর V
অংশ ৪-এ দেখেছেন, একই adder দুই অর্থেই কাজ করে — যতক্ষণ ফল সীমার ভেতরে থাকে। সীমা ছাড়ালে কী হয়, আর কীভাবে চেনা যায়?
Unsigned overflow
আসল যোগফল ≥ 2n. চেনার উপায়: MSB থেকে carry বেরোয়, অর্থাৎ C = 1. বিয়োগে উল্টো — ধার (borrow) লাগলে; কোন CPU বিয়োগে C-কে কীভাবে লেখে, তা পর্ব ২০-এ।
8-bit: 200 + 100 → 44Signed overflow
আসল যোগফল −2n−1-এর কম বা 2n−1 − 1-এর বেশি। চেনার উপায়: একই চিহ্নের দুটি সংখ্যা যোগে ভিন্ন চিহ্নের ফল, অর্থাৎ overflow flag V = 1.
8-bit: 100 + 50 → −106ভিন্ন চিহ্নের দুটি সংখ্যা যোগে কখনো signed overflow হয় না। কারণ: একটি ≥ ০ আর একটি < ০ হলে যোগফল দুটি সংখ্যার মাঝখানে থাকে — বড়টির চেয়ে বড় নয়, ছোটটির চেয়ে ছোট নয়। দুটিই সীমার ভেতরে ছিল, তাই যোগফলও ভেতরে। আর একই চিহ্নের দুটি সংখ্যার যোগফল সীমার বাইরে গেলে, সেটি বড়জোর 2 × (−2n−1) = −2n বা 2 × (2n−1 − 1) পর্যন্ত যায়; mod 2n-এ সেটি ঠিক এক “পাক” ঘুরে উল্টো চিহ্নের অর্ধেকে পড়ে। তাই চিহ্ন বদলে যাওয়াই নিশ্চিত লক্ষণ।
V = Cin ⊕ Cout — hardware-এর এক-গেটের নিয়ম
Hardware-এর জন্য “চিহ্ন দেখা” নিয়মটিকে একটি গেটে নামানো যায়। MSB-র ঘরে তিনটি ইনপুট: a7, b7 আর নিচ থেকে আসা carry Cin; আউটপুট sum বিট s7 আর Cout. আটটি ক্ষেত্র দেখুন:
| a7 | b7 | Cin | s7 | Cout | Overflow? | Cin ⊕ Cout |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | না (ধনাত্মক + ধনাত্মক = ধনাত্মক) | 0 |
| 0 | 0 | 1 | 1 | 0 | হ্যাঁ (ধনাত্মক + ধনাত্মক = ঋণাত্মক) | 1 |
| 0 | 1 | 0 | 1 | 0 | না (ভিন্ন চিহ্ন) | 0 |
| 0 | 1 | 1 | 0 | 1 | না (ভিন্ন চিহ্ন) | 0 |
| 1 | 0 | 0 | 1 | 0 | না (ভিন্ন চিহ্ন) | 0 |
| 1 | 0 | 1 | 0 | 1 | না (ভিন্ন চিহ্ন) | 0 |
| 1 | 1 | 0 | 0 | 1 | হ্যাঁ (ঋণাত্মক + ঋণাত্মক = ধনাত্মক) | 1 |
| 1 | 1 | 1 | 1 | 1 | না (ঋণাত্মক + ঋণাত্মক = ঋণাত্মক) | 0 |
“Overflow?” কলাম আর শেষ কলাম আটটি সারিতেই মিলে যায় — এটাই প্রমাণ (সম্পূর্ণ ক্ষেত্র-বিশ্লেষণ বা truth table দিয়ে)। সমতুল্য আরেকটি রূপ, যা চিহ্নের বিট থেকে সরাসরি লেখা যায়: V = a7·b7·s7′ + a7′·b7′·s7 (এখানে ′ মানে উল্টো বা NOT). এখন সবচেয়ে বিখ্যাত উদাহরণটি বিট ধরে ধরে দেখুন:
bit 7 6 5 4 3 2 1 0 a 0 1 1 1 1 1 1 1 (+127) b 0 0 0 0 0 0 0 1 (+1)
bit 7 6 5 4 3 2 1 0 carry-in ? 1 1 1 1 1 1 0 a 0 1 1 1 1 1 1 1 b 0 0 0 0 0 0 0 1 sum ? 0 0 0 0 0 0 0
bit 7 6 5 4 3 2 1 0 carry-in 1 1 1 1 1 1 1 0 a 0 1 1 1 1 1 1 1 b 0 0 0 0 0 0 0 1 sum 1 0 0 0 0 0 0 0 carry-out of bit 7 = 0
127 + 1 = 128 -> pattern 1000 0000 = 128 C = 0, correct
V = Cin(bit7) xor Cout(bit7) = 1 xor 0 = 1 +127 + +1 -> 1000 0000 = -128 overflow! sign rule: (+) + (+) gave (-)
- দুটি ধনাত্মক সংখ্যা। a =
0111 1111= +127 (৮ বিট signed-এর সর্বোচ্চ), b =0000 0001= +1. দুটির MSB-ই ০. - নিচের সাত ঘর। bit 0-এ 1 + 1 = 10, তাই sum 0, carry 1. এরপর bit 1 থেকে bit 6 প্রতিটিতে 1 + 0 + carry 1 = 10 — sum 0, carry আবার ১. Carry ঘর থেকে ঘরে ছড়িয়ে যাচ্ছে (ripple carry, পর্ব ১১)।
- MSB-এর ঘর। bit 7-এ 0 + 0 + carry-in 1 = 1, carry-out 0. পুরো ফল
1000 0000. অর্থাৎ MSB-তে carry ঢুকেছে (Cin = 1) কিন্তু বেরোয়নি (Cout = 0). - Unsigned অর্থে ঠিক। MSB থেকে carry বেরোয়নি, তাই C = 0. Unsigned হিসেবে 127 + 1 = 128, আর
1000 0000unsigned-এ ঠিক 128. কোনো ভুল নেই। - Signed অর্থে overflow. V = Cin ⊕ Cout = 1 ⊕ 0 = 1. দুটি ধনাত্মক যোগে ফলের MSB ১, অর্থাৎ −128 — চিহ্নের নিয়মও একই কথা বলছে। আসল উত্তর +128, যা ৮ বিট signed-এ ধরে না। একই বিট, একই adder; এক চশমায় ঠিক, অন্য চশমায় ভুল।
এবার আরও কয়েকটি ক্লাসিক জোড়া। নিচের simulator একটি ৮ বিটের ripple adder — প্রতিটি বিটে full adder-এর সূত্র চালায়, MSB-তে ঢোকা carry মনে রাখে, তারপর C আর V ছাপে আর দুই চশমায় উত্তর ঠিক কি না বলে। (বিয়োগ 60 − 90 এখানে 60 + (−90) হিসেবে যোগ করা হয়েছে।)
// 8-bit ripple adder: sum bits, carry out C and signed overflow V.
const bin8 = v => v.toString(2).padStart(8, "0");
const s8 = p => (p & 0x80) ? p - 256 : p; // pattern -> signed value
function add8(a, b) { // a, b: patterns 0..255
let carry = 0, sum = 0, cin7 = 0;
for (let i = 0; i < 8; i++) {
const x = (a >> i) & 1, y = (b >> i) & 1;
if (i === 7) cin7 = carry; // carry INTO the MSB
sum |= (x ^ y ^ carry) << i;
carry = (x & y) | (x & carry) | (y & carry);
}
return { sum, C: carry, V: cin7 ^ carry };
}
const tests = [[100, 50], [-100, -50], [127, 1], [-128, -1],
[60, -90], [-56, 100], [-1, 1]];
for (const [x, y] of tests) {
const a = x & 0xFF, b = y & 0xFF, { sum, C, V } = add8(a, b);
const uOk = a + b === sum ? "ok" : "WRONG";
const sOk = x + y === s8(sum) ? "ok" : "WRONG";
console.log(`${x} + ${y}: ${bin8(a)} + ${bin8(b)} = ${bin8(sum)} C=${C} V=${V}`);
console.log(` unsigned ${a}+${b} -> ${sum} ${uOk};` +
` signed ${x}+${y} -> ${s8(sum)} ${sOk}`);
}আউটপুট:
100 + 50: 01100100 + 00110010 = 10010110 C=0 V=1
unsigned 100+50 -> 150 ok; signed 100+50 -> -106 WRONG
-100 + -50: 10011100 + 11001110 = 01101010 C=1 V=1
unsigned 156+206 -> 106 WRONG; signed -100+-50 -> 106 WRONG
127 + 1: 01111111 + 00000001 = 10000000 C=0 V=1
unsigned 127+1 -> 128 ok; signed 127+1 -> -128 WRONG
-128 + -1: 10000000 + 11111111 = 01111111 C=1 V=1
unsigned 128+255 -> 127 WRONG; signed -128+-1 -> 127 WRONG
60 + -90: 00111100 + 10100110 = 11100010 C=0 V=0
unsigned 60+166 -> 226 ok; signed 60+-90 -> -30 ok
-56 + 100: 11001000 + 01100100 = 00101100 C=1 V=0
unsigned 200+100 -> 44 WRONG; signed -56+100 -> 44 ok
-1 + 1: 11111111 + 00000001 = 00000000 C=1 V=0
unsigned 255+1 -> 0 WRONG; signed -1+1 -> 0 ok
প্রতিটি সারিতে C=1 ঠিক তখনই, যখন unsigned উত্তর WRONG, আর V=1 ঠিক তখনই, যখন signed উত্তর WRONG — দুটি flag দুটি আলাদা প্রশ্নের উত্তর। চার রকম ক্ষেত্রই আছে: 100 + 50-এ শুধু signed ভাঙে; −56 + 100 (অর্থাৎ বিটে 200 + 100) আর −1 + 1-এ শুধু unsigned ভাঙে; −100 + −50 আর −128 + −1-এ দুটোই ভাঙে; 60 + −90-এ কোনোটিই না। −1 + 1-এ C = 1 হলেও signed উত্তর 0 একদম ঠিক — signed হিসাবে C-কে overflow ভাবা একটি প্রচলিত ভুল।
অসমতার ফাঁদ — −(−128) আর INT_MIN
Two's complement-এ ঋণাত্মক দিকে একটি মান বেশি: ৮ বিটে −128 আছে, +128 নেই। তাই −(−128) হিসাব করলে ~1000 0000 + 1 = 0111 1111 + 1 = 1000 0000 — আবার −128 (প্রথম simulator-এর শেষ সারি)। ৩২ বিট int-এ একই ঘটনা INT_MIN = −2,147,483,648-এর সাথে: -INT_MIN, abs(INT_MIN), এমনকি INT_MIN / -1 — তিনটিরই আসল উত্তর +2,147,483,648, যা int-এ ধরে না। C-তে তিনটিই undefined behaviour; x86-64-এ INT_MIN / -1 সাধারণত hardware exception দিয়ে প্রোগ্রাম থামিয়ে দেয়, যদিও শূন্য দিয়ে ভাগ হয়নি। “একটি সংখ্যার পরম মান সবসময় ধনাত্মক” — integer-এর জগতে কথাটি সত্য নয়।
৮ · C ভাষায় integer — রূপান্তরের নিয়ম, ফাঁদ আর undefined behaviour
Hardware-এর নিয়ম সহজ: বিট যোগ হয়, flag ওঠে। কিন্তু C ভাষা এর ওপর নিজের কিছু নিয়ম বসিয়েছে, আর বাস্তবের বহু বাগ ও নিরাপত্তা-ত্রুটি এই নিয়মের ফাঁক থেকে আসে। (C-র bitwise operator-এর বিস্তারিত পাবেন C কোর্সের Bitwise operations পর্বে, আর type রূপান্তরের নিয়ম Type conversion পর্বে।)
ফাঁদ ১: -1 > 0u সত্য
C-তে কোনো operator-এর দুই পাশে ভিন্ন type থাকলে আগে usual arithmetic conversions হয়। তার আগে char, short-এর মতো ছোট type integer promotion-এ int হয়ে যায়। এরপর নিয়মটি মোটামুটি এমন: একই “rank”-এর (একই আকারের) int আর unsigned int মিললে int-টি unsigned-এ রূপান্তরিত হয়। আর −1-কে unsigned-এ নিলে পাওয়া যায় 232 − 1 = 4,294,967,295 (mod 232). ফলে -1 > 0u আসলে তুলনা করে 4294967295 > 0 — সত্য।
আরও সূক্ষ্ম অংশ: ফল platform-এর ওপর নির্ভর করে। -1L > 0u (long বনাম unsigned int) x86-64 Linux-এ মিথ্যা, কারণ সেখানে long ৬৪ বিটের আর সব unsigned int মান ধরতে পারে, তাই unsigned-টিই long হয়ে যায়। কিন্তু ৬৪ বিট Windows-এ long ৩২ বিটের, তাই একই তুলনা সত্য! sizeof আর strlen ফেরত দেয় size_t, যা unsigned — তাই if (strlen(a) - strlen(b) < 0) কখনো সত্য হয় না।
ফাঁদ ২: কখনো শেষ না হওয়া লুপ
/* উদাহরণ — bug: i is unsigned, so i >= 0 is always true */
for (unsigned i = n; i >= 0; i--)
process(a[i]); /* after i = 0, i-- gives 4294967295 */
/* fix: test before decrementing */
for (size_t i = n; i-- > 0; )
process(a[i]); /* visits n-1 ... 0, then stops */
প্রথম লুপে i = 0-এর পরে i-- wrap করে সর্বোচ্চ মানে চলে যায়, শর্ত আবার সত্য, আর a[4294967295] পড়তে গিয়ে প্রোগ্রাম সাধারণত crash করে। GCC-তে -Wextra চালু থাকলে “comparison of unsigned expression in ‘>= 0’ is always true” ধরনের warning দেয় — warning চালু রাখার আরেকটি কারণ।
ফাঁদ ৩: signed overflow — undefined behaviour
C মানদণ্ড বলে: unsigned হিসাব সবসময় mod 2n — wrap করা নিয়মসিদ্ধ। কিন্তু signed হিসাবের ফল সীমার বাইরে গেলে সেটি undefined behaviour — মানদণ্ড কোনো ফলের প্রতিশ্রুতি দেয় না। হার্ডওয়্যার তো wrap-ই করত; তাহলে সমস্যা কোথায়? সমস্যা হলো optimizer (compiler-এর যে অংশ কোড দ্রুত করে) ধরে নেয় UB কখনো ঘটে না, আর সেই ধারণা থেকে কোড বদলে দেয়। নিচের overflow-যাচাইটি দেখুন:
/* উদাহরণ — a naive overflow check */
int will_overflow(int x) {
return x + 100 < x; /* "if it wrapped, the sum is smaller" */
}
; what gcc -O2 -masm=intel typically emits on x86-64 (Intel syntax, illustration)
will_overflow:
xor eax, eax ; always return 0
ret
Compiler-এর যুক্তি: “signed overflow হতেই পারে না, তাই x + 100 সবসময় x-এর চেয়ে বড়, তাই ফল সবসময় 0.” পাহারাদার শর্তটি পুরোপুরি মুছে গেল। নিরাপদ বিকল্প:
- যোগের আগেই যাচাই:
if (b > 0 && a > INT_MAX - b)বাif (b < 0 && a < INT_MIN - b)— এখানে কোনো overflow ঘটে না। - Compiler-এর built-in: GCC আর Clang-এর
__builtin_add_overflow(a, b, &r)ফল r-এ (wrap করা রূপে) রাখে আর overflow হলে true ফেরত দেয়; C23 মানদণ্ডে একই কাজের জন্য<stdckdint.h>-এরckd_addএসেছে। - বড় type-এ হিসাব: দুটি ৩২ বিট int-কে
int64_t-এ যোগ করলে overflow হয় না; তারপর সীমা যাচাই। - যাচাইয়ের টুল:
-fsanitize=undefined(UBSan) চলার সময় signed overflow ধরে জানায়;-fwrapvsigned হিসাবকে নিয়মমতো wrap করতে বাধ্য করে।
আরও বিস্তারিত পাবেন C কোর্সের Undefined behavior ও নিরাপদ কোড পর্বে। নিচের প্রোগ্রাম চারটি ঘটনা দেখায় — কিন্তু কোথাও signed overflow ঘটায় না:
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
int main(void) {
unsigned int u = UINT_MAX;
printf("UINT_MAX = %u\n", u);
printf("UINT_MAX + 1u = %u\n", u + 1u); /* defined: wraps mod 2^32 */
int neg = -1;
unsigned int zero = 0u;
/* what C silently does in (neg > zero): int -> unsigned int */
printf("(unsigned)-1 = %u\n", (unsigned int)neg);
printf("-1 > 0u = %d\n", (unsigned int)neg > zero);
printf("-1 > 0 (int) = %d\n", neg > (int)zero);
int8_t b = -45;
int32_t w = b; /* sign extension */
uint32_t z = (uint8_t)b; /* zero extension */
printf("int8_t -45 = 0x%02X\n", (unsigned int)(uint8_t)b);
printf("sign-extended = 0x%08X = %d\n", (unsigned int)w, (int)w);
printf("zero-extended = 0x%08X = %u\n", (unsigned int)z, (unsigned int)z);
int r;
if (__builtin_add_overflow(INT_MAX, 1, &r)) /* no UB is executed */
printf("INT_MAX + 1 : overflow caught, wrapped r = %d\n", r);
return 0;
}gcc-তে (x86-64 Linux) আউটপুট:
UINT_MAX = 4294967295
UINT_MAX + 1u = 0
(unsigned)-1 = 4294967295
-1 > 0u = 1
-1 > 0 (int) = 0
int8_t -45 = 0xD3
sign-extended = 0xFFFFFFD3 = -45
zero-extended = 0x000000D3 = 211
INT_MAX + 1 : overflow caught, wrapped r = -2147483648
প্রথম দুই লাইন unsigned wrap: সর্বোচ্চ মানের পরে ০। তৃতীয়-চতুর্থ লাইনে আমরা নিজে হাতে সেই রূপান্তর লিখেছি, যা C neg > zero লিখলে চুপচাপ করত: −1 হয়ে যায় 4294967295, তাই তুলনা 1 (সত্য)। কাস্টটি স্পষ্ট লেখায় compiler-এর -Wsign-compare warning আসে না, আর পাঠকের কাছেও উদ্দেশ্য পরিষ্কার থাকে; int হিসেবে তুলনা করলে (পঞ্চম লাইন) স্বাভাবিক উত্তর 0. পরের তিন লাইনে stepper ২-এর ঘটনাই ৩২ বিটে: 0xD3 sign-extend হয়ে 0xFFFFFFD3 (−45), zero-extend হয়ে 0x000000D3 (211). শেষ লাইনে __builtin_add_overflow overflow ধরেছে আর r-এ wrap করা মান −2147483648 রেখেছে — প্রোগ্রামে কোথাও signed overflow-এর UB ঘটেনি।
Shell-এর হিসাবেও fixed width আছে। Bash-এর $(( )) ৬৪ বিট signed integer (C-র intmax_t) দিয়ে হিসাব করে; bash-এর manual নিজেই বলে, overflow যাচাই করা হয় না:
# bash arithmetic: 64-bit signed integers, no overflow check
echo "max = $(( 9223372036854775807 ))"
echo "max + 1 = $(( 9223372036854775807 + 1 ))"
echo "min - 1 = $(( -9223372036854775808 - 1 ))"
echo "2**63 = $(( 2**63 ))"
echo "2**64 = $(( 2**64 ))"
x=$(( (100 + 50) & 0xFF )) # keep only the low 8 bits
echo "8-bit 100+50: pattern $x, signed $(( x >= 128 ? x - 256 : x ))"আউটপুট:
max = 9223372036854775807
max + 1 = -9223372036854775808
min - 1 = 9223372036854775807
2**63 = -9223372036854775808
2**64 = 0
8-bit 100+50: pattern 150, signed -106
263 − 1-এর পরে সরাসরি −263: এটিই ৬৪ বিট two's complement-এর wrap. 264 হিসাব করতে গিয়ে ফল ০ — mod 264-এ 264 ≡ 0. শেষ লাইনে & 0xFF দিয়ে ৮ বিট রেখে আবার দেখা গেল 100 + 50 signed ৮ বিটে −106 (adder simulator-এর প্রথম সারির মতো)। তাই shell script-এ বড় ফাইলের আকার বা টাকার হিসাব করার সময়ও সীমার কথা মনে রাখতে হয়।
৯ · বাস্তবের দুর্ঘটনা — Ariane 5, Gangnam Style আর Year 2038
এতক্ষণের নিয়মগুলো শুধু পরীক্ষার বিষয় নয়। নিচের তিনটি ঘটনা সবচেয়ে বেশি উদ্ধৃত হয়; প্রতিটি থেকে আলাদা শিক্ষা।
Ariane 5, Flight 501 (৪ জুন ১৯৯৬): ইউরোপের নতুন Ariane 5 রকেট উৎক্ষেপণের প্রায় ৩৭ সেকেন্ড পরে পথ হারিয়ে ভেঙে পড়ে এবং ধ্বংস হয়। তদন্ত বোর্ডের প্রতিবেদন অনুযায়ী inertial reference system (রকেটের অবস্থান আর গতি মাপার ব্যবস্থা)-এর software (Ariane 4 থেকে প্রায় হুবহু নেওয়া, Ada ভাষায় লেখা) একটি ৬৪ বিট floating-point মানকে ১৬ বিট signed integer-এ রূপান্তর করছিল। Ariane 5 অনেক দ্রুত ওঠে, তাই horizontal velocity-সংক্রান্ত মানটি ১৬ বিট signed-এর সর্বোচ্চ 32,767 ছাড়িয়ে যায়। রূপান্তরে exception হয়, সেটি সামলানো ছিল না; প্রধান ও backup দুটি unit-ই একই কারণে বন্ধ হয়ে যায়, আর flight computer ভুল তথ্য পেয়ে রকেট ঘুরিয়ে ফেলে। মজার ব্যাপার, ওই অংশের হিসাবটি উৎক্ষেপণের পরে দরকারই ছিল না।
Gangnam Style আর YouTube-এর counter (ডিসেম্বর ২০১৪): PSY-এর “Gangnam Style” ভিডিওর ভিউ ৩২ বিট signed integer-এর সীমা 2,147,483,647-এর কাছে পৌঁছালে YouTube জানায়, তারা view counter ৬৪ বিটে নিয়ে গেছে — আর রসিকতা করে বলে, কোনো ভিডিও এত বার দেখা হবে তা ভাবা হয়নি। প্রতিবেদন অনুযায়ী পরিবর্তনটি আগেই করা হয়েছিল, তাই counter আসলে ভাঙেনি; ঘোষণাটির খানিকটা ছিল মজা। তবু এটি দেখায়, “এত বড় সংখ্যা কখনো আসবে না” ধারণাটি কত দ্রুত পুরোনো হয়।
Year 2038 problem: Unix-এর time_t ১৯৭০ সালের ১ জানুয়ারি 00:00:00 UTC থেকে সেকেন্ড গোনে। ৩২ বিট signed হলে সর্বোচ্চ 231 − 1 = 2,147,483,647 সেকেন্ড, যা পূর্ণ হয় ১৯ জানুয়ারি ২০৩৮, 03:14:07 UTC-তে (বাংলাদেশ সময় সকাল ৯টা ১৪ মিনিট ৭ সেকেন্ড)। এক সেকেন্ড পরে wrap করে মান হয় −231, অর্থাৎ ১৩ ডিসেম্বর ১৯০১ 20:45:52 UTC. একে বলে Year 2038 problem. ৬৪ বিট Linux, Windows আর macOS-এ time_t অনেক আগে থেকেই ৬৪ বিট, আর Linux kernel ও glibc ৩২ বিট সিস্টেমের জন্যও ৬৪ বিট সময়ের ব্যবস্থা যোগ করেছে। ঝুঁকি রয়ে গেছে পুরোনো embedded যন্ত্র, ফাইল ফরম্যাট আর ডেটাবেসের ৩২ বিট timestamp ঘরে।
তিনটি ঘটনার সাধারণ সূত্র: কেউ একজন একটি মানের সীমা ধরে নিয়েছিলেন — “এই বেগ ১৬ বিটে ধরবে”, “ভিউ ২০০ কোটি ছাড়াবে না”, “এই software ২০৩৮ পর্যন্ত চলবে না।” Integer-এর width বেছে নেওয়া একটি প্রকৌশল-সিদ্ধান্ত; সেটি লিখে রাখুন, যাচাই করুন, আর সীমা ছাড়ালে কী হবে তার পরিকল্পনা রাখুন।
আরও গভীরে: RISC-V-তে x86 বা ARM-এর মতো কোনো flag register নেই; overflow জানতে হলে বাড়তি instruction লাগে (যেমন unsigned যোগের পর sltu দিয়ে ফল আর operand তুলনা), আর RV64-এর addw-এর মতো “W” instruction ৩২ বিট ফলকে ৬৪ বিটে sign-extend করে রাখে। ARM-এর NZCV আর x86-এর CF/OF flag ঠিক এই পর্বের C আর V. ছবি, অডিও আর DSP-র কাজে wrap-এর বদলে saturating arithmetic বেশি কাজের — সাদা পিক্সেলে আলো বাড়ালে কালো হয়ে যাওয়ার বদলে সাদাই থাকে — তাই x86-এর SSE2 (paddsb) আর ARM-এর qadd-এর মতো সীমায় আটকে থাকা যোগের instruction আছে।
১০ · AI-এর যুগে integer-এর বিট-রূপ
AI টুল two's complement-এর সংজ্ঞা নির্ভুল বলতে পারে, কিন্তু সীমানার ক্ষেত্রগুলোতে প্রায়ই হোঁচট খায় — আর বাগ থাকে ঠিক সেখানেই। কয়েকটি সাধারণ ভুল:
| AI-এর প্রচলিত ভুল | ঠিক কথা | কীভাবে যাচাই |
|---|---|---|
| ৮ বিট signed সীমা “−127 … 127” | −128 … 127 (two's complement) | সীমার সূত্র −2n−1 … 2n−1 − 1 |
| Signed যোগে “carry হলে overflow” | Signed-এ V দেখতে হয়; −1 + 1-এ C = 1 কিন্তু ফল ঠিক | adder simulator চালিয়ে |
x + 1 < x দিয়ে signed overflow যাচাই | এটি UB-নির্ভর; optimizer মুছে দিতে পারে | Compiler Explorer-এ -O2 assembly দেখে |
“abs() সবসময় ধনাত্মক” | abs(INT_MIN) UB | INT_MIN দিয়ে পরীক্ষা, UBSan |
Sign extension আর zero extension গুলিয়ে ফেলা, lb/lbu বা MIPS-RISC-V নিয়ম মেশানো | Signed-এ MSB কপি, unsigned-এ ০ | ISA manual, hex-এ ছাপিয়ে |
AI-কে প্রশ্ন করার সময় width, নিয়ম আর প্রতিটি ধাপ চেয়ে নিন, যাতে আপনি নিজে মেলাতে পারেন:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- −45-এর ৮ বিট two's complement কত? —
1101 0011(0xD3). - n বিট two's complement-এর সীমা? — −2n−1 থেকে 2n−1 − 1; ৮ বিটে −128 … 127.
- Signed overflow কীভাবে চেনা যায়? — একই চিহ্নের দুটি operand-এর যোগফলের চিহ্ন ভিন্ন হলে; hardware-এ V = Cin ⊕ Cout (MSB).
- Two's complement কেন sign-magnitude-এর চেয়ে ভালো? — শূন্য একটি, আর signed-unsigned যোগ-বিয়োগে একই adder চলে।
- C-তে
-1 > 0u-এর মান? — 1 (সত্য), কারণ −1 unsigned-এ রূপান্তরিত হয়ে UINT_MAX হয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১একসময় বড় বড় কম্পিউটার one's complement বা sign-magnitude ব্যবহার করত। শেষ পর্যন্ত সবাই two's complement-এ এলো কেন?
১৯৫০ আর ১৯৬০-এর দশকে কোনো একক নিয়ম ছিল না। IBM 704/7090 পরিবারের মতো যন্ত্র integer রাখত sign-magnitude-এ, কারণ মানুষ কাগজে এভাবেই লেখে, আর দশমিক যন্ত্রের যুগ থেকে আসা প্রকৌশলীদের কাছে এটিই স্বাভাবিক লাগত। CDC 6600 (Seymour Cray-এর নকশা, ১৯৬৪) আর UNIVAC 1100 পরিবার ব্যবহার করত one's complement; সেখানে negate করা মানে শুধু সব বিট উল্টানো, যা এক ধাপে হয়।
তাহলে two's complement জিতল কেন? তিনটি কারণ এই পর্বেই দেখেছেন। প্রথম, একটি শূন্য — দুটি শূন্য মানে প্রতিটি “সমান কি না” যাচাইয়ে বাড়তি যুক্তি, আর সফটওয়্যারে −0 নিয়ে অদ্ভুত বাগ। দ্বিতীয়, একটি adder — signed আর unsigned যোগ-বিয়োগ একই circuit-এ, end-around carry-র মতো বাড়তি পথ নেই; বহু-শব্দের (multi-word) যোগেও carry সোজা পরের শব্দে যায়। তৃতীয়, transistor সস্তা হওয়ার পর negate-এর বাড়তি “+1” খরচ নগণ্য হয়ে গেল, কারণ adder-এর carry-in ইনপুটেই সেটি বিনা খরচে ঢোকানো যায় (পর্ব ২০)।
মোড় ঘোরানো ঘটনা ছিল ১৯৬৪ সালের IBM System/360 — এটি binary integer-এর জন্য two's complement নেয়, আর বিশাল বাজারের কারণে পরের যন্ত্রগুলো তা অনুসরণ করে। ১৯৭০-এর DEC PDP-11, পরে Intel-এর microprocessor — সবাই two's complement. ভাষার মানদণ্ডও শেষে বাস্তবতা মেনে নিয়েছে: দীর্ঘদিন C আর C++ তিনটি নিয়মই অনুমোদন করত, কিন্তু C++20 আর C23 signed integer-এর জন্য two's complement বাধ্যতামূলক করেছে। মজার ব্যাপার, overflow এখনো UB-ই রয়ে গেছে — রূপ নির্দিষ্ট হলেও হিসাবের প্রতিশ্রুতি বদলায়নি।
মূল উপলব্ধি: সবচেয়ে “স্বাভাবিক” নকশা সবসময় সেরা নয়। Two's complement মানুষের কাছে কম সহজ, কিন্তু hardware-এর কাছে সবচেয়ে সরল — আর দীর্ঘমেয়াদে সরল hardware-ই জেতে।
প্র ০২Ariane 5-এর দুর্ঘটনা কি শুধু একটি “overflow বাগ”? এখান থেকে প্রকৌশলের কী কী শিক্ষা নেওয়া উচিত?
শুধু overflow বললে গল্পের অর্ধেক বলা হয়। তদন্ত বোর্ডের (প্রধান ছিলেন ফরাসি গণিতবিদ Jacques-Louis Lions) প্রতিবেদনে কয়েকটি স্তরের ভুল উঠে আসে। প্রথম স্তর প্রযুক্তিগত: ৬৪ বিট floating-point থেকে ১৬ বিট signed integer-এ রূপান্তরে মান 32,767 ছাড়িয়ে যায়। Ada ভাষা এমন রূপান্তরে exception তোলে — অর্থাৎ ভাষা ভুলটি ধরেছিল — কিন্তু ওই নির্দিষ্ট ভেরিয়েবলের জন্য সুরক্ষা ইচ্ছা করে বাদ রাখা হয়েছিল, কারণ processor-এর কাজের চাপ একটি সীমার নিচে রাখার লক্ষ্য ছিল, আর Ariane 4-এর পথে মানটি সীমা ছাড়াত না বলে যুক্তি দেওয়া হয়েছিল।
দ্বিতীয় স্তর নকশাগত: Ariane 4-এর জন্য যাচাই করা software নতুন রকেটে প্রায় হুবহু নেওয়া হয়, অথচ Ariane 5-এর গতিপথ আলাদা, তাই ধারণাটি আর সত্য ছিল না। পুরোনো ধারণাগুলো নতুন প্রেক্ষাপটে আবার যাচাই হয়নি। তৃতীয় স্তর ব্যবস্থাগত: backup unit-এ একই software, তাই একই ইনপুটে সেটিও একইভাবে বন্ধ হয় — redundancy শুধু hardware-এর ভাঙন থেকে বাঁচায়, একই software-এর ভুল থেকে নয়। আর exception হলে unit বন্ধ করে দেওয়ার নীতি ধরে নিয়েছিল ভুল মানে hardware নষ্ট, software-এর ভুল নয়। চতুর্থত, পুরো ব্যবস্থাকে বাস্তব গতিপথের ডেটা দিয়ে একসাথে পরীক্ষা করা হয়নি।
আজকের সফটওয়্যারে এর প্রতিধ্বনি সর্বত্র: পুরোনো library নতুন কাজে নেওয়া, “এই মান কখনো এত বড় হবে না” লেখা মন্তব্য, বা AI-এর দেওয়া কোড অন্য প্রেক্ষাপটে বসানো। প্রতিবেদনের সুপারিশের মূল কথা ছিল — কোন মান কোন সীমায় থাকবে তা স্পষ্ট লিখে রাখা, আর সেই ধারণা যাচাই করা।
মূল উপলব্ধি: Overflow একটি বিটের সমস্যা, কিন্তু দুর্ঘটনা হয় ধারণার সমস্যায়। কোডের প্রতিটি width-এর পেছনে একটি অনুমান থাকে; অনুমানটি নথিভুক্ত না থাকলে কেউ জানবে না কখন সেটি ভেঙে গেছে।
প্র ০৩Hardware তো signed যোগেও wrap করে — তাহলে C মানদণ্ড signed overflow-কে undefined behaviour রেখেছে কেন? এটা কি ভালো সিদ্ধান্ত?
ঐতিহাসিক কারণটি এই পর্বের অংশ ৩-এ: যখন C মানদণ্ড লেখা হয় (১৯৮৯), তখনো one's complement আর sign-magnitude যন্ত্র ছিল, আর কিছু যন্ত্র overflow হলে trap করত (সাথে সাথে exception তুলত)। প্রতিটি যন্ত্রে আলাদা আচরণ, তাই মানদণ্ড কোনো একটি ফল বাধ্যতামূলক না করে বলে দিল — “এটি করবেন না।” Unsigned-এর বেলায় সব যন্ত্রে mod 2n স্বাভাবিক ছিল, তাই সেটি নির্দিষ্ট করা হলো।
পরে এই “অনির্দিষ্টতা” compiler-এর হাতে একটি শক্তিশালী হাতিয়ার হয়ে দাঁড়ায়। for (int i = 0; i < n; i++) a[i] = … লুপে compiler ধরে নিতে পারে i কখনো wrap করবে না, তাই ৬৪ বিট যন্ত্রে i-কে সরাসরি ৬৪ বিট pointer offset হিসেবে রাখা, লুপের মোট চক্র আগেই গোনা বা vectorize (SIMD দিয়ে একসাথে কয়েকটি ঘর হিসাব) করা যায়। x * 2 / 2-কে x বানানো, x + 1 > x-কে সত্য ধরা — এগুলো বাস্তব benchmark-এ গতি দেয়।
কিন্তু দামও বড়। অংশ ৮-এর মতো overflow-যাচাই নীরবে মুছে যায়, আর বহু নিরাপত্তা-ত্রুটির পেছনে integer overflow থেকে ভুল আকারের buffer বরাদ্দ রয়েছে। তাই অনেক বড় প্রজেক্ট সাবধানী পথ নেয়: Linux kernel -fno-strict-overflow দিয়ে build হয়, যাতে signed wrap-কে নিয়মসিদ্ধ ধরা হয়। অন্য ভাষাগুলো আলাদা সিদ্ধান্ত নিয়েছে: Java-তে int overflow সবসময় wrap (নির্দিষ্ট আচরণ); Rust debug build-এ overflow হলে panic করে (প্রোগ্রাম ত্রুটি দেখিয়ে থেমে যায়) আর release build-এ ডিফল্টভাবে wrap করে, সাথে checked_add, wrapping_add, saturating_add-এর মতো স্পষ্ট পদ্ধতি দেয়; C23-এ এসেছে ckd_add.
ভালো না খারাপ — উত্তর নির্ভর করে আপনি কী চান। Benchmark-এর কয়েক শতাংশ গতি, নাকি পূর্বানুমেয় আচরণ? আধুনিক প্রবণতা হলো “ডিফল্টভাবে নিরাপদ, দরকার হলে দ্রুত” — অর্থাৎ wrap বা trap স্পষ্ট করে লিখতে দেওয়া।
মূল উপলব্ধি: Undefined behaviour মানে “hardware যা করবে তাই” নয়; মানে “compiler ধরে নেবে এটা কখনো ঘটবে না।” এই পার্থক্য না বুঝলে নিখুঁত দেখতে কোডও optimizer-এর হাতে বদলে যায়।
প্র ০৪৬৪ বিট কম্পিউটারে তো time_t ৬৪ বিট — তাহলে Year 2038 problem নিয়ে এখনো কেন চিন্তা?
কারণ সময় শুধু CPU-র register-এ থাকে না; থাকে ফাইলে, ডেটাবেসে, network protocol-এ আর লাখো ছোট যন্ত্রে। প্রথমত, embedded যন্ত্র: রাউটার, CCTV DVR, শিল্প-কারখানার controller, গাড়ির ইলেকট্রনিক্স, স্মার্ট মিটার — এর অনেকগুলো ৩২ বিট ARM বা MIPS chip-এ পুরোনো Linux বা RTOS (real-time OS) চালায়, আর ১৫-২০ বছর বদলানো হয় না। আজ যে যন্ত্র বসানো হচ্ছে, তা ২০৩৮ সালেও চলতে পারে।
দ্বিতীয়ত, ডেটার ফরম্যাট: কোনো ফাইল বা ডেটাবেসের ঘরে timestamp ৩২ বিট signed হিসেবে রাখা থাকলে, প্রোগ্রাম ৬৪ বিট হলেও লেখার সময় truncation হবে। কিছু পুরোনো ফাইল সিস্টেম আর ডেটাবেসের timestamp type-এর সীমা ঠিক এই ২০৩৮ সালেই শেষ হয় (উদাহরণ হিসেবে প্রায়ই MySQL-এর TIMESTAMP type-এর উল্লেখ করা হয়); নতুন version-এ অনেকগুলোর সমাধান এসেছে, কিন্তু পুরোনো ডেটা আর পুরোনো কোড রয়ে যায়। Network protocol আর binary file header-এর ৩২ বিট ঘরও একই ঝুঁকিতে।
তৃতীয়ত, ভবিষ্যতের তারিখ: ২০৩৮-এর আগেই সমস্যা দেখা দিতে পারে — ৩০ বছরের ঋণ, বিমা বা সনদের মেয়াদ, ভবিষ্যতের অনুস্মারক হিসাব করার সময়। অনেক প্রতিবেদনে উল্লেখ আছে যে কিছু সিস্টেম এভাবে আগেই সমস্যায় পড়েছে।
সমাধানের দিকে বড় অগ্রগতি হয়েছে: Linux kernel ২০২০ সালের কাছাকাছি সময়ে ৩২ বিট আর্কিটেকচারেও ৬৪ বিট সময়ের system call যোগ করেছে, glibc-তে _TIME_BITS=64 দিয়ে ৩২ বিট প্রোগ্রামও ৬৪ বিট time_t পায়, আর কিছু Linux distribution তাদের ৩২ বিট সংস্করণ পুরোপুরি ৬৪ বিট সময়ে নিয়ে গেছে। এটি Y2K-র মতো — সময়মতো কাজ করলে দিনটি নিঃশব্দে পেরোবে, আর লোকে বলবে “কিছুই তো হলো না।”
মূল উপলব্ধি: Integer-এর width ডেটার সাথে ভ্রমণ করে। CPU বদলালেই সমস্যা মেটে না; ফাইল, protocol আর ডেটাবেসের প্রতিটি ঘরের width-ও দেখতে হয়।
প্র ০৫“যে সংখ্যা কখনো ঋণাত্মক হয় না, সেটি unsigned রাখুন” — এই পরামর্শ কি ঠিক? Java-তে unsigned integer নেই কেন?
শুনতে যৌক্তিক: array-র সূচক, বয়স, জিনিসের সংখ্যা কখনো ঋণাত্মক নয়, তাই unsigned রাখলে একটি বিট বাঁচে আর ভুল মান “ধরা পড়ে।” বাস্তবে উল্টো ঘটে। Unsigned-এর wrap-around ঘটে ঠিক শূন্যের পাশে — যেখানে সাধারণ প্রোগ্রাম সবচেয়ে বেশি ঘোরাফেরা করে। a - b-এ b বড় হলে ছোট ঋণাত্মক সংখ্যার বদলে আসে বিশাল ধনাত্মক সংখ্যা, যা কোনো “< 0” যাচাইয়ে ধরা পড়ে না। Signed-এর বিপদ-অঞ্চল (±২০০ কোটির কাছে) সাধারণ কোড থেকে অনেক দূরে।
তার ওপর অংশ ৮-এর মিশ্র তুলনার নিয়ম: একটি unsigned ঢুকলেই −1 হয়ে যায় বিশাল সংখ্যা। এজন্য কিছু বড় সফটওয়্যার প্রতিষ্ঠানের coding style guide বলে, শুধু “ঋণাত্মক হবে না” বলে unsigned নেবেন না; unsigned রাখুন bit pattern, mask আর mod 2n হিসাবের জন্য। C++-এর কয়েকজন প্রধান নকশাকারও পরে মত দিয়েছেন যে standard library-র size() unsigned হওয়া একটি ভুল ছিল, আর C++20-এ signed আকার দেওয়ার জন্য std::ssize যোগ হয়েছে।
Java-র নকশাকার James Gosling সরলতার জন্য unsigned type রাখেননি (char ছাড়া); তাঁর যুক্তি ছিল, বেশিরভাগ প্রোগ্রামার unsigned-এর নিয়ম ঠিকমতো বোঝেন না। পরে Java 8-এ Integer.toUnsignedString, compareUnsigned-এর মতো method এসেছে, কারণ network protocol আর file format-এ unsigned মান পড়তে হয়। অন্যদিকে C-র size_t unsigned হওয়ার যুক্তিও আছে — ৩২ বিট যুগে পুরো ঠিকানা-পরিসর গোনার জন্য।
মূল উপলব্ধি: Type বেছে নেওয়া মানে কোন সীমানায় বিপদ বসবে তা বেছে নেওয়া। সাধারণ হিসাবে signed, বিট আর modular কাজে unsigned — আর দুটি মেশানোর জায়গায় স্পষ্ট cast ও warning চালু রাখা।
অনুশীলন
পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন।
-
রূপান্তর (হাতে হিসাব): (ক) −100 আর −1-এর ৮ বিট two's complement রূপ binary ও hex-এ লিখুন। (খ) ৮ বিট প্যাটার্ন
0xB4-কে unsigned, sign-magnitude, one's complement, two's complement আর excess-128 — পাঁচ নিয়মে দশমিকে প্রকাশ করুন।(ক) 100 = 64 + 32 + 4 =
0110 0100. উল্টে1001 1011, ১ যোগে1001 1100=0x9C. যাচাই: −128 + 16 + 8 + 4 = −100. আর 1 =0000 0001→ উল্টে1111 1110→ +1 =1111 1111=0xFF; যাচাই: −128 + 127 = −1.(খ)
0xB4=1011 0100.- Unsigned: 128 + 32 + 16 + 4 = 180.
- Sign-magnitude: MSB ১ → ঋণাত্মক;
011 0100= 52 → −52. - One's complement: উল্টে
0100 1011= 75 → −75. - Two's complement: 180 − 256 = −76 (বা −128 + 32 + 16 + 4).
- Excess-128: 180 − 128 = 52.
লক্ষ করুন, one's আর two's complement-এর মান ঠিক ১ তফাত (−75 বনাম −76) — ঋণাত্মক প্যাটার্নে সবসময় তাই হয়।
-
Flag নির্ণয় (trace by hand): ৮ বিট adder-এ নিচের প্রতিটি যোগের ফল (hex), C আর V বের করুন, আর বলুন signed অর্থে ফল ঠিক কি না: (ক)
0x50 + 0x50(খ)0xF0 + 0x20(গ)0x90 + 0x90(ঘ)0xC0 + 0xC0.যোগ Signed মানে ফল C V Signed অর্থে (ক) 0x50 + 0x50 80 + 80 0xA0(−96)0 1 ভুল — আসল 160 সীমার বাইরে (খ) 0xF0 + 0x20 −16 + 32 0x10(16)1 0 ঠিক — ভিন্ন চিহ্ন, overflow অসম্ভব (গ) 0x90 + 0x90 −112 + −112 0x20(32)1 1 ভুল — আসল −224 (ঘ) 0xC0 + 0xC0 −64 + −64 0x80(−128)1 0 ঠিক — −128 ঠিক সীমায় (ক)-তে MSB-তে carry ঢোকে (0x50-এর bit 6 দুটিই ১) কিন্তু বেরোয় না, তাই V = 1 ⊕ 0 = 1. (ঘ) সবচেয়ে চতুর: দুটি ঋণাত্মক যোগে ফল ঋণাত্মক −128, তাই overflow নেই — যদিও C = 1. এখানে Cin = 1, Cout = 1, তাই V = 0.
-
আউটপুট অনুমান করুন: x86-64 Linux-এ gcc দিয়ে (৩২ বিট int) নিচের অংশটি কী ছাপবে, আর কেন?
unsigned int a = 5; int b = -10; if (a + b > 0) puts("positive"); else puts("not positive"); printf("%u\n", a + b); unsigned char d = 250; d += 10; printf("%d\n", d); printf("%d\n", -1 < (long)sizeof(int)); printf("%d\n", -1 < sizeof(int));positive 4294967291 4 1 0a + b: int আর unsigned int মিলেছে, তাই b = −10 হয়ে যায় 232 − 10 = 4294967286; যোগফল 4294967291 (mod 232) — ধনাত্মক, তাই “positive”, আর%u-এ সেই সংখ্যাটিই।d += 10: হিসাব int-এ 260, তারপর unsigned char-এ রাখার সময় mod 256 → 4.sizeofদেয় size_t (এখানে ৬৪ বিট unsigned long).(long)cast করলে দুটিই signed, তাই −1 < 4 সত্য → 1. Cast ছাড়া −1 রূপান্তরিত হয় 264 − 1-এ, তাই তুলনা মিথ্যা → 0. (gcc-Wextraশেষ লাইনে sign-compare warning দেবে — ঠিক এই ফাঁদের জন্যই।)
-
কোড ও circuit লিখুন: (ক) C-তে
int safe_add(int a, int b, int *out)লিখুন, যা overflow হলে 0 ফেরত দেবে আর না হলে যোগফল*out-এ রেখে 1 ফেরত দেবে — কোনো অবস্থায় signed overflow ঘটাবে না, built-in-ও ব্যবহার করবে না। (খ) ৮ বিট adder-এর চিহ্ন-বিট a7, b7 আর ফলের s7 থেকে V-এর Boolean সূত্র লিখুন।#include <limits.h> int safe_add(int a, int b, int *out) { if (b > 0 && a > INT_MAX - b) return 0; /* would exceed INT_MAX */ if (b < 0 && a < INT_MIN - b) return 0; /* would go below INT_MIN */ *out = a + b; /* now guaranteed in range */ return 1; }কৌশলটি হলো যোগের বদলে বিয়োগ দিয়ে তুলনা: b > 0 হলে
INT_MAX - bকখনো overflow করে না, আর b < 0 হলেINT_MIN - b-ও সীমার ভেতরে থাকে। b = 0 হলে কোনো যাচাই লাগে না। পরীক্ষা:safe_add(INT_MAX, 1, &r)→ 0;safe_add(INT_MIN, -1, &r)→ 0;safe_add(-5, 3, &r)→ 1, r = −2.(খ) Overflow হয় দুই ক্ষেত্রে: দুটি ধনাত্মক থেকে ঋণাত্মক, বা দুটি ঋণাত্মক থেকে ধনাত্মক। তাই V = a7′·b7′·s7 + a7·b7·s7′. অংশ ৭-এর ছক থেকে দেখা যায় এটি Cin ⊕ Cout-এর সমান; hardware-এ সাধারণত দ্বিতীয়টি নেওয়া হয়, কারণ একটিমাত্র XOR গেট লাগে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি ঋণাত্মক দশমিক সংখ্যার ৮ বিট two's complement বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- চিহ্ন বাদ দিয়ে পরম মানকে binary-তে লেখা
- বাঁয়ে ০ বসিয়ে ৮ বিট পূর্ণ করা
- প্রতিটি বিট উল্টানো (~x)
- ফলের সাথে ১ যোগ করা
- মূল ধনাত্মক রূপের সাথে যোগ করে ৮ বিটে ০ আসে কি না যাচাই
৮ বিট two's complement-এ কোন পরিসরের পূর্ণসংখ্যা রাখা যায়?
- −127 থেকে +127
- −128 থেকে +127
- −127 থেকে +128
- 0 থেকে 255
৮ বিট two's complement প্যাটার্ন 1111 1011-এর দশমিক মান কত?
- 251
- −4
- −5
- −123
কোন পদ্ধতিতে শূন্যের একটিমাত্র রূপ থাকে এবং signed ও unsigned যোগে একই adder ব্যবহার করা যায়?
- Sign-magnitude
- One's complement
- Excess-K
- Two's complement
৮ বিট adder-এ 0111 1111 + 0000 0001 যোগ করলে carry flag (C) আর overflow flag (V) কত হবে?
- C = 0, V = 1
- C = 1, V = 1
- C = 0, V = 0
- C = 1, V = 0
৩২ বিট int-এর একটি C প্রোগ্রামে printf("%d", -1 > 0u); কী ছাপবে?
- 0, কারণ −1 শূন্যের চেয়ে ছোট
- 1, কারণ −1 unsigned-এ রূপান্তরিত হয়ে UINT_MAX হয়
- Undefined behaviour, তাই যেকোনো কিছু
- Compile error
৪ বিট two's complement সংখ্যা 1010-কে ৮ বিটে sign-extend করলে কী পাওয়া যাবে?
- 0000 1010
- 1010 0000
- 1111 1010
- 1000 1010
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- n বিটে ঠিক 2nটি প্যাটার্ন; অর্থ ঠিক করে পড়ার নিয়ম। Unsigned: ০ … 2n − 1, আর hardware-এর হিসাব mod 2n.
- Two's complement: MSB-এর ওজন −2n−1, −x = ~x + 1, সীমা −2n−1 … 2n−1 − 1, শূন্য একটি, একই adder.
- Sign-magnitude আর one's complement-এ দুটি শূন্য; excess-K-তে ক্রম রক্ষা হয় — float-এর exponent.
- Signed সংখ্যা বড় করতে sign extension (MSB কপি), unsigned-এ zero extension; truncation মানে mod 2k.
- C = unsigned overflow, V = signed overflow (V = Cin ⊕ Cout); C-তে signed overflow UB, মিশ্র তুলনায় int unsigned হয়ে যায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৬ · ডেটার বিট-রূপ — অক্ষর, Unicode, endianness আর bit manipulationপরবর্তী পর্বঅক্ষর, byte-এর ক্রম আর বিট নিয়ে কাজ।
- পর্ব ০৪ · সংখ্যা পদ্ধতি — binary, octal, hexadecimal আর রূপান্তরআগের পর্বভিত্তি রূপান্তর আবার ঝালিয়ে নিন।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বলজিক গেট থেকে pipeline আর cache — পুরো রোডম্যাপ।
- C প্রোগ্রামিং · Undefined behavior ও নিরাপদ কোডপাশের ট্র্যাকSigned overflow আর অন্যান্য UB থেকে বাঁচার কৌশল।
- C প্রোগ্রামিং · Bitwise operationsপাশের ট্র্যাক&, |, ^, ~, shift — C-তে বিট নিয়ে কাজ।
- কম্পিউটার নেটওয়ার্ক · Error detectionপাশের ট্র্যাকOne's complement যোগে Internet checksum-এর হিসাব।