Foundationপ্রথম নীতি থেকে
LEVEL 1লেসন ৬/১৪কঠিন১ ঘণ্টা

Integer Overflow — যখন সত্যিকারের উত্তর জায়গায় আঁটে না

Integer Overflow

যখন একটা arithmetic operation-এর সত্যিকারের গাণিতিক ফলাফল n bit-এ আঁটে না, তখন কী ঘটে তা representation-ভেদে ভিন্ন — unsigned-এ এটা সংজ্ঞায়িত wraparound, signed-এ C/C++-এ এটা undefined behavior, আর এই ফারাকটাই বাস্তব জগতে সবচেয়ে বিধ্বংসী bug-ক্লাসগুলোর একটার জন্ম দিয়েছে।

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

  • Unsigned overflow (সংজ্ঞায়িত wraparound) আর signed overflow (undefined behavior)-এর মধ্যে পার্থক্য নির্ভুলভাবে ব্যাখ্যা করতে পারবেন
  • কেন `x + 1 > x` -এর মতো একটা overflow-check compiler optimization-এ হারিয়ে যেতে পারে সেটা প্রদর্শন করতে পারবেন
  • Ariane 5, Y2K38-এর মতো বাস্তব ঘটনায় ঠিক কোন representation limit দায়ী ছিল সেটা নির্ভুলভাবে বর্ণনা করতে পারবেন
  • Checked, wrapping, আর saturating arithmetic-এর মধ্যে বেছে নেওয়ার সঠিক মানদণ্ড প্রয়োগ করতে পারবেন
  • `__builtin_add_overflow`-এর মতো hardware-সমর্থিত overflow-detection ব্যবহার করে নিরাপদ arithmetic লিখতে পারবেন

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

আগে এটা বুঝি

আগের লেসনের শেষে আমরা একটা অদ্ভুত জিনিস দেখেছিলাম: INT8_MIN (-128) negate করলে গাণিতিকভাবে +128 হওয়া উচিত, কিন্তু ৮ bit-এ signed range-এর সর্বোচ্চ মান 127+128 জায়গাই নেই। সেই একটা উদাহরণকে আমরা “বিশেষ ক্ষেত্র” বলে রেখে দিয়েছিলাম।

কিন্তু এটা আসলে বিশেষ ক্ষেত্র না। এটা একটা অনেক বড় প্রশ্নের ছোট্ট নমুনা মাত্র:

একটা fixed সংখ্যক bit দিয়ে representation বানালে, সত্যিকারের গাণিতিক ফলাফল সবসময়ই সেই সীমার বাইরে যেতে পারে। তখন কী ঘটে?

উত্তরটা নির্ভর করে আপনি unsigned না signed নিয়ে কাজ করছেন তার উপর — আর এই দুইয়ের মধ্যেকার পার্থক্যটা এতটাই বড় যে একদিকে এটা “নিরাপদ, পূর্বাভাসযোগ্য আচরণ” আর অন্যদিকে এটা প্রোগ্রামিং ভাষার সবচেয়ে বিপজ্জনক concept-গুলোর একটা — undefined behavior

মূল ধারণা

দুইটা overflow, দুইটা সম্পূর্ণ ভিন্ন জগৎ

Unsigned overflow — সংজ্ঞায়িত, নিরাপদ, পূর্বাভাসযোগ্য

Unsigned integers লেসনে আমরা প্রতিষ্ঠা করেছি: n-bit unsigned addition আসলে ℤ_{2ⁿ}-তে (mod 2ⁿ) addition। C স্ট্যান্ডার্ড এটা স্পষ্টভাবে সংজ্ঞায়িত করে:

“A computation involving unsigned operands can never overflow, because a result that cannot be represented … is reduced modulo the number that is one greater than the largest value that can be represented.” — C11 §6.2.5/9

uint8_t a = 250;
uint8_t b = 10;
uint8_t c = a + b;   // 260 mod 256 = 4 — সংজ্ঞা অনুযায়ী, guaranteed

c == 4 — প্রতিটা compiler, প্রতিটা platform-এ, প্রতিবার। এটা বাগ না; এটা spec অনুযায়ী আচরণ। ভুলটা হয় যখন প্রোগ্রামার এই wraparound আশা করেননি (পরের অংশে দেখব)।

Signed overflow — Undefined Behavior

C/C++ স্ট্যান্ডার্ড signed integer overflow সম্পর্কে সম্পূর্ণ ভিন্ন ভাষায় কথা বলে:

“If an exceptional condition occurs during the evaluation of an expression … the behavior is undefined.” — C11 §6.5/5

Undefined behavior (UB) মানে ভাষার স্ট্যান্ডার্ড কোনো গ্যারান্টি দেয় না — কিছুই ঘটতে পারে। Wraparound হতে পারে (বেশিরভাগ হার্ডওয়্যারে two’s complement adder-এ এটাই “স্বাভাবিকভাবে” ঘটে), প্রোগ্রাম crash করতে পারে, অথবা — সবচেয়ে বিপজ্জনক — compiler এমন optimization করতে পারে যা আপনার লেখা কোড-কেই বদলে দেয়

ভেতরে কী ঘটছে

Compiler কীভাবে আপনার নিরাপত্তা চেক মুছে দেয়

এটাই এই লেসনের সবচেয়ে গুরুত্বপূর্ণ অংশ — কারণ এটা বিশ্বাস করা কঠিন যতক্ষণ না নিজের চোখে দেখা যায়।

একজন প্রোগ্রামার overflow ধরার জন্য স্বাভাবিকভাবেই এভাবে লিখতে চাইবেন:

#include <limits.h>

int add_safely(int x, int y) {
    if (x + y < x) {           // overflow হলে যোগফল x-এর চেয়ে ছোট হবে
        // error handle করুন
        return INT_MAX;
    }
    return x + y;
}

যুক্তিটা স্বজ্ঞাত: যদি y ধনাত্মক হয়, x + y অবশ্যই x-এর চেয়ে বড় হওয়া উচিত — যদি না overflow ঘটে থাকে। তাই x + y < x overflow-এর একটা signal।

কিন্তু এই চেকটা compiler-এর কাছে অর্থহীন — কারণ x + y নিজেই যদি overflow করে, সেটা UB, আর compiler স্ট্যান্ডার্ড অনুযায়ী ধরেই নেয় যে signed overflow কখনো ঘটে না। সেই অনুমানের উপর ভিত্তি করে, x + y < x গাণিতিকভাবে সবসময় মিথ্যা (যেহেতু y ধরে নেওয়া হচ্ছে non-negative বা overflow “না ঘটার” জগতে থাকছে) — তাই একটা optimizing compiler পুরো if ব্লকটাই মুছে ফেলতে পারে

EXPERIMENT

GCC নিজের চোখে overflow check মুছে ফেলছে

Linux / macOS, GCC বা Clang· ১০ মিনিট
// overflow_check.c
#include <stdio.h>
#include <limits.h>

int add_safely(int x, int y) {
    if (x + y \< x) {
        printf("overflow detected!\n");
        return INT_MAX;
    }
    return x + y;
}

int main(void) {
    int result = add_safely(INT_MAX, 1);
    printf("result = %d\n", result);
    return 0;
}
$ gcc -O0 -o test_o0 overflow_check.c && ./test_o0
overflow detected!
result = 2147483647

$ gcc -O2 -o test_o2 overflow_check.c && ./test_o2
result = -2147483648

-O0-এ (কোনো optimization নেই) চেকটা কাজ করে — কারণ compiler সরল, mechanical translation করে, যা হয়ে থাকে দুই’s complement wraparound। -O2-এ compiler ধরে নেয় overflow কখনো ঘটে না, তাই if condition-টাকে constant-false ধরে নিয়ে সম্পূর্ণ মুছে দেয় — আর add_safely শুধু raw x + y করে, যা wraparound করে INT_MIN (-2147483648) দেয়। ঠিক যে বাগটা প্রোগ্রামার আটকাতে চেয়েছিলেন, সেটাই optimization-এর কারণে ঘটে গেল, নীরবে।

gcc -O2 -fsanitize=undefined চালিয়ে দেখুন — এবার UBSan runtime-এ overflow detect করে সতর্ক করবে। এটাই production code-এ signed-overflow বাগ ধরার standard উপায়।

এটা কী প্রমাণ করে

signed overflow UB শুধু তাত্ত্বিক ঝুঁকি না — বাস্তব optimizing compiler সত্যিই একটা মানুষের লেখা নিরাপত্তা চেক নীরবে মুছে দেয়।

সঠিক পদ্ধতি — overflow ঘটার আগে চেক করুন:

#include <limits.h>
#include <stdbool.h>

