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

gdb দিয়ে Instruction-Level Debugging

Debugging with gdb

gdb দিয়ে আগের এগারোটা লেসনের প্রতিটা ধারণা — calling convention, stack frame, addressing mode, GOT/PLT — একটা সত্যিকারের চলমান প্রসেসে সরাসরি চোখে দেখা যায়, breakpoint-এ থামিয়ে instruction-বাই-instruction।

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

  • break/run/next/step/continue দিয়ে gdb-এর core workflow চালাতে পারবেন, source-line stepping বনাম instruction-level stepping-এর পার্থক্য বুঝে
  • disassemble কমান্ড দিয়ে gdb যে assembly execute করছে তা সরাসরি দেখতে ও পড়তে পারবেন, set disassembly-flavor দিয়ে AT&T/Intel বদলাতে পারবেন
  • info registers দিয়ে calling-convention অনুযায়ী rdi/rsi-তে argument বসতে দেখে System V ABI-র বাস্তবতা সরাসরি যাচাই করতে পারবেন
  • x কমান্ড দিয়ে stack memory সরাসরি examine করে saved return address, saved rbp, ও local variable-এর অবস্থান খুঁজে বের করতে পারবেন
  • stepi/nexti দিয়ে instruction-level single-stepping করে conditional jump-এর flag-নির্ভর branch সরাসরি taken/not-taken দেখতে পারবেন
  • একটা সম্পূর্ণ gdb session চালিয়ে compiler-generated assembly-কে module-এর আগের লেসনগুলোর ধারণার সাথে সরাসরি মিলিয়ে যাচাই করতে পারবেন

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

আগে এটা বুঝি

এগারোটা লেসন ধরে আমরা 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-levelinstruction-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 ঠিক এভাবেই কাজ করে। দ্বিতীয়, শেষ callcall 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      0x7fffffffe440

rbp আর 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-এর saved rbp (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: 32

a (10) [rbp-0x14]-এ, b (32) [rbp-0x18]-এ — ঠিক disassemble add-এর mov DWORD PTR [rbp-0x14],edi / mov DWORD PTR [rbp-0x18],esi অনুযায়ী।

add()-এর stack frame — ঠিকানা থেকে ব্যাখ্যা পর্যন্ত
  1. $rbp (0x7fffffffe440)এই frame-এর ভিত্তি, prologue-এ push rbp; mov rbp,rsp দিয়ে স্থাপিত
  2. [rbp] = 0x...e470saved rbp — caller (main)-এর frame pointer, ফেরার পর restore হবে
  3. [rbp+8] = 0x...11c9return address — call instruction-এর ঠিক পরের byte, main+41
  4. [rbp-0x14] = 10a — rdi থেকে prologue-এ কপি করা প্রথম argument
  5. [rbp-0x18] = 32b — rsi থেকে prologue-এ কপি করা দ্বিতীয় argument
  6. [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-takenmov 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” দাবির সবচেয়ে সরাসরি প্রমাণ।

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

EXPERIMENT

নিজে চালিয়ে পুরো session-টা reproduce করুন

Linux (gcc, gdb)· ২৫ মিনিট
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 করা যায়, শুধু পড়া তথ্য না।

নিজে বানান

BUILD IT

নিজের হাতে backtrace — একটা gdb Python কমান্ড

Python (gdb API) · ●●●○○
  1. gdb-এর Python API ব্যবহার করে একটা নতুন কমান্ড define করুন
  2. বর্তমান $rbp থেকে শুরু করে frame chain হাতে হাঁটুন — [rbp]-এ saved rbp, [rbp+8]-এ return address
  3. প্রতিটা ধাপে return address-কে info symbol দিয়ে function নামে রূপান্তর করুন
  4. 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 পড়ে, একই উত্তর দিচ্ছে।

নিজে বাড়ান:

  1. প্রতিটা frame-এ শুধু return address না, সেই frame-এর local variable-ও (DWARF debug info দিয়ে, gdb.Frame API ব্যবহার করে) প্রিন্ট করুন
  2. একটা recursive function (যেমন factorial) দিয়ে পরীক্ষা করুন — walkframes কি recursion depth ঠিকভাবে ধরতে পারে?
  3. 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 নাম-ম্যাপিং ছাড়া।

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

1

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-এর মান কি একই থাকবে, নাকি বদলাবে? ব্যাখ্যা করুন।

প্রয়োগ

$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, কিন্তু একই কোড।

3

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/jsSF; je/jneZF; jl/jgeSF আর OF-এর তুলনা; ja/jbCF/ZF) — তারপর info registers eflags-এর bracket-এ সেই নির্দিষ্ট flag-এর উপস্থিতি/অনুপস্থিতি চেক করলেই জাম্প হবে কি না আগে থেকেই বলে দেওয়া যায়, stepi না করেই।

4

একটা প্রোগ্রামে একটা loop ১০,০০০ বার চলে, আর আপনি জানতে চান ঠিক কোন iteration-এ একটা নির্দিষ্ট variable 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।

5

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 হিসেবে শেখার ভালো সম্পূরক