5.1.30 · D3 · Coding › C Programming › Undefined behavior — comprehensive list, why to avoid
Yeh page UB topic note ka hands-on lab hai. Parent note ne categories list ki thin; yahan hum actual experiments chalate hain. Hum har tarah ke UB cases enumerate karte hain — har sign, har zero, har degenerate input, har limiting value — phir aise examples kaam karte hain jo us map ke har cell ko hit karte hain. Pehle tum guess karo ("Forecast"), phir hum step by step reason karte hain, phir hum real arithmetic se verify karte hain.
Pehli line se pehle: ek quick vocabulary anchor taaki koi bhi symbol samajh na aaye aisa na ho.
Definition Teen words jinpar hum zyada rely karenge
==bit width n == :: ek number type mein kitne binary digits hote hain. Ek int usually n = 32 bits ka hota hai. Socho 32 chote on/off switches ek line mein.
two's complement :: woh standard tarika jisme hardware signed integers store karta hai. Top switch ka matlab hai "negative". n = 32 ke saath values − 2 31 se 2 31 − 1 tak jaati hain.
==INT_MAX / INT_MIN== :: ek signed int ki sabse badi aur sabse choti values: INT_MAX = 2 31 − 1 = 2147483647 aur INT_MIN = − 2 31 = − 2147483648 .
Dhyan karo ki range lopsided hai: positive se ek zyada negative number hai. Yeh ek asymmetry hi neeche kai UB bugs ki seed hai — isko dhyan mein rakhna.
Practice mein milne wala har UB bug neeche diye gaye cells mein se kisi ek mein aata hai. Worked examples ka kaam hai ki har cell par kam se kam ek baar land kiya jaaye .
Cell
Case class
Degenerate / limiting input
Example jo ise hit karta hai
A
Signed overflow — positive side
x near INT_MAX
Ex 1
B
Signed overflow — the INT_MIN trap
INT_MIN / -1
Ex 2
C
Divide / modulo by zero
denominator = 0
Ex 3
D
Shift out of range
shift ≥ n or shift < 0
Ex 4
E
Out-of-bounds — read past the end
index = length
Ex 5
F
Sequencing — modify twice, no order
a[i] = i++
Ex 6
G
Lifetime — dangling pointer
pointer after scope ends
Ex 7
H
Varargs type mismatch (real-world)
%d fed a double
Ex 8
I
Unsigned wrap is DEFINED (the control case)
UINT_MAX + 1
Ex 9
J
Exam twist — overflow check that vanishes
x + 1 < x
Ex 10
Prerequisite links yahan hain taaki jab bhi koi cell confuse kare tum neeche ja sako:
Integer Types and Overflow , Sequence Points and Operator Precedence , Pointers and Memory in C , malloc and free — Dynamic Memory , Compiler Optimizations and -O flags , Sanitizers — ASan and UBSan , Strict Aliasing Rule .
Neeche di gayi figure map hai — ek signed int ki number line, jisme danger points mark kiye gaye hain. Har arithmetic example isi par refer karta hai.
Dono taraf ke do red cliffs dekho: kisi bhi edge se bahar jaana UB hai. Beech ka mint region safe zone hai.
Worked example Ex 1 — Ceiling ke paas add karna
int x = 2000000000 ; // 2e9, below INT_MAX = 2147483647
int y = x + 200000000 ; // 2.2e9 — over the top
Forecast: y kya hai? Kya yeh 2.2 × 1 0 9 hai? Kya yeh ek bada negative number hai? Ya phir "koi defined answer hi nahi hai"?
Steps:
Mathematical sum compute karo: 2000000000 + 200000000 = 2200000000 .
Yeh step kyun? Type check karne se pehle hume true integer value chahiye.
Ceiling se compare karo: INT_MAX = 2147483647 . Kyunki 2200000000 > 2147483647 , result fit nahi hota . s01 figure dekho — hum right red cliff se bahar gaye.
Yeh step kyun? Overflow define hota hai range se bahar jaane se, kisi wrap formula se nahi.
Kyunki type signed hai, range se bahar jaana undefined behavior hai. y ki koi guaranteed value nahi hai — na wrapped number, na kuch aur.
Yeh step kyun? Parent note ka rule: signed overflow koi requirements impose nahi karta. Debug build mein wrapped value dikh sakti hai, lekin standard kuch promise nahi karta.
Fix — compute karne se pehle check karo:
if (x > INT_MAX - 200000000 ) { /* would overflow */ }
else y = x + 200000000 ;
Yeh step kyun? x > INT_MAX − 200000000 likhne se har quantity safe mint zone ke andar rehti hai — subtraction 2147483647 − 200000000 = 1947483647 kabhi overflow nahi karta.
Verify: true sum 2200000000 , INT_MAX = 2147483647 se 52516353 zyada hai → overflow confirm. Guard threshold INT_MAX − 200000000 = 1947483647 , aur x = 2000000000 > 1947483647 → guard sahi fire karta hai.
Worked example Ex 2 — Sabse chote number ko minus one se divide karna
int a = INT_MIN; // -2147483648
int b = a / - 1 ; // "should be +2147483648"
Forecast: division harmless lagti hai — koi zero nahi. Safe hai ya UB?
Steps:
Mathematical answer hai − ( − 2147483648 ) = 2147483648 .
Yeh step kyun? Kisi value ko negate karna sign flip karta hai; range test karne ke liye hume true result pata hona chahiye.
Ceiling check karo: INT_MAX = 2147483647 . Lekin 2147483648 > 2147483647 — exactly ek zyada.
Yeh step kyun? Yahan lopsided range hume bite karti hai — INT_MIN ka koi positive twin nahi hai.
Toh division ka result type mein overflow karta hai → UB , chahe humne zero se divide na kiya ho.
Yeh step kyun? Overflow result ke fit na hone ke baare mein hai, chahe woh kahaan se bhi aaye.
Fix: ek poisonous pair ko guard karo.
if (a == INT_MIN && divisor == - 1 ) { /* special-case it */ }
Verify: − INT_MIN = 2147483648 = INT_MAX + 1 → exactly ceiling se ek zyada, overflow confirm.
Worked example Ex 3 — Khaali divisor
int n = 10 , d = 0 ;
int q = n / d; // and int r = n % d;
Forecast: paper par "divide by zero" infinity hoti hai. C kya karta hai?
Steps:
Mathematically 10/0 ki koi finite value nahi — koi aisa integer q nahi jiske liye q × 0 = 10 ho.
Yeh step kyun? Division ka matlab hai "d kitni baar fit hota hai"; agar d = 0 toh sawaal hi meaningless hai.
Standard isliye / 0 aur % 0 dono ko undefined behavior declare karta hai. Kai CPUs par yeh hardware trap raise karta hai (crash), lekin yeh guaranteed nahi hai — compiler poore path ko optimize kar ke hataa sakta hai.
Yeh step kyun? "Yeh reliably crash karta hai" ek myth hai; UB ka matlab hai crash bhi promised nahi hai.
Fix — denominator validate karo:
if (d == 0 ) return ERR;
int q = n / d;
Verify (defined companion case): jab d = 2 ho, 10/2 = 5 aur 10%2 = 0 — operation sirf tab well-defined hai jab d = 0 ho.
Worked example Ex 4 — Bahut zyada shift karna
unsigned int u = 1 u ;
unsigned int big = u << 40 ; // int is 32 bits
int neg = - 1 ;
int oops = neg << 3 ; // left-shifting a negative value
Forecast: 1u << 40 — kya yeh bas zero ho jaata hai? Aur kya tum ek negative number ko left-shift kar sakte ho?
Steps:
Ek 32-bit type ke liye valid shift counts hain 0 ≤ count ≤ 31 . Humne 40 maanga.
Yeh step kyun? Rule yeh hai: ≥ n (yahan n = 32 ) shift karna UB hai. 40 ≥ 32 → map se bahar.
Toh u << 40 UB hai, defined zero nahi . s01 dekho: socho jaise bits itni dur shift ho gayi ki woh gir gayi aur standard kuch bhi guarantee karna band kar deta hai .
Yeh step kyun? Beginners assume karte hain ki overshift 0 dega — yeh galat hai; yeh undefined hai.
neg << 3 − 1 ko left-shift karta hai. Ek negative signed value ko left-shift karna bhi UB hai.
Yeh step kyun? Shift ke under sign bit behaviour standard ke according unspecified/undefined hai.
Fix: count ko range mein rakho aur sirf non-negative ya unsigned values ko shift karo.
if (count < 32 ) big = u << count;
unsigned int safe = ( unsigned )neg << 3 ; // define the wrap intent
Verify (defined case): ek legal shift 1 u ≪ 4 = 16 aur 1 u ≪ 31 = 2147483648 ; count 31 aakhri legal hai kyunki 31 < 32 .
Worked example Ex 5 — Index = length padhna
int a [ 3 ] = { 10 , 20 , 30 }; // valid indices: 0, 1, 2
int v = a [ 3 ]; // one past the end
Forecast: array "continue" hoti lagti hai — kya a[3] ek random number dega, ya UB hai?
Steps:
Length 3 ki array ke indices hain 0 , 1 , 2 . Aakhri valid index hai length − 1 = 2 .
Yeh step kyun? Indices zero-based hain; count aur top index mein ek ka fark hota hai — yeh bhi ek off-by-one trap hai.
a[3] woh slot padhta hai jo length par hai, jo object ke bahar hai → UB . (Tum ek one-past pointer a+3 bana sakte ho, lekin tum use dereference nahi kar sakte.)
Yeh step kyun? Parent note ne stress kiya tha: one-past pointers legal hain, one-past reads nahi.
Symptom: yeh adjacent stack garbage print kar sakta hai, neighbour ko corrupt kar sakta hai, ya silently crash kar sakta hai.
Yeh step kyun? "Theek se print hua" sabse dangerous UB symptom hai.
Fix / detect: 0 <= i && i < 3 validate karo, aur ASan chalao jo isko instantly flag karta hai.
Verify: valid indices hain { 0 , 1 , 2 } ; count hai 3 ; max legal index = 3 − 1 = 2 , toh index 3 exactly 1 se out of bounds hai.
a[i] = i++
int i = 0 ;
int a [ 3 ] = { 0 };
a [i] = i ++ ; // UB
Forecast: kya yeh a[0] = 0 set karta hai aur phir i = 1 banata hai? Intended-looking answer guess karo.
Steps:
Ek statement mein i ke saath do kaam hote hain: yeh read hota hai (index a[i] choose karne ke liye) aur yeh modify hota hai (i++ se).
Yeh step kyun? Hume har jagah locate karni hai jahan same object change hota hai ya use hota hai.
"index ke liye i use karo" aur "i increment karo" ke beech koi sequence point nahi hai. i ko modify karna aur bina ordering ke ise unrelated purpose ke liye use karna = UB .
Yeh step kyun? Parent ka Category 3: yeh undefined hai, sirf "5 ya 6" nahi.
Toh yeh "obviously a[0]=0" nahi hai — compiler a[0] ya a[1] mein likh sakta hai, aur i kuch bhi ho sakta hai.
Yeh step kyun? Sequencing UB ka kabhi "natural" outcome assume mat karo.
Fix — alag karo:
a [i] = i;
i ++ ;
Verify: i = 0 ke saath fixed code ke baad: a[0] = 0, phir i 1 ho jaata hai — ek well-defined single modification per statement.
Worked example Ex 7 — Local ka address return karna
int * make ( void ) {
int local = 42 ;
return & local; // local dies at return
}
int * p = make ();
int val = * p; // UB
Forecast: value 42 abhi abhi wahan thi — kya *p zaroor 42 padh lega?
Steps:
local ek automatic variable hai — iska storage ek stack slot hai jo sirf make() ke chalte waqt exist karta hai.
Yeh step kyun? Legality decide karta hai lifetime, raw byte value nahi.
Jab make() return karta hai, woh slot reuse ke liye free ho jaata hai. Pointer p ab expired storage ki taraf point karta hai — yeh dangling pointer hai. *p padhna UB hai.
Yeh step kyun? Kisi object ko uski lifetime ke baad access karna undefined hai, chahe bytes linger karein.
Yeh ek baar 42 print kar sakta hai, phir agli function call ke baad break ho jaata hai jab slot overwrite ho jaaye — classic "ek baar kaam karta hai" trap.
Fix — by value return karo ya heap allocation use karo:
int make ( void ) { return 42 ; } // by value
// or: int *p = malloc(sizeof *p); *p = 42; // caller frees
Verify: intended value 42 hai; fixed by-value version exactly 42 return karta hai bina kisi lifetime issue ke.
Worked example Ex 8 — Ek temperature logger jo jhooth bolta hai
Ek weather program temperature ko double mein store karta hai aur log karta hai:
double temp = 3.5 ;
printf ( "temp = %d\n " , temp); // %d expects int, got double — UB
Forecast: kya yeh 3 print karta hai? 3.5? Ya koi junk number?
Steps:
%d ek int ka format hai; temp ek double hai. printf apne extra arguments blindly format string use karke padhta hai.
Yeh step kyun? Varargs functions mein koi type checking nahi hoti — format string hi ek maatra contract hai.
double ke bytes int se alag layout mein hote hain, toh unhe int ki tarah padhna UB deta hai — printed number 3 nahi, 3.5 nahi, balki undefined garbage hai.
Yeh step kyun? Varargs mein type/format mismatch explicit UB hai (parent Category 5).
Fix: specifier ko type se match karo.
printf ( "temp = %f\n " , temp); // %f for double -> prints 3.500000
Verify: 3.5 par %f sahi 3.5 deta hai; agar tum sach mein integer part chahte the, toh woh (int)3.5 == 3 hoga.
UINT_MAX + 1 (koi bug nahi!)
unsigned int u = 4294967295 u ; // UINT_MAX for 32-bit
unsigned int w = u + 1 ;
Forecast: agar signed overflow UB tha, toh kya unsigned overflow bhi UB hai?
Steps:
UINT_MAX = 2 32 − 1 = 4294967295 . 1 add karne par 4294967296 = 2 32 milta hai.
Yeh step kyun? Wrap rule apply karne se pehle true sum establish karo.
Unsigned arithmetic defined hai 2 n modulo wrap karne ke liye. Toh w = 4294967296 mod 2 32 = 0 .
Yeh step kyun? Yahi parent ka key contrast hai: unsigned wrap guaranteed hai, signed overflow UB hai.
Yeh koi bug nahi hai — yeh woh tool hai jab tum chahte ho wraparound.
Yeh step kyun? Ring-counter behaviour ke liye deliberately unsigned/uint32_t use karo.
Verify: ( UINT_MAX + 1 ) mod 2 32 = 4294967296 mod 4294967296 = 0 . Defined aur exact.
Worked example Ex 10 — "Safety check"
-O2 par kyun gayab ho jaata hai
int over ( int x ) {
if (x + 1 < x) return - 1 ; // "detect overflow"
return x + 1 ;
}
Forecast: INT_MAX par, kya return -1 line kabhi run hoti hai?
Steps:
Programmer ki umeed hai: jab x = INT_MAX ho, x + 1 overflow ho kar chhoti number ban jaaye, toh x + 1 < x true hoga → return − 1 .
Yeh step kyun? Pehle intended (galat) mental model samjho.
Lekin signed overflow UB hai, aur optimizer ko assume karne ki permission hai ki UB kabhi nahi hoti . Us assumption ke under, mathematically x + 1 < x hamesha false hai.
Yeh step kyun? Compiler defined world mein reason karta hai jahan overflow impossible hai.
Hamesha false hone ki wajah se, compiler if aur uska return -1 delete kar deta hai . Safety net chali gayi — -O2 par function bas return x + 1 karta hai, jo top par undefined hai.
Yeh step kyun? Yahi "UB propagate karta hai aur code remove karta hai" behaviour hai parent ke optimizer model se.
Fix — defined domain mein reason karo (add karne se pehle compare karo):
if (x == INT_MAX) return - 1 ;
return x + 1 ;
Yeh step kyun? Comparison x == INT_MAX kabhi overflow nahi karta, toh compiler ise delete nahi kar sakta.
Verify: sirf woh x jiske liye x + 1 overflow karta hai, woh hai x = INT_MAX = 2147483647 ; fixed guard exactly wahan fire karta hai aur kahin nahi. x = 5 ke liye, fixed code 6 return karta hai.
Recall Har cell kaun sa tha?
Ex 1 :: Cell A — positive signed overflow
Ex 2 :: Cell B — INT_MIN / -1
Ex 3 :: Cell C — divide/modulo by zero
Ex 4 :: Cell D — shift out of range
Ex 5 :: Cell E — out-of-bounds read
Ex 6 :: Cell F — sequencing double-modify
Ex 7 :: Cell G — dangling pointer lifetime
Ex 8 :: Cell H — varargs %d vs double
Ex 9 :: Cell I — unsigned wrap (defined control case)
Ex 10 :: Cell J — the vanishing overflow check
Mnemonic Ek line yaad rakhne ke liye
Edge se pehle, baad mein nahi. Range check karo add karne se pehle , divisor check karo divide karne se pehle , index check karo padhne se pehle , aur lifetime check karo dereference karne se pehle — kyunki edge ke baad, standard tumhara kuch nahi.