bool add_overflows(int x, int y) {
    if (y > 0 && x > INT_MAX - y) return true;   // ধনাত্মক overflow
    if (y < 0 && x < INT_MIN - y) return true;   // ঋণাত্মক overflow
    return false;
}

এখানে কোনো actual overflow ঘটছে না — সব comparison overflow-এর আগেই করা হচ্ছে, তাই UB-এর কোনো সুযোগ নেই। এটাই “before, not after” নীতি: signed arithmetic-এ, ফলাফল হিসাব করার পর চেক করা নিরাপদ না।

আরও সহজ পথ — compiler builtin ব্যবহার করুন, যা hardware-এর carry/overflow flag সরাসরি পড়ে (Digital Logic মডিউলে, Level 2-এ, এই flag-টাই ALU-এর ভেতর থেকে সরাসরি আসে):

int result;
if (__builtin_add_overflow(x, y, &result)) {
    // overflow ঘটেছে, result-এর মান undefined হিসেবে গণ্য করুন
} else {
    // result নিরাপদে ব্যবহার করুন
}

C23 স্ট্যান্ডার্ড এটাকেই পোর্টেবল করেছে <stdckdint.h>-এ (ckd_add, ckd_sub, ckd_mul) — মানে এখন আর compiler-নির্দিষ্ট builtin-এর দরকার নেই।

উদাহরণ

Ariane 5 — একটা 64-bit float যখন 16-bit-এ আঁটেনি

১৯৯৬ সালের ৪ জুন, European Space Agency-র Ariane 5 রকেট উৎক্ষেপণের মাত্র ৩৭ সেকেন্ড পর নিজেই নিজেকে ধ্বংস করে — প্রায় ৩৭০ মিলিয়ন ডলারের রকেট ও payload হারিয়ে যায়। কারণ খুঁজে বের করতে ইনকোয়ারি বোর্ডের সময় লাগে মাত্র কয়েক সপ্তাহ, কারণ সমস্যাটা ছিল একটা একক, নির্দিষ্ট representation overflow।

যা আসলে ঘটেছিল (এটা একটা জনপ্রিয় ভুল-ধারণার বিষয়, তাই সঠিক বিবরণ গুরুত্বপূর্ণ): এটা কোনো সাধারণ int + int overflow ছিল না। Inertial Reference System (SRI)-এর সফটওয়্যার একটা ৬৪-bit floating-point মান (horizontal bias, রকেটের অনুভূমিক বেগ-সম্পর্কিত)-কে একটা ১৬-bit signed integer-এ convert করছিল। Ariane 5-এর বেগ-প্রোফাইল Ariane 4 (যেখান থেকে এই সফটওয়্যার পুনর্ব্যবহার করা হয়েছিল)-এর চেয়ে অনেক বেশি ছিল, তাই সেই float value ১৬-bit signed integer-এর সীমা (32,767)-এর চেয়ে বড় হয়ে যায়।

এই conversion-এর জন্য exception handling ছিল — কিন্তু শুধু ৭টা এমন variable-এর ৪টাতে; বাকি ৩টাতে (horizontal bias-সহ) protection বাদ দেওয়া হয়েছিল, কারণ Ariane 4-এর flight profile বিশ্লেষণ করে প্রকৌশলীরা “গাণিতিকভাবে অসম্ভব” বলে সিদ্ধান্ত নিয়েছিলেন — একটা ভুল অনুমান, যেটা নতুন রকেটে আর সত্যি ছিল না। Overflow ঘটার পর handled exception পুরো SRI computer-কে shut down করে দেয় (এটাই নকশা ছিল — ধরে নেওয়া হয়েছিল hardware fault, software bug না)। Backup SRI একই কোড চালাচ্ছিল, একই কারণে সেটাও বন্ধ হয়ে যায়। Guidance system-বিহীন রকেট ভুল দিকে nozzle ঘুরিয়ে aerodynamic force-এ ভেঙে পড়ে, আর automatic self-destruct সক্রিয় হয়।

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

EXPERIMENT

Y2K38 — নিজের সিস্টেমে ঘড়ি overflow করে দেখুন

Linux / macOS, 32-bit time_t সিমুলেশন· ১০ মিনিট

Unix time_t ঐতিহাসিকভাবে একটা ৩২-bit signed integer — ১৯৭০ সালের ১ জানুয়ারি থেকে সেকেন্ড গোনে। এর সর্বোচ্চ মান:

>>> 2**31 - 1
2147483647
>>> import datetime
>>> datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=2**31 - 1)
datetime.datetime(2038, 1, 19, 3, 14, 7)

