gdb দিয়ে Instruction-Level Debugging
Debugging with gdb
gdb দিয়ে আগের এগারোটা লেসনের প্রতিটা ধারণা — calling convention, stack frame, addressing mode, GOT/PLT — একটা সত্যিকারের চলমান প্রসেসে সরাসরি চোখে দেখা যায়, breakpoint-এ থামিয়ে instruction-বাই-instruction।
আগে এটা বুঝি
এগারোটা লেসন ধরে আমরা assembly পড়েছি — কাগজে, objdump-এর স্থির
আউটপুটে, readelf-এর টেবিলে। প্রতিবারই একটা প্রশ্ন বাকি থেকে গেছে:
এই সব কি সত্যিই ঘটে, এই মুহূর্তে, একটা চলমান প্রোগ্রামে?
rdi-তে কি সত্যিই argument বসে, ঠিক call-এর আগমুহূর্তে? Stack-এ
কি সত্যিই saved rbp আর return address ঠিক সেই অফসেটে বসে যা
lesson ৫-এ আমরা এঁকেছিলাম? একটা cmp-এর পর jns কি সত্যিই flag
দেখে branch নেয়, নাকি এটা শুধু তত্ত্ব?
এই লেসনে আমরা একটা প্রোগ্রামকে থামিয়ে, ভেতরে ঢুকে, প্রতিটা দাবি
নিজে চোখে যাচাই করব — gdb (GNU Debugger) দিয়ে। এটাই এই
module-এর driving question-এর চূড়ান্ত উত্তর: “আমার C function
compile হয়ে ঠিক কোন instruction-গুলো হলো, আর কেন?” — gdb হলো
সেই টুল যা দিয়ে উত্তরটা প্রমাণ করা যায়, শুধু অনুমান না।
// sum.c
#include \<stdio.h>
int add(int a, int b) {
int result = a + b;
return result;
}
int classify(int n) {
if (n \< 0) {
return -1;
}
return 1;
}
int main(void) {
int x = 10, y = 32;
int total = add(x, y);
int sign_neg = classify(total - 100); /* -58 → ঋণাত্মক পথ */
int sign_pos = classify(total); /* 42 → ধনাত্মক পথ */
printf("total=%d neg=%d pos=%d\n", total, sign_neg, sign_pos);
return 0;
}$ gcc -O0 -g -o sum sum.c
$ gdb ./sumএই একটা ছোট প্রোগ্রাম দিয়েই পুরো লেসন চলবে — calling convention, stack frame, আর একটা conditional branch, তিনটাই এর ভেতরে আছে।
মূল ধারণা
Core workflow — পাঁচটা কমান্ড যথেষ্ট শুরু করতে
break \<location> ব্রেকপয়েন্ট বসানো — নির্দিষ্ট function/line/address-এ থামা
run প্রোগ্রাম চালানো, প্রথম ব্রেকপয়েন্ট পর্যন্ত
next একটা source লাইন এগোনো — function call এলে ভেতরে না ঢুকে টপকে যাওয়া
step একটা source লাইন এগোনো — function call এলে ভেতরে ঢোকা
continue পরের ব্রেকপয়েন্ট পর্যন্ত চালিয়ে যাওয়া(gdb) break add
Breakpoint 1 at 0x1157: file sum.c, line 4.
(gdb) run
Starting program: /home/user/sum
Breakpoint 1, add (a=10, b=32) at sum.c:4
4 int result = a + b;লক্ষ্য করুন add (a=10, b=32) — gdb ইতিমধ্যেই argument-এর প্রকৃত
মান দেখাচ্ছে, যদিও আমরা শুধু break add বলেছি। এটা কাজ করে
compile-time-এ -g দিয়ে যোগ করা debug information (DWARF
format, .debug_info section — গত লেসনের .debug_* section)-এর
কারণে, যেটা gdb-কে বলে দেয় কোন variable কোন stack offset-এ, কোন
address কোন source লাইনের সাথে মেলে।
Instruction-level কমান্ড — এই লেসনের কেন্দ্র
disassemble \<function> gdb এই মুহূর্তে যে assembly execute করছে তা দেখানো
info registers সব general-purpose register-এর বর্তমান মান
x/\<n><f><u> \<addr> memory-র একটা ঠিকানার বিষয়বস্তু examine করা
stepi / nexti একটা মাত্র machine instruction এগোনো (source লাইন না)x কমান্ডের format ভাঙা যাক: x/4xw $rsp মানে — 4টা unit,
x = hexadecimal ফরম্যাটে, w = word (4 byte) সাইজে, ঠিকানা
$rsp। কিছু সাধারণ variant:
| ফরম্যাট | মানে |
|---|---|
x/4xw addr | ৪টা 4-byte hex value |
x/2gx addr | ২টা 8-byte (giant) hex value |
x/5i addr | ৫টা instruction, disassemble করে |
x/s addr | একটা null-terminated string |
x/dw addr | একটা 4-byte signed decimal value |
next বনাম nexti, step বনাম stepi
চারটা কমান্ডকে দুই অক্ষে ভাগ করা যায়:
| line-level | instruction-level | |
|---|---|---|
| টপকে যায় (call-এ না ঢুকে) | next (n) | nexti (ni) |
| ভেতরে ঢোকে | step (s) | stepi (si) |
next/step একটা সম্পূর্ণ source লাইন এগোয় — -O0-এ একটা
সাধারণ লাইন প্রায়ই ৩-৬টা instruction-এ কম্পাইল হয় (গত উদাহরণেই
int result = a + b; চারটা instruction), আর next/step সবগুলো
এক ধাপে চালিয়ে দেয়, পরের source লাইনে না পৌঁছানো পর্যন্ত
থামে না। nexti/stepi একবারে ঠিক একটা machine instruction
চালায় — এটাই “instruction-level debugging”-এর প্রকৃত সংজ্ঞা, আর
এই লেসনের বাকি অংশ মূলত এই দুটো কমান্ড ঘিরেই।
ভেতরে কী ঘটছে
কেন breakpoint prologue-এর পরে বসে
break add করলে gdb breakpoint বসাল 0x1157-এ, add-এর নিজের
প্রথম instruction (0x1149, endbr64) না। কারণ: gdb ডিফল্টভাবে
debug-info দেখে ফাংশনের prologue (push rbp; mov rbp,rsp; আর
argument-কে stack-এ কপি করা — গত module-এর stack-frame লেসন)
টপকে প্রথম প্রকৃত source-level statement-এ থামে। এটা ব্যবহারিক
কারণে — prologue শেষ না হলে rbp-ভিত্তিক local variable এখনো
সঠিকভাবে স্থাপিত না, তাই gdb-এর “a=10, b=32” দেখানোর ক্ষমতাও
নির্ভর করে prologue শেষ হওয়ার উপর।
নিজে prologue-এর আগে থামতে চাইলে, address দিয়ে breakpoint দিন:
(gdb) break *add* চিহ্নটা gdb-কে বলে “এটা একটা function নাম না ভেবো, বরং
add-এর symbol-address-এ সরাসরি breakpoint বসাও” — এই ক্ষেত্রে
সেই ঠিকানাটাই ফাংশনের প্রথম byte।
Prologue-এর আগে register দেখা — argument populate হওয়ার মুহূর্ত
main-এর disassembly প্রথমে দেখা যাক, add-কে call করার ঠিক
আগ পর্যন্ত:
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000000011a0 \<+0>: endbr64
0x00000000000011a4 \<+4>: push rbp
0x00000000000011a5 \<+5>: mov rbp,rsp
0x00000000000011a8 \<+8>: sub rsp,0x20
0x00000000000011ac \<+12>: mov DWORD PTR [rbp-0x14],0xa
0x00000000000011b3 \<+19>: mov DWORD PTR [rbp-0x10],0x20
0x00000000000011ba \<+26>: mov edx,DWORD PTR [rbp-0x10]
0x00000000000011bd \<+29>: mov eax,DWORD PTR [rbp-0x14]
0x00000000000011c0 \<+32>: mov esi,edx
0x00000000000011c2 \<+34>: mov edi,eax
0x00000000000011c4 \<+36>: call 0x1149 \<add>
0x00000000000011c9 \<+41>: mov DWORD PTR [rbp-0xc],eax
0x00000000000011cc \<+44>: mov eax,DWORD PTR [rbp-0xc]
0x00000000000011cf \<+47>: sub eax,0x64
0x00000000000011d2 \<+50>: mov edi,eax
0x00000000000011d4 \<+52>: call 0x1170 \<classify>
0x00000000000011d9 \<+57>: mov DWORD PTR [rbp-0x8],eax
0x00000000000011dc \<+60>: mov eax,DWORD PTR [rbp-0xc]
0x00000000000011df \<+63>: mov edi,eax
0x00000000000011e1 \<+65>: call 0x1170 \<classify>
0x00000000000011e6 \<+70>: mov DWORD PTR [rbp-0x4],eax
0x00000000000011e9 \<+73>: mov ecx,DWORD PTR [rbp-0x4]
0x00000000000011ec \<+76>: mov edx,DWORD PTR [rbp-0x8]
0x00000000000011ef \<+79>: mov eax,DWORD PTR [rbp-0xc]
0x00000000000011f2 \<+82>: mov esi,eax
0x00000000000011f4 \<+84>: lea rdi,[rip+0xe05] # 0x2000
0x00000000000011fb \<+91>: call 0x1050 \<printf@plt>
0x0000000000001200 \<+96>: mov eax,0x0
0x0000000000001205 \<+101>: leave
0x0000000000001206 \<+102>: ret
End of assembler dump.দুইটা জিনিস চোখে পড়া উচিত এখনই। প্রথম, total-এর জন্য rbp-0xc,
sign_neg-এর জন্য rbp-0x8, sign_pos-এর জন্য rbp-0x4 — গত
module-এর stack-frame layout ঠিক এভাবেই কাজ করে। দ্বিতীয়, শেষ
call — call 0x1050 \<printf@plt> — গত লেসনের PLT, সরাসরি এই
main-এর ভেতরে।
এখন call add-এর ঠিক আগে breakpoint বসিয়ে argument populate
হওয়ার মুহূর্তটা ধরি:
(gdb) break *0x11c4
Breakpoint 2 at 0x11c4: file sum.c, line 3.
(gdb) run
Starting program: /home/user/sum
Breakpoint 2, 0x00000000000011c4 in main () at sum.c:3
3 int total = add(x, y);
(gdb) info registers rdi rsi
rdi 0xa 10
rsi 0x20 32এই মুহূর্তটাই System V ABI-র calling convention সরাসরি চোখে
দেখা — add(x, y) কল হওয়ার ঠিক আগে, প্রথম দুইটা integer argument
rdi, rsi-তে বসে গেছে, ঠিক যেমন লেসন ৪ দাবি করেছিল। কোনো তত্ত্ব
না — এই মুহূর্তে সত্যিকারের একটা প্রসেসের রেজিস্টারে সত্যিই এই মান।
উদাহরণ
Stack frame সরাসরি হাঁটা
add-এর ভেতরে ঢুকে, prologue-এর পরের breakpoint-এ:
(gdb) continue
Continuing.
Breakpoint 1, add (a=10, b=32) at sum.c:4
4 int result = a + b;
(gdb) info registers rbp rsp
rbp 0x7fffffffe440 0x7fffffffe440
rsp 0x7fffffffe440 0x7fffffffe440rbp আর rsp সমান — অস্বাভাবিক মনে হতে পারে, কিন্তু এটা ঠিক।
add-এ কোনো explicit sub rsp, N নেই (উপরের disassemble add
আউটপুটে যাচাই করুন — push rbp; mov rbp,rsp সরাসরি argument-store-এ
চলে যায়)। কারণ: add একটা leaf function — এটা নিজে অন্য কোনো
ফাংশন call করে না, তাই System V ABI-র red zone (rsp-এর নিচের
১২৮ byte, যা leaf function কোনো sub rsp ছাড়াই নিরাপদে ব্যবহার
করতে পারে) দিয়ে কাজ চলে যাচ্ছে — গত module-এর stack-frame লেসনের
একটা optimization বাস্তবে ধরা পড়ল।
এখন frame-টা নিজেই dump করি:
(gdb) x/2gx $rbp
0x7fffffffe440: 0x00007fffffffe470 0x00000000000011c9দুইটা 8-byte (giant) hex value, $rbp থেকে শুরু:
[rbp]=0x00007fffffffe470— এটাইmain-এর savedrbp(caller-এর frame pointer,push rbpদিয়ে সংরক্ষিত)[rbp+8]=0x00000000000011c9— এটাই return address
যাচাই করা যাক দ্বিতীয়টা — এটা কি সত্যিই সেই ঠিকানা যেখানে main
call add-এর পর ফিরে আসবে?
(gdb) x/i 0x11c9
0x11c9 \<main+41>: mov DWORD PTR [rbp-0xc],eaxমিলে গেল — উপরের disassemble main আউটপুটে ঠিক এই লাইনটাই ছিল
call-এর (0x11c4) পরের instruction। call instruction নিজে
৫ byte (E8 + ৪ byte relative offset), তাই 0x11c4 + 5 = 0x11c9 —
return address হিসেবে ঠিক এটাই stack-এ push হয়েছিল, call
instruction চলার সময়েই।
Argument দুটোও নিজের অফসেটে যাচাই করা যাক:
(gdb) x/dw $rbp-0x14
0x7fffffffe42c: 10
(gdb) x/dw $rbp-0x18
0x7fffffffe428: 32a (10) [rbp-0x14]-এ, b (32) [rbp-0x18]-এ — ঠিক
disassemble add-এর mov DWORD PTR [rbp-0x14],edi / mov DWORD PTR [rbp-0x18],esi অনুযায়ী।
- $rbp (0x7fffffffe440)এই frame-এর ভিত্তি, prologue-এ push rbp; mov rbp,rsp দিয়ে স্থাপিত
- [rbp] = 0x...e470saved rbp — caller (main)-এর frame pointer, ফেরার পর restore হবে
- [rbp+8] = 0x...11c9return address — call instruction-এর ঠিক পরের byte, main+41
- [rbp-0x14] = 10a — rdi থেকে prologue-এ কপি করা প্রথম argument
- [rbp-0x18] = 32b — rsi থেকে prologue-এ কপি করা দ্বিতীয় argument
- [rbp-0x4] = (এখনো অনির্ধারিত)result — এই লাইনটাই এখনো চলেনি
এই একটাই memory dump লেসন ৫-এর সম্পূর্ণ stack-frame diagram-কে byte-এ byte-এ যাচাই করে দিল।
Conditional branch — flag দেখে সরাসরি taken/not-taken
classify-এর disassembly:
(gdb) disassemble classify
Dump of assembler code for function classify:
0x0000000000001170 \<+0>: endbr64
0x0000000000001174 \<+4>: push rbp
0x0000000000001175 \<+5>: mov rbp,rsp
0x0000000000001178 \<+8>: mov DWORD PTR [rbp-0x4],edi
0x000000000000117b \<+11>: cmp DWORD PTR [rbp-0x4],0x0
0x000000000000117f \<+15>: jns 0x118b \<classify+27>
0x0000000000001181 \<+17>: mov eax,0xffffffff
0x0000000000001186 \<+22>: jmp 0x1190 \<classify+32>
0x000000000000118b \<+27>: mov eax,0x1
0x0000000000001190 \<+32>: pop rbp
0x0000000000001191 \<+33>: ret
End of assembler dump.jns = “Jump if Not Sign” — sign flag (SF) 0 হলে jump করে
(মান অ-ঋণাত্মক), SF=1 হলে জাম্প না করে পরের instruction-এ
পড়ে (মান ঋণাত্মক, -1 রিটার্নের পথে)। cmp DWORD PTR [rbp-0x4],0x0 আসলে n - 0-এর ফলাফলের ভিত্তিতে flag সেট করে, ফলাফল
নিজে কোথাও সংরক্ষণ না করেই — flag-ই যথেষ্ট।
jns-এ breakpoint বসিয়ে দুইবার দেখি — একবার n = -58 (ঋণাত্মক
পথ), একবার n = 42 (ধনাত্মক পথ):
(gdb) break *0x117f
Breakpoint 3 at 0x117f: file sum.c, line 8.
(gdb) continue
Continuing.
Breakpoint 3, 0x000000000000117f in classify (n=-58) at sum.c:8
8 if (n \< 0) {
(gdb) info registers eflags
eflags 0x286 [ IF PF SF ]SF পতাকাটা তালিকায় আছে — সেট হয়ে আছে, কারণ -58 - 0 = -58,
ঋণাত্মক। jns তাই জাম্প করবে না:
(gdb) stepi
0x0000000000001181 8 if (n \< 0) {
(gdb) print $rip
$1 = (void (*)()) 0x1181 \<classify+17>$rip এখন 0x1181 — জাম্পের ঠিক পরের instruction, 0x118b-এ
না। মানে jns not-taken — mov eax,0xffffffff পথে পড়ল, -1
রিটার্ন হবে।
দ্বিতীয়বার, n = 42:
(gdb) continue
Continuing.
Breakpoint 3, 0x000000000000117f in classify (n=42) at sum.c:8
8 if (n \< 0) {
(gdb) info registers eflags
eflags 0x202 [ IF ]এবার SF তালিকায় নেই — 42 - 0 = 42, অ-ঋণাত্মক। jns তাই
জাম্প করবে:
(gdb) stepi
0x000000000000118b 8 if (n \< 0) {
(gdb) print $rip
$2 = (void (*)()) 0x118b \<classify+27>$rip লাফ দিয়ে 0x118b-এ পৌঁছাল — jmp (0x1186-এর) এড়িয়ে
সরাসরি mov eax,0x1-এ, taken জাম্প। একই instruction, একই কোড,
দুইটা আলাদা মান — আর eflags রেজিস্টারের একটামাত্র bit-ই ঠিক করে
দিল কোন পথে যাবে। এটাই লেসন ৬-এর “control flow = compare + branch”
দাবির সবচেয়ে সরাসরি প্রমাণ।
নিজে চালিয়ে দেখুন
নিজে চালিয়ে পুরো session-টা reproduce করুন
gcc -O0 -g -o sum sum.c
gdb ./sumধাপে ধাপে:
(gdb) set disassembly-flavor intel
(gdb) disassemble main
(gdb) disassemble add
(gdb) disassemble classify
(gdb) break *0x11c4 # আপনার নিজের disassembly থেকে ঠিক ঠিকানা বসান
(gdb) run
(gdb) info registers rdi rsi
(gdb) continue
(gdb) x/2gx $rbp
(gdb) x/i $rbp+8 # সরাসরি অফসেট দিয়ে — একই কাজ 0x11c9 হাতে লেখার বদলেনিজের ঠিকানা মেলাবেন না আমার লেখাগুলোর সাথে হুবহু — compiler
ভার্সন, distro, এমনকি একই gcc-র ভিন্ন বিল্ড ভিন্ন byte-offset
দিতে পারে (function alignment, prologue-এর সামান্য ভিন্নতা)। যা
মেলা উচিত: প্যাটার্নটা — push rbp; mov rbp,rsp, তারপর
argument store, তারপর [rbp]-এ saved rbp আর [rbp+8]-এ return
address।
eflags অংশটার জন্য:
(gdb) disassemble classify
(gdb) break *\<jns-এর ঠিকানা, আপনার disassembly থেকে>
(gdb) run
(gdb) info registers eflags
(gdb) stepi
(gdb) print $rip
(gdb) continue
(gdb) info registers eflags
(gdb) stepi
(gdb) print $ripদুইবার continue-এর মধ্যে eflags-এর SF bit-এর উপস্থিতি/অনুপস্থিতি
তুলনা করুন, আর দুইবার $rip-এর চূড়ান্ত মান — একটা +17 (not-taken)
আরেকটা +27-জাতীয় (taken) অফসেটে যাওয়া উচিত (আপনার নিজের বিল্ডের
প্রকৃত অফসেট অনুযায়ী)।
অতিরিক্ত challenge: watch result কমান্ড দিয়ে (একটা watchpoint
— নির্দিষ্ট memory বদলালেই থামে, breakpoint-এর মতো নির্দিষ্ট
ঠিকানায় না) add-এর ভেতরে result variable-এ কখন লেখা হয় তা
breakpoint ছাড়াই ধরুন।
এই লেসনের প্রতিটা claim — argument populate হওয়া, stack frame-এর ঠিকানা, conditional jump-এর flag-নির্ভরতা — একটা বাস্তব প্রসেসে হুবহু reproduce করা যায়, শুধু পড়া তথ্য না।
নিজে বানান
নিজের হাতে backtrace — একটা gdb Python কমান্ড
- gdb-এর Python API ব্যবহার করে একটা নতুন কমান্ড define করুন
- বর্তমান $rbp থেকে শুরু করে frame chain হাতে হাঁটুন — [rbp]-এ saved rbp, [rbp+8]-এ return address
- প্রতিটা ধাপে return address-কে info symbol দিয়ে function নামে রূপান্তর করুন
- gdb-এর নিজস্ব bt কমান্ডের সাথে ফলাফল মিলিয়ে যাচাই করুন
মূল ধারণা: gdb-এর নিজস্ব backtrace (bt) কমান্ড ঠিক এই লেসনের
x/2gx $rbp কৌশলটাই বারবার করে, প্রতিটা frame-এ [rbp]-এর মান
পরের frame-এর rbp হিসেবে ব্যবহার করে — একটা linked list-এর মতো
frame chain হাঁটা। নিজে লিখলে বোঝা যায় bt আসলে কী “জাদু” করে না,
শুধু এই একই manual কাজটা স্বয়ংক্রিয় করে দেয়।
# walkframes.py — (gdb) source walkframes.py দিয়ে লোড করুন
import gdb
class WalkFrames(gdb.Command):
"""হাতে frame chain walk করে saved rbp আর return address প্রিন্ট করে।"""
def __init__(self):
super().__init__("walkframes", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
rbp = int(gdb.parse_and_eval("$rbp"))
depth = int(arg) if arg else 5
inferior = gdb.selected_inferior()
for i in range(depth):
if rbp == 0:
break
mem = inferior.read_memory(rbp, 16)
saved_rbp = int.from_bytes(bytes(mem[0:8]), "little")
return_addr = int.from_bytes(bytes(mem[8:16]), "little")
try:
sym = gdb.execute(f"info symbol 0x{return_addr:x}", to_string=True).strip()
except gdb.error:
sym = "অজানা"
print(f"frame {i}: rbp=0x{rbp:x} saved_rbp=0x{saved_rbp:x} "
f"return_addr=0x{return_addr:x} ({sym})")
if saved_rbp <= rbp: # main-এর উপরে গেলে চেইন থেমে যাওয়া উচিত
break
rbp = saved_rbp
WalkFrames()(gdb) break add
(gdb) run
(gdb) source walkframes.py
(gdb) walkframes 3প্রত্যাশিত ফলাফল (এই লেসনের add-এর breakpoint থেকে):
frame 0: rbp=0x7fffffffe440 saved_rbp=0x7fffffffe470 return_addr=0x11c9 (mov $0x0,%eax in section .text of sum)
frame 1: rbp=0x7fffffffe470 saved_rbp=0x7fffffffe4a0 return_addr=0x... (__libc_start_call_main in section .text of libc.so.6)frame 0-এর মান এই লেসনের example অংশের x/2gx $rbp ফলাফলের
সাথে হুবহু মিলে যাওয়া উচিত — কারণ এটা ঠিক সেই একই memory read,
শুধু হাতে-লেখা loop-এর মধ্য দিয়ে।
এবার gdb-এর নিজের কমান্ড দিয়ে যাচাই করুন:
(gdb) bt 3
#0 add (a=10, b=32) at sum.c:4
#1 0x00000000000011c9 in main () at sum.c:3
#2 0x... in __libc_start_call_main (...)#1-এর ঠিকানা (0x11c9) আপনার walkframes-এর frame 0-এর
return_addr-এর সাথে মেলা উচিত — দুইটা সম্পূর্ণ ভিন্ন implementation
(gdb-এর নিজস্ব C কোড বনাম আপনার Python স্ক্রিপ্ট), একই raw memory
পড়ে, একই উত্তর দিচ্ছে।
নিজে বাড়ান:
- প্রতিটা frame-এ শুধু return address না, সেই frame-এর local
variable-ও (DWARF debug info দিয়ে,
gdb.FrameAPI ব্যবহার করে) প্রিন্ট করুন - একটা recursive function (যেমন factorial) দিয়ে পরীক্ষা করুন —
walkframesকি recursion depth ঠিকভাবে ধরতে পারে? - Frame pointer omission (
gcc -O2 -fomit-frame-pointer, ডিফল্ট-O2-এ) দিয়ে কম্পাইল করে চালান —rbp-ভিত্তিক এই কৌশলটা কি এখনো কাজ করে? (উত্তর: না — কেন সেটাই lesson ৮-এর-O2পাঠের একটা সরাসরি পরিণতি, gdb তখন.eh_frame/CFI তথ্যের উপর নির্ভর করে)
বাস্তব সিস্টেমে
gdb যেখানে সিদ্ধান্ত নেয়
Core dump post-mortem debugging। Production-এ একটা সার্ভার
crash করলে, ulimit -c unlimited সেট থাকলে kernel একটা core
ফাইল লেখে (গত লেসনের ET_CORE)। gdb ./server core দিয়ে সেই
crash-এর মুহূর্তের সম্পূর্ণ state — সব register, সব stack frame —
পরে বিশ্লেষণ করা যায়, প্রোগ্রামটা আবার crash করার অপেক্ষা না করেই।
Remote debugging — embedded/IoT। gdbserver একটা target
device-এ (Raspberry Pi, একটা microcontroller board) চলে, আসল gdb
ডেভেলপারের ল্যাপটপে চলে, target remote host:port দিয়ে সংযুক্ত
হয়। JTAG/SWD ডিবাগ প্রোব-ভিত্তিক embedded debugging (OpenOCD +
gdb) একই gdb কমান্ড set ব্যবহার করে, এমনকি OS-বিহীন bare-metal
firmware-এও।
Reverse debugging — rr। Mozilla-র rr টুল একটা প্রোগ্রামের
পুরো execution রেকর্ড করে, তারপর gdb-এর ভেতর থেকে পিছনের দিকে
step করা যায় (reverse-stepi, reverse-continue) — “এই ভ্যারিয়েবলটা
কখন এই ভুল মানটা পেল” প্রশ্নের উত্তর খুঁজতে অসাধারণ কার্যকর, যেখানে
normal forward debugging-এ বারবার re-run করে বাগ পুনরায় ঘটানো লাগত।
Kernel debugging — KGDB/KDB। Linux kernel নিজেই gdb দিয়ে
ডিবাগ করা যায় (KGDB), দুইটা মেশিনের মধ্যে সিরিয়াল/নেটওয়ার্ক
কানেকশনে — একই break/step/info registers মানসিকতা, শুধু এবার
টার্গেট একটা user process না, পুরো অপারেটিং সিস্টেম নিজেই।
Exploit development — pwndbg/GEF। নিরাপত্তা গবেষকরা gdb-কে
pwndbg বা GEF-এর মতো plugin দিয়ে সমৃদ্ধ করেন — stack/heap
visualization, ROP gadget খোঁজা, GOT overwrite দেখা (গত লেসনের
GOT/PLT আক্রমণের ভিত্তি) — সবই এই লেসনের মৌলিক x, info registers
কমান্ডের উপর তৈরি একটা layer।
IDE integration। VS Code, CLion, Qt Creator-এর গ্রাফিকাল
ডিবাগার — breakpoint-এ ক্লিক, variable hover — এদের বেশিরভাগ পেছনে
আসলে gdb-কে machine interface mode (gdb --interpreter=mi)-এ
চালায়, ঠিক এই লেসনের কমান্ডগুলোই পাঠায়, শুধু আউটপুট একটা
structured format-এ পার্স করে GUI-তে দেখায়।
Conditional breakpoint দিয়ে production-স্টাইল debugging।
break add if a > 100 — শুধু নির্দিষ্ট শর্তে থামা, হাজার হাজার
call-এর মধ্যে একটা নির্দিষ্ট, বিরল case ধরার জন্য অমূল্য। এই লেসনের
n = -58 বনাম n = 42 উদাহরণে যদি হাজার হাজার call থাকত, break *0x117f if n > 1000000 দিয়ে শুধু আগ্রহের case-এ থামা যেত।
যে ভুলগুলো সবাই করে
“gdb কোড interpret করে চালায়, তাই এটা 'আসল' execution না।”
সম্পূর্ণ ভুল, আর এটা বোঝা গুরুত্বপূর্ণ। gdb যে প্রোগ্রামটা ডিবাগ
করছে সেটা স্বাভাবিকভাবে compile হওয়া নেটিভ মেশিন কোড, সরাসরি CPU-তে
চলছে — ঠিক gdb ছাড়া চালালে যা হতো, হুবহু একই instruction, একই
গতি (breakpoint-এ থামা মুহূর্ত ছাড়া)। gdb ptrace() সিস্টেম কল
ব্যবহার করে (Linux-এ) — এটা kernel-কে বলে “এই প্রসেসটার উপর আমাকে
নিয়ন্ত্রণ দাও: single-step করাতে দাও, register পড়তে/লিখতে দাও,
breakpoint-এ থামাতে দাও (int3 instruction বসিয়ে)।”
break add আসলে add-এর প্রথম instruction-এর byte-টা সাময়িকভাবে
0xCC (int3, “trap” instruction) দিয়ে বদলে দেয় — CPU সেই byte-এ
পৌঁছালে একটা trap তৈরি হয়, kernel gdb-কে জানায়, gdb আসল byte
ফিরিয়ে রাখে আর নিয়ন্ত্রণ দেখায়। এটা interpretation না — এটা
আসল হার্ডওয়্যার-স্তরের একটা কৌশল, নেটিভ execution-এর মধ্যেই।
“step সবসময় নিরাপদ — কখনো লাইব্রেরি কোডে হারিয়ে যাবেন না।”
step যেকোনো function call-এর ভেতরে ঢোকে, সেটা আপনার নিজের
কোড হোক বা libc-এর printf-এর মতো লাইব্রেরি ফাংশন — যদি সেই
ফাংশনের জন্য debug info (-g) না থাকে (যা printf-এর জন্য
সাধারণত থাকে না, libc সাধারণত strip করা), gdb শুধু assembly-স্তরে
আটকে থাকে, বা বিভ্রান্তিকরভাবে ?? দেখায়।
এই কারণেই next বেশি ব্যবহৃত হয় দৈনন্দিন ডিবাগিংয়ে — এটা call-কে
পুরোপুরি চালিয়ে (ভেতরে না ঢুকে) পরের লাইনে যায়। step তখনই
ব্যবহার করুন যখন সত্যিই ভেতরে ঢুকতে চান, নিজের বা debug-info-সহ
কোনো লাইব্রেরির কোডে। skip কমান্ড (skip function printf) দিয়ে
নির্দিষ্ট ফাংশনকে স্থায়ীভাবে step-এর আওতার বাইরে রাখা যায়।
“disassemble মানেই gdb কোনো special debug-build দরকার, রিলিজ বাইনারিতে কাজ করবে না।”
disassemble, info registers, x, stepi/nexti — এই লেসনের
সব instruction-level কমান্ডের জন্য debug info লাগেই না। এগুলো
সরাসরি machine code আর CPU state পড়ে, কোনো .debug_* section
ছাড়াই কাজ করে — একটা সম্পূর্ণ strip-করা রিলিজ বাইনারিতেও।
যা debug info (-g) ছাড়া কাজ করবে না: source-line mapping
(4 int result = a + b; দেখানো), variable নাম দিয়ে access
(print a — অফসেট না জানলে gdb বুঝবে না a মানে কী), আর next/
step-এর “একটা সম্পূর্ণ source লাইন” ধারণা (কারণ “লাইন” concept-টাই
debug info থেকে আসে)।
একটা strip করা বাইনারিতে আপনি এখনো disassemble 0x1149,0x1170
(ঠিকানার range দিয়ে) বা stepi করতে পারবেন — শুধু add (a=10, b=32) at sum.c:4-এর বদলে খালি ঠিকানা আর register দেখবেন,
human-friendly নাম-ম্যাপিং ছাড়া।
বুঝেছেন কি না দেখুন
1break add দিলে gdb breakpoint বসায় add-এর প্রথম instruction-এ
না, বরং prologue শেষ হওয়ার পরের instruction-এ। এই আচরণটা কেন
ব্যবহারিকভাবে সুবিধাজনক, আর কখন এটা আপনার উদ্দেশ্যের বিপরীতে যেতে
পারে?
যুক্তি
break add দিলে gdb breakpoint বসায় add-এর প্রথম instruction-এ
না, বরং prologue শেষ হওয়ার পরের instruction-এ। এই আচরণটা কেন
ব্যবহারিকভাবে সুবিধাজনক, আর কখন এটা আপনার উদ্দেশ্যের বিপরীতে যেতে
পারে?সুবিধা: prologue (push rbp; mov rbp,rsp; + argument-কে
stack-এ কপি) শেষ না হলে, rbp-ভিত্তিক local variable আর argument
এখনো তাদের “স্থায়ী” জায়গায় (stack slot) বসেনি — কিছু হয়তো এখনো
শুধু register-এ। gdb-এর add (a=10, b=32) জাতীয় human-friendly
প্রদর্শন এই কপি সম্পন্ন হওয়ার উপর নির্ভর করে (DWARF debug info
সাধারণত variable-এর অবস্থান stack-offset হিসেবে এনকোড করে, prologue
শেষ হওয়ার পর থেকে বৈধ ধরে নিয়ে)। Prologue টপকে থামলে ডেভেলপার
সাথে সাথে অর্থবহ variable মান দেখতে পান, বিভ্রান্তিকর আংশিক-অবস্থা
না।
যখন এটা বিপরীতে যায়: যদি আপনি ঠিক prologue নিজে কীভাবে
কাজ করে সেটা পড়াতে/দেখাতে চান (এই লেসনের মতো) — push rbp কখন
ঘটে, rsp-এর মান prologue-এর আগে-পরে কেমন বদলায় — তাহলে ডিফল্ট
break add আপনাকে সেই মুহূর্তগুলো মিস করিয়ে দেবে, কারণ ততক্ষণে
prologue ইতিমধ্যে চলে গেছে। সমাধান: break *add (ঠিকানা-ভিত্তিক,
* সহ) — এটা প্রথম instruction-এই থামে, prologue সহ পুরোটাই
পর্যবেক্ষণযোগ্য।
আরেকটা বাস্তব case: security research-এ, একটা function-এর
argument তার আসল, register-ভিত্তিক ফর্মে দেখতে চাইলে (stack-এ
কপি হওয়ার আগেই, exploit-এর জন্য register state গুরুত্বপূর্ণ হতে
পারে) — সেখানেও break *function_name prologue-এর আগে থামায়,
break function_name না।
2একটা function-এ breakpoint বসানো আছে যা recursive (নিজেকে নিজে
call করে)। x/2gx $rbp দিয়ে saved-rbp আর return-address দেখলেন,
তারপর continue দিয়ে আবার একই breakpoint-এ থামলেন (recursive call-এর
কারণে) — এবার $rbp-এর মান কি একই থাকবে, নাকি বদলাবে? ব্যাখ্যা
করুন।
প্রয়োগ
x/2gx $rbp দিয়ে saved-rbp আর return-address দেখলেন,
তারপর continue দিয়ে আবার একই breakpoint-এ থামলেন (recursive call-এর
কারণে) — এবার $rbp-এর মান কি একই থাকবে, নাকি বদলাবে? ব্যাখ্যা
করুন।$rbp-এর মান বদলাবে — প্রতিটা recursive call তার নিজের নতুন
stack frame বানায়, তাই নিজের নতুন rbp মান নিয়ে।
প্রতিবার function call হলে (recursive হোক বা না হোক), push rbp; mov rbp,rsp চলে — নতুন rbp মান হয় এই কলের rsp-এর মান
prologue শুরুর ঠিক আগমুহূর্তে। প্রতিটা recursive call আগেরটার
তুলনায় stack-এ আরও নিচে (numerically ছোট ঠিকানায়, x86-64-এ
stack নিচের দিকে বাড়ে) — তাই দ্বিতীয়বার breakpoint-এ থামলে $rbp
প্রথমবারের চেয়ে ছোট একটা ঠিকানা হবে।
এই লেসনের x/2gx $rbp-এর ফলাফলও তাই বদলাবে: [rbp] (saved rbp)
এবার হবে প্রথম কলের $rbp-এর মান (কারণ সেটাই এই দ্বিতীয় কলের
“caller”), আর [rbp+8] (return address) হবে সেই recursive-call
instruction-এর ঠিক পরের ঠিকানা — যেটা main-এর মধ্যে না, বরং
একই function-এর ভেতরেই, যেখান থেকে সে নিজেকে call করেছিল।
এটাই bt (backtrace) কমান্ড দিয়ে recursive call-এ একই function
নাম বারবার দেখার কারণ — #0 factorial (n=1), #1 factorial (n=2),
#2 factorial (n=3) … প্রতিটা আলাদা $rbp, আলাদা frame, কিন্তু
একই কোড।
3info registers eflags দুইবার চালিয়ে দুইটা ভিন্ন মান পেলেন:
0x286 [ IF PF SF ] আর 0x202 [ IF ]। শুধু bracket-এর ভেতরের
flag নামের তালিকা দেখে (হেক্স মান না দেখেই) কীভাবে বলবেন কোনটাতে
jns জাম্প করবে?
যুক্তি
info registers eflags দুইবার চালিয়ে দুইটা ভিন্ন মান পেলেন:
0x286 [ IF PF SF ] আর 0x202 [ IF ]। শুধু bracket-এর ভেতরের
flag নামের তালিকা দেখে (হেক্স মান না দেখেই) কীভাবে বলবেন কোনটাতে
jns জাম্প করবে?jns জাম্প করে যখন Sign Flag (SF) সেট করা নেই। তালিকায়
SF না থাকা মানে flag clear (0), থাকা মানে set (1)। gdb-এর
info registers eflags শুধু সেট করা flag-গুলোর নাম বন্ধনীর
ভেতরে দেখায় — যা bracket-এ নেই তা clear ধরে নিতে হবে।
0x286 [ IF PF SF ] — SF তালিকায় আছে → set → jns জাম্প
করবে না।
0x202 [ IF ] — SF তালিকায় নেই → clear → jns জাম্প
করবে।
সাধারণ নিয়ম: j<condition>-জাতীয় instruction-এর জন্য ঠিক কোন
flag(গুলো) প্রাসঙ্গিক তা জানতে হবে (jns/js → SF; je/jne
→ ZF; jl/jge → SF আর OF-এর তুলনা; ja/jb → CF/ZF)
— তারপর info registers eflags-এর bracket-এ সেই নির্দিষ্ট flag-এর
উপস্থিতি/অনুপস্থিতি চেক করলেই জাম্প হবে কি না আগে থেকেই বলে দেওয়া
যায়, stepi না করেই।
4একটা প্রোগ্রামে একটা loop ১০,০০০ বার চলে, আর আপনি জানতে চান ঠিক
কোন iteration-এ একটা নির্দিষ্ট variable x-এর মান প্রথমবার 0-এর
নিচে যায়। break + continue বারবার টাইপ করা ছাড়া gdb-তে এটা
কীভাবে কার্যকরভাবে করবেন?
ডিজাইন
x-এর মান প্রথমবার 0-এর
নিচে যায়। break + continue বারবার টাইপ করা ছাড়া gdb-তে এটা
কীভাবে কার্যকরভাবে করবেন?Conditional breakpoint ব্যবহার করুন — শর্তটা gdb-কেই evaluate করতে দিন, প্রতিবার আপনাকে manually চেক করতে হবে না:
(gdb) break loop_body_line if x \< 0এখন continue একবার দিলেই gdb নিজে নিজে হাজার হাজার iteration
চালিয়ে যাবে (breakpoint-এ থামবে ঠিকই প্রতিবার, কিন্তু শর্ত মিথ্যা
হলে সাথে সাথে আবার চালিয়ে যাবে, আপনাকে কিছু টাইপ করতে হবে না),
আর শুধু যেবার x \< 0 সত্যি হয় সেবারই আসল থামা ঘটবে।
বিকল্প, আরও দ্রুত পদ্ধতি — commands:
(gdb) break loop_body_line
Breakpoint 1 at 0x...
(gdb) commands
> silent
> if x >= 0
> continue
> end
> endএটা কার্যত একই কাজ করে, কিন্তু কমান্ড sequence স্পষ্টভাবে লেখা — বড়, জটিল শর্তের জন্য বেশি flexible (একাধিক লাইন লজিক লেখা যায়)।
কেন manual continue স্প্যামিং খারাপ: ১০,০০০ বার আপনাকে
continue টাইপ করতে হতো (বা continue 10000 দিলেও, প্রতিটাতেই
breakpoint hit-এর overhead, আর আপনি জানতেন না ঠিক কতবার লাগবে) —
conditional breakpoint এই পুরো iteration gdb-এর নিজের দ্রুত internal
loop-এ চালায়, শুধু আসল মুহূর্তে থেমে human attention-এর জন্য অপেক্ষা
করে। এটাই gdb-কে interactive debugging-এর বাইরে গিয়ে একটা
programmable টুল বানায় — এই লেসনের walkframes.py build-এর মতোই,
gdb নিজে scriptable।
5main-এর disassembly-তে শেষ printf call-টা call 0x1050 \<printf@plt> — এই একটা লাইন এই পুরো module-এর কোন কোন আগের লেসনের
সাথে সরাসরি যুক্ত, আর gdb দিয়ে সেই সংযোগ কীভাবে verify করবেন?
প্রয়োগ
main-এর disassembly-তে শেষ printf call-টা call 0x1050 \<printf@plt> — এই একটা লাইন এই পুরো module-এর কোন কোন আগের লেসনের
সাথে সরাসরি যুক্ত, আর gdb দিয়ে সেই সংযোগ কীভাবে verify করবেন?দুইটা আগের লেসনের সাথে সরাসরি যুক্ত: Position-Independent Code (GOT/PLT) আর ELF Format (relocations)।
printf@plt মানে এই call printf-এর সরাসরি ঠিকানায় যাচ্ছে না —
PLT stub-এর মধ্য দিয়ে, যেটা GOT-এ থাকা resolved ঠিকানায় jump করে
(PIC লেসনের মূল বিষয়)। gdb দিয়ে এটা সরাসরি verify করা যায়:
(gdb) break *0x11fb
(gdb) run
(gdb) disassemble 0x1050,0x1060
0x0000000000001050 \<printf@plt>: endbr64
0x0000000000001054 \<printf@plt+4>: bnd jmp *0x2fd5(%rip) ; বা Intel-এ jmp [rip+...]এই jmp [rip+offset]-টাই PLT lesson-এর PLT[printf]: jmp [rip+got_offset] diagram-এর হুবহু বাস্তব রূপ। আরও এক ধাপ এগিয়ে,
সেই GOT slot-এর বর্তমান মান দেখা যায়:
(gdb) x/gx 0x4008 ; GOT-এর ঠিকানা readelf -r থেকে
0x4008: 0x00007ffff7c521a0প্রথমবার এই breakpoint-এ পৌঁছানোর আগে যদি GOT পরীক্ষা করেন,
হয়তো এটা এখনো resolver-এর ঠিকানা দেখাবে (lazy binding-এর
RESOLVE-এর আগে) — call-এর পরে একই ঠিকানা দেখলে সেটা আসল
printf-এর ঠিকানা দেখাবে (glibc-তে কোথাও, ASLR-এর কারণে প্রতি
রানে ভিন্ন হবে — যদিও gdb ডিফল্টভাবে debuggee-র জন্য ASLR বন্ধ
রাখে, set disable-randomization off দিয়ে চালু করা যায়)।
এই একটা লাইন — call 0x1050 \<printf@plt> — এভাবে module-এর
শেষ তিনটা লেসনকে (PIC, ELF, gdb) একসাথে এক জায়গায় নিয়ে আসে: PIC
লেসন ব্যাখ্যা করেছিল কেন এই indirection দরকার, ELF লেসন দেখিয়েছিল
এই তথ্য কোথায় ফাইলে সংরক্ষিত (.rela.plt, .dynsym), আর এই
লেসন দেখাল সেটা কীভাবে সরাসরি একটা চলমান প্রসেসে verify করা যায়।
এরপর কী
Module শেষ — এতক্ষণ যা শিখলাম, আর এরপর কী
পিছনে তাকান। বারোটা লেসনে আমরা একটা সম্পূর্ণ সেতু তৈরি করেছি —
মানুষের লেখা a = b + c-এর মতো একটা লাইন থেকে সিলিকনে সত্যিই কী
ঘটে সেই পর্যন্ত।
শুরু করেছিলাম assembler-linker-loader pipeline দিয়ে — সোর্স
কীভাবে ধাপে ধাপে একটা চলমান প্রসেসে রূপান্তরিত হয়। তারপর
x86-64-এর AT&T ও Intel syntax আর তার register set শিখেছি, আর
ARM64-এর RISC/load-store দর্শন দিয়ে একটা বিপরীত ঘরানার তুলনা
পেয়েছি। System V calling convention শিখিয়েছে কীভাবে function
এর মধ্যে argument আর return value ভ্রমণ করে, আর stack
frame/prologue/epilogue দেখিয়েছে সেই ভ্রমণের memory-ভিত্তিক
কাঠামো। Control flow-কে আমরা compare-and-branch হিসেবে দেখেছি
— আজকের jns তারই একটা জীবন্ত উদাহরণ। Addressing mode ও pointer
arithmetic দেখিয়েছে effective address গণনার নিয়ম, আর সেখান থেকেই
-O0 বনাম -O2 compiler output পড়া শিখেছি — এই module-এর driving
question-এর প্রথম বড় payoff। Inline assembly-র honest, বিরল-ব্যবহারের
জায়গাটা বুঝেছি। তারপর position-independent code-এ GOT/PLT
দিয়ে দেখেছি কীভাবে shared library যেকোনো ঠিকানায় নিরাপদে চলে,
ELF format-এ দেখেছি সেই সব তথ্য কীভাবে একটা ফাইলে organized —
section (build-time) বনাম segment (runtime)। আর আজ, gdb দিয়ে
এই সবকিছুকে একটা সত্যিকারের চলমান প্রসেসে সরাসরি চোখে দেখেছি —
register-এ argument বসা, stack-এ frame গড়ে ওঠা, flag দেখে branch
নেওয়া, সবই তাত্ত্বিক দাবি না, verify-করা বাস্তবতা।
এই পুরো module-এর description ছিল: “Assembly শেখার উদ্দেশ্য assembly-তে software লেখা না। উদ্দেশ্য হলো compiler কী বানায় সেটা পড়তে পারা, debugger-এ কী দেখছি বুঝতে পারা, আর performance/security-র প্রশ্নে নিচে নামতে পারা।” — এই বারোটা লেসনের শেষে, ঠিক এই তিনটা ক্ষমতাই আপনার হাতে। আপনি এখন compiler কী বানায় তা পড়তে পারেন, আর debugger-এ কী দেখছেন তা বুঝতে পারেন।
এরপর কী — Operating Systems
কিন্তু একটা বড় প্রশ্ন এখনো অস্পর্শিত। আমাদের sum প্রোগ্রামটা
চলার সময় ধরেই নিয়েছে তার নিজের একটা rsp, নিজের একটা address
space, নিজের registers — যেন পুরো মেশিনটাই তার। বাস্তবে একই সময়ে
শত শত প্রোগ্রাম চলছে, সবাই একই CPU, একই RAM শেয়ার করছে, কেউ কারো
memory-তে হাত দিতে পারছে না, কেউ জানেই না অন্যরা আছে।
আমার program একা মনে করে পুরো machine তার — এই বিভ্রম OS কীভাবে বানায়?
এটাই পরের module, Operating Systems-এর driving question। আর এই module সেই প্রশ্নের উত্তর দেওয়া শুরু করবে ঠিক যেখানে আমরা থামলাম:
- “OS কী, কেন দরকার — bare metal থেকে kernel” — gdb দিয়ে আজ
যে register, যে memory address আমরা সরাসরি পড়লাম, সেগুলো আসলে
kernel-এর অনুমতি নিয়েই পড়া হয়েছে —
ptrace()একটা system call, আর system call-ই হলো user program আর kernel-এর মধ্যে একমাত্র বৈধ দরজা - “Kernel space vs user space, privilege rings” — আমাদের
sumপ্রোগ্রাম CPU-র একটা কম-privileged ring-এ চলে; কেন, আর কীভাবে hardware নিজেই এই সীমারেখা প্রয়োগ করে - “System calls — mechanism ও cost” —
printf-এর ভেতরেই কোথাও একটাwrite()system call লুকিয়ে আছে, PLT-এর ওপারে — সেই call ঠিক কীভাবে user-space থেকে kernel-এ ঝাঁপ দেয়, আর তার real performance cost কত
লক্ষ্য করুন — Operating Systems module-এর প্রয়োজনীয়তা তালিকায়
দুটো module আছে: computer-architecture (hardware-এর privilege
ring, interrupt mechanism বোঝার জন্য) এবং assembly (ঠিক আজ
যা শিখলেন — register, stack, calling convention, ELF — সবকিছুর
উপর দাঁড়িয়েই OS-এর প্রতিটা abstraction ব্যাখ্যা করা হবে)। এই দুই
module মিলেই সেই ভিত্তি তৈরি করল যার উপর fork(), malloc(),
page fault, context switch — সবকিছু দাঁড়াবে।
sum-এর মতো একটা ছোট প্রোগ্রাম যখন printf কল করে, সেই কলের
গভীরে গিয়ে দেখা — এখান থেকেই পরের module শুরু হবে।
আরও পড়ুন
- Debugging with GDB — The GNU Source-Level Debugger — Free Software Foundation · সম্পূর্ণ manual, বিশেষত Chapter 8 (Examining Data) ও Chapter 10 (Examining Memory)
- System V Application Binary Interface — AMD64 Architecture Processor Supplement · এই লেসনের info registers ফলাফল যাচাই করার প্রামাণ্য উৎস — calling convention, Chapter 3
- The Art of Debugging with GDB, DDD, and Eclipse — Norman Matloff, Peter Jay Salzman · gdb-কে ব্যবহারিক workflow হিসেবে শেখার ভালো সম্পূরক