২০৩৮ সালের ১৯ জানুয়ারি, ০৩:১৪:০৭ UTC-এর পরের সেকেন্ডে এই মান overflow করে signed integer wraparound-এ -2147483648-এ চলে যায় — যা 1901-এর একটা তারিখ হিসেবে interpret হয়:

>>> datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=-2**31)
datetime.datetime(1901, 12, 13, 20, 45, 52)

সরাসরি সিমুলেট করুন:

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

int main(void) {
    int32_t t = 2147483647;      // 2038-01-19 03:14:07 UTC
    t += 1;                       // এক সেকেন্ড পরে
    printf("wrapped time_t = %d\n", t);   // -2147483648

    time_t future = (time_t)t;
    printf("%s", ctime(&future));
    return 0;
}

এই কারণেই আধুনিক সিস্টেমে time_t এখন সাধারণত 64-bit (যার সীমা ~২৯২ বিলিয়ন বছর পর — কার্যত অসীম), কিন্তু বহু পুরনো embedded system, database format (কিছু 32-bit filesystem timestamp), আর legacy protocol এখনও 32-bit-এই আটকে আছে — ঠিক Y2K-এর মতোই, কিন্তু এবার সময়সীমা আগে থেকেই জানা।

এটা কী প্রমাণ করে

একটা সংজ্ঞা-নির্ধারিত representation limit বাস্তব সময়সীমার সাথে মিলে গেলে একটা ভবিষ্যৎ বাগ হয়ে দাঁড়ায় — আজ যা 'অনেক দূরের সমস্যা' মনে হয়।

নিজে বানান

BUILD IT

Overflow-নিরাপদ Arithmetic Library

C অথবা Rust · ●●●○○
  1. checked_add/checked_sub/checked_mul লিখুন যা overflow হলে একটা error/None রিটার্ন করে
  2. wrapping_add লিখুন যা unsigned modular semantics অনুকরণ করে (signed input-কেও)
  3. saturating_add লিখুন যা overflow হলে MIN/MAX-এ clamp করে
  4. তিনটাকেই একই ইনপুট (INT_MAX + 1, INT_MIN - 1, স্বাভাবিক case) দিয়ে test করুন এবং তিনটা ভিন্ন ফলাফল তুলনা করুন
  5. একটা real scenario বেছে নিন (audio mixing, inventory count, game score) আর যুক্তি দিন কোন policy সেখানে সঠিক
#include <limits.h>
#include <stdbool.h>

bool checked_add_i32(int32_t a, int32_t b, int32_t *out) {
    if (b > 0 && a > INT32_MAX - b) return false;
    if (b < 0 && a < INT32_MIN - b) return false;
    *out = a + b;
    return true;
}

int32_t wrapping_add_i32(int32_t a, int32_t b) {
    // unsigned-এ cast করে যোগ করুন — সেটা সংজ্ঞায়িত (UB-মুক্ত),
    // তারপর ফিরিয়ে signed bit pattern হিসেবে পড়ুন
    uint32_t ua = (uint32_t)a, ub = (uint32_t)b;
    uint32_t sum = ua + ub;             // সংজ্ঞায়িত mod 2^32
    return (int32_t)sum;                 // implementation-defined reinterpret,
                                          // দুই's complement platform-এ predictable
}

int32_t saturating_add_i32(int32_t a, int32_t b) {
    int32_t result;
    if (checked_add_i32(a, b, &result)) return result;
    return (b > 0) ? INT32_MAX : INT32_MIN;
}

Rust-এ এই তিনটাই প্রথম শ্রেণির citizen — নিজে তুলনা করে দেখুন:

let a: i32 = i32::MAX;
println!("{:?}", a.checked_add(1));     // None
println!("{}", a.wrapping_add(1));       // -2147483648
println!("{}", a.saturating_add(1));     // 2147483647
// release build-এ প্লেইন a + 1 নীরবে wrap করে;
// debug build-এ panic করে — overflow-checks flag নিয়ন্ত্রণ করে

কোনটা কখন:

Policyকোথায় ব্যবহার হয়
CheckedFinancial calculation, security-critical logic — ভুল হলে জানতে হবে
WrappingHash function, ring buffer index, checksum — wraparound-ই কাম্য আচরণ
SaturatingAudio/image pixel value, game health bar, UI progress — “সর্বোচ্চে আটকে থাকা” স্বাভাবিক

বাস্তব সিস্টেমে

Overflow যেখানে সত্যিই বিপর্যয় ডেকেছে

  • Ariane 5 (১৯৯৬) — উপরে বিস্তারিত। ৩৭০ মিলিয়ন ডলার, একটা single unprotected 64-bit-to-16-bit conversion।

  • Y2K38 — 32-bit signed time_t ২০৩৮-এ overflow করবে; এখনও বহু embedded/legacy system ঝুঁকিতে।

  • “Civilization”-এ Gandhi-র কিংবদন্তি — একটা জনপ্রিয় (কিন্তু বিতর্কিত) গল্প: Gandhi-র aggression rating খুবই কম (১) সেট করা ছিল একটা unsigned byte-এ; Democracy adopt করলে aggression -2 কমার কথা, কিন্তু unsigned byte-এ 1 - 2 “ঋণাত্মক” হতে পারে না — wrap করে 255 (সর্বোচ্চ aggression) হয়ে যায়, বানিয়ে দেয় আক্রমণাত্মক Gandhi। মূল Civilization I কোডে এই নির্দিষ্ট bug সত্যিই ছিল কি না তা historians-দের কাছে অমীমাংসিত/বিতর্কিত — Sid Meier নিজে অস্পষ্ট মন্তব্য করেছেন। কিন্তু গল্পটা এত জনপ্রিয় হওয়ার কারণ যান্ত্রিকতাটা সম্পূর্ণ বাস্তব ও সাধারণ: এই ধরনের unsigned underflow wraparound bug অসংখ্য বাস্তব সিস্টেমে ঘটেছে। (মজার তথ্য: Civilization V-এ ডেভেলপাররা ইচ্ছাকৃতভাবে Gandhi-কে nuke-প্রবণ বানিয়েছেন — কিংবদন্তির প্রতি একটা homage হিসেবে।)

  • ২০১৫ সালের একটা স্টক ট্রেডিং outage — একটা trading system-এ signed 32-bit counter overflow করে negative order-quantity তৈরি করেছিল, যা ভুল bulk sell order-এ রূপান্তরিত হয় — মিলিয়ন ডলারের ক্ষতি কয়েক মিনিটে।

  • ব্লকচেইন/স্মার্ট কন্ট্রাক্ট overflow exploit — Solidity-র পুরনো সংস্করণে (0.8.0-এর আগে) integer overflow ছিল silent wraparound, UB না — কিন্তু ফলাফল একইরকম বিপজ্জনক ছিল। ২০১৮-এর “BEC token” hack-এ attacker-রা একটা multiplication overflow কাজে লাগিয়ে astronomically বড় সংখ্যক token “তৈরি” করেছিল একটা ছোট transfer থেকে। Solidity 0.8.0 থেকে সব arithmetic default-এ checked (overflow হলে revert করে)।

  • গেম/ইমেজ প্রসেসিং-এ saturating arithmetic — 8-bit pixel value-তে (0-255) ব্রাইটনেস বাড়ালে wraparound হলে সাদা pixel হঠাৎ কালো হয়ে যেত (২৫৫ + ১০ = ৯ mod ২৫৬)। তাই সব image library saturating add ব্যবহার করে — clamp(255+10, 0, 255) = 255, যেটা visually সঠিক (“আরও উজ্জ্বল করা যায় না, ইতিমধ্যে সর্বোচ্চ”)।

  • Boeing 787 জেনারেটর রিসেট বাগ (২০১৫, FAA directive) — একটা internal software counter (উড়োজাহাজের generator control unit-এ) প্রতি ~২৪৮ দিনে overflow করত, generator-কে fail-safe mode-এ পাঠিয়ে সম্পূর্ণ electrical power হারানোর ঝুঁকি তৈরি করত যদি একটানা এত দিন উড়োজাহাজ চালু থাকত — সমাধান ছিল নিয়মিত reboot।

যে ভুলগুলো সবাই করে

“Overflow মানেই crash বা error message।”

বাস্তবে বেশিরভাগ overflow নীরব। Unsigned overflow সংজ্ঞা অনুযায়ী wrap করে, কোনো সতর্কতা ছাড়াই — প্রোগ্রাম চলতে থাকে, শুধু ভুল উত্তর নিয়ে। Signed overflow (UB) প্রায়ই একইভাবে wrap করে (hardware-স্তরে), কারণ compiler optimization “লক্ষণীয়ভাবে” কিছু না করেও কোডকে ভুল বানাতে পারে (যেমন উপরের overflow-check মুছে যাওয়ার উদাহরণ)।

crash হওয়াটাই বরং ভালো — অন্তত তখন সমস্যাটা দৃশ্যমান। সবচেয়ে বিপজ্জনক overflow হলো সেগুলো যা নীরবে ভুল উত্তর দেয় আর প্রোগ্রাম স্বাভাবিকভাবে চলতে থাকে — যেমন Ariane 5-এর ৩ সেকেন্ড আগে পর্যন্ত সবকিছু “স্বাভাবিক” দেখাচ্ছিল।

“আমার সংখ্যাগুলো ছোট, overflow নিয়ে ভাবার দরকার নেই।”

“ছোট” সংখ্যা ধরে নেওয়াটাই সবচেয়ে সাধারণ ভুল। কয়েকটা সত্যিকারের প্যাটার্ন যেখানে “নিরাপদ” সংখ্যা হঠাৎ overflow করে:

  • গুণ, যোগ না — দুইটা 16-bit সংখ্যা (সর্বোচ্চ 65,535) গুণ করলে ফলাফল 32-bit-এর প্রায় পুরোটা লাগতে পারে (65535 × 65535 ≈ 4.3 × 10⁹)। image processing-এ width × height × bytes-per-pixel এভাবেই overflow করে।
  • সঞ্চয়ী যোগফল (accumulator) — একটা loop-এ ছোট ছোট মান বারবার যোগ করলে সময়ের সাথে বড় হয়ে যায় (একটা counter, একটা running total)।
  • Intermediate calculation(a + b) * c -তে a + b নিজে ছোট মনে হলেও, চূড়ান্ত টাইপে cast হওয়ার আগেই intermediate ধাপে overflow ঘটতে পারে যদি টাইপ narrow হয়।
  • Unit conversion — মিলিসেকেন্ড থেকে ন্যানোসেকেন্ড (× 10⁶) -এর মতো conversion 32-bit-কে সহজেই ছাপিয়ে যায়।

“Signed আর unsigned overflow মূলত একই জিনিস, শুধু sign আলাদা।”

এটাই এই লেসনের সবচেয়ে গুরুত্বপূর্ণ ভুল-ধারণা ভাঙা। Unsigned overflow ভাষার স্ট্যান্ডার্ড অনুযায়ী সংজ্ঞায়িত (mod 2ⁿ) — আপনি নির্ভর করতে পারেন এটার আচরণের উপর। Signed overflow undefined behavior — ভাষার স্ট্যান্ডার্ড কোনো গ্যারান্টি দেয় না, আর compiler সেই “কখনো ঘটে না” অনুমানের ভিত্তিতে কোড বদলে দিতে পারে।

এটা শুধু academic পার্থক্য না — উপরের GCC experiment-এ দেখা গেছে এই পার্থক্যটাই একটা কার্যকর নিরাপত্তা চেককে সম্পূর্ণ অকার্যকর করে দিতে পারে।

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

1

নিচের C ফাংশনটা কেন signed overflow-এর ক্ষেত্রে ভুল, এবং হার্ডওয়্যার পর্যায়ে কী “সাধারণত” ঘটে তা আলাদা করে ব্যাখ্যা করুন:

int is_sum_positive(int a, int b) {
    return (a + b) > 0;
}

a = INT_MAX, b = 1 দিয়ে কল করলে কী হওয়া উচিত, আর বাস্তবে কী হতে পারে?

যুক্তি

গাণিতিকভাবে INT_MAX + 1 অবশ্যই 0-এর চেয়ে বড় (এটা একটা বিশাল ধনাত্মক সংখ্যা), তাই ফাংশনটা true ফেরত দেওয়া “উচিত” মনে হতে পারে — কিন্তু আসল representable মানে এই যোগফলটা প্রকাশই করা যায় না।

ভাষার দৃষ্টিতে: a + b signed overflow ঘটায়, যা UB। ফাংশনের পুরো আচরণ তখন undefined — true, false, crash, বা compiler-এর পছন্দমতো যেকোনো কিছু, সব-ই “সঠিক” (ভাষার নিয়ম অনুযায়ী কিছুই ভুল না, কারণ কোনো নিয়মই প্রযোজ্য না)।

হার্ডওয়্যার পর্যায়ে (সাধারণত): two’s complement adder কোনো “signed” বা “unsigned” জানে না (আগের লেসনের মূল insight)। INT_MAX + 1 bit-level-এ 0111...1 + 1 = 1000...0, যেটা signed পাঠে INT_MIN (একটা বিশাল ঋণাত্মক সংখ্যা)। তাই “সাধারণত” (কোনো optimization ছাড়া) ফাংশনটা false ফেরত দেয় — গাণিতিক প্রত্যাশার ঠিক উল্টো!

কিন্তু optimization-সহ compiler ধরে নিতে পারে a + b overflow করে না, তাই a-কে বড় ধনাত্মক সংখ্যা (INT_MAX) আর b-কে ধনাত্মক (1) হিসেবে ধরে পুরো ফাংশনটাকেই constant true -এ ভাঁজ করে ফেলতে পারে — কোনো actual addition ছাড়াই।

তিনটা ভিন্ন সম্ভাব্য উত্তর (true তাত্ত্বিকভাবে, false hardware-এ un-optimized, true optimized) — এটাই দেখায় কেন UB-কে “শুধু hardware wraparound” ধরে নেওয়া বিপজ্জনক।

2

একটা video game-এ player-এর health uint8_t (0-255)-এ রাখা হয়েছে। একটা poison effect প্রতি টিকে damage (একটা uint8_t) বিয়োগ করে। কোড:

uint8_t health = 5;
uint8_t damage = 10;
health = health - damage;

health-এর মান কী হবে, আর গেমপ্লেতে এর ফলাফল কী হতে পারে?

প্রয়োগ

health - damage = 5 - 10, unsigned arithmetic-এ যা mod 256 হিসেব হয়: 5 - 10 + 256 = 251

health হয়ে যায় 251 — অর্থাৎ player মৃত হওয়ার বদলে প্রায় পূর্ণ স্বাস্থ্যে “সুস্থ” হয়ে যায়! এটা একটা ক্লাসিক unsigned underflow bug, ঠিক Gandhi-কিংবদন্তির মতো mechanism।

সঠিক সমাধান — clamp/saturate করুন, wrap হতে দেবেন না:

uint8_t new_health = (damage >= health) ? 0 : health - damage;

অথবা saturating subtraction ব্যবহার করুন:

health = (health > damage) ? health - damage : 0;

সাধারণ নীতি: যেখানে “শূন্যের নিচে যাওয়া” গাণিতিকভাবে অর্থহীন (health, inventory count, remaining time), সেখানে unsigned subtraction-এর আগে সবসময় a >= b চেক করুন, নাহলে saturating type/library ব্যবহার করুন।

3

__builtin_add_overflow(a, b, &result) ব্যবহার করা if (a + b < a) -এর চেয়ে ভালো কেন — শুধু “এটা একটা builtin তাই দ্রুত” ছাড়া আসল কারণটা কী?

যুক্তি

আসল কারণ correctness, শুধু performance না।

if (a + b < a) লেখা মানে a + b টা আগেই evaluate করা হয়ে গেছে — signed হলে সেই evaluation-টাই যদি overflow করে, UB ইতিমধ্যে ঘটে গেছে তুলনাটা করার আগেই। Compiler তখন ধরে নিতে পারে এই overflow “কখনো হয় না” আর পুরো চেকটাকেই বাতিল করতে পারে (যেমন Experiment-এ দেখানো হয়েছে)।

__builtin_add_overflow ভিন্নভাবে কাজ করে — এটা hardware-এর overflow/carry flag সরাসরি পড়ে (একই flag যেটা ALU addition করার সময় সেট করে — Level 2-এ digital logic-এ এই ঠিক এই hardware mechanism দেখব), বাস্তবে overflow হওয়ার সময়ই ধরে ফেলে, কোনো পরবর্তী “যদি ইতিমধ্যে ভুল হয়ে গেছে” আচরণের উপর নির্ভর না করে। ভাষার স্ট্যান্ডার্ড এই builtin-এর আচরণকে সংজ্ঞায়িত করে (unlike raw signed overflow), তাই compiler এটাকে “অসম্ভব” ধরে optimize করতে পারে না।

সংক্ষেপে: raw arithmetic দিয়ে overflow-এর ফলাফল পড়ার চেষ্টা করবেন না (কারণ ফলাফলটাই UB); overflow-detecting primitive দিয়ে overflow ঘটার মুহূর্তটা ধরুন।

4

আপনি একটা payment-processing সিস্টেম ডিজাইন করছেন যা টাকার পরিমাণ (সেন্ট/পয়সায়, integer) যোগ করে। overflow হ্যান্ডলিং-এর জন্য checked, wrapping, নাকি saturating arithmetic বেছে নেবেন, আর কেন অন্য দুইটা বিপজ্জনক?

ডিজাইন

Checked arithmetic — এটাই সঠিক পছন্দ।

Wrapping কেন বিপজ্জনক: একটা transaction যদি overflow করে wrap করে, একটা বিশাল amount হঠাৎ ছোট বা এমনকি ঋণাত্মক (signed হলে) হয়ে যেতে পারে — অর্থাৎ সিস্টেম নীরবে ভুল amount প্রসেস করবে, হয়তো কারো account থেকে ভুল টাকা কেটে নেবে বা ভুল টাকা জমা দেবে। এটা শুধু bug না, এটা আর্থিক ক্ষতি এবং সম্ভবত জালিয়াতির সুযোগ (ব্লকচেইন BEC token hack ঠিক এই কারণেই সম্ভব হয়েছিল)।

Saturating কেন বিপজ্জনক: clamp করা মানে টাকা হারিয়ে যাওয়া নীরবে। যদি একটা যোগফল MAX_VALUE-এ clamp হয়, প্রকৃত (বড়) amount -টা কোথাও রেকর্ড হয়নি — accounting-এ এই ফাঁক পরে মেলানো অসম্ভব। Audio/pixel-এ saturation ঠিক আছে কারণ “সর্বোচ্চ উজ্জ্বলতা”-ই কাম্য visual ফলাফল; টাকায় “সর্বোচ্চ” একটা arbitrary কাটআপ, প্রকৃত মান না।

Checked arithmetic সঠিক কারণ: overflow ঘটলে তাৎক্ষণিকভাবে transaction প্রত্যাখ্যাত হয়, error propagate হয়, আর কোনো আংশিক/ভুল state persist হয় না। আর্থিক সিস্টেমে “ভুল উত্তর দেওয়ার চেয়ে fail করা” সবসময় ভালো।

বাস্তবায়নের বিস্তারিত: এমনকি checked arithmetic যথেষ্ট না যদি টাইপ নিজেই খুব ছোট হয় — বাস্তব payment system-এ সাধারণত int64 (বা arbitrary-precision decimal) ব্যবহার হয় যাতে বাস্তব transaction volume-এ overflow structurally অসম্ভব হয়ে যায়, আর checked arithmetic শুধু “defense in depth” হিসেবে থাকে।

এরপর কী

আমরা দেখেছি integers-এর একটা কঠোর সীমা আছে — একটা fixed সংখ্যক bit দিয়ে শুধু ততগুলো ভিন্ন মান-ই প্রকাশ করা যায়, তার বেশি না। Overflow সেই সীমার একদিকের গল্প।

কিন্তু integer-এর আরেকটা সীমাবদ্ধতা আছে, যেটা overflow-এর চেয়েও মৌলিক — এটা “কত বড় সংখ্যা” নিয়ে না, বরং “কী ধরনের সংখ্যা” নিয়ে। Integer দিয়ে ভগ্নাংশ লেখাই যায় না। 3 / 2 যদি integer division হয়, উত্তর 1 — বাকি অর্ধেকটা হারিয়ে যায়। π, 0.1, √2 — এদের কোনোটাই একটা integer bit pattern-এ নেই।

পরের লেসনে আমরা দেখব এই সমস্যার দুইটা সমাধান — fixed-point (একটা integer, শুধু একটা implied scale factor সহ) আর floating-point (scientific notation-এর মতো, একটা “ভাসমান” দশমিক বিন্দু) — আর কেন দ্বিতীয়টা শেষ পর্যন্ত জিতেছে।

আরও পড়ুন

  • Ariane 5 Flight 501 Failure — Inquiry Board Report — European Space Agency / CNES (1996) · ৬৪-bit float থেকে ১৬-bit signed integer conversion overflow-এর প্রামাণ্য বিবরণ
  • A Guide to Undefined Behavior in C and C++ — John Regehr · Signed overflow UB compiler optimization-এ কীভাবে আসলে প্রভাব ফেলে তার classic ব্যাখ্যা
  • C23 Standard, §7.20a — ckd_add, ckd_sub, ckd_mul — ISO/IEC 9899:2024 · Checked arithmetic এখন ভাষার standard library-র অংশ