First look at a program:
// simple.c int value = 0; int main(int argc, char **argv) { value ++; value ++; value ++; value ++; value ++; value ++; }
Now I want to debug it step by step and track the change of value. How can I do it?
You can use gdb. But if you want to understand what's going on behind the scenes, it's better to do it by hand.
This requires an understanding of the nature of single step tracing (Linux as an example):
- x86_64 architecture, the FLAGS register has a TF flag bit to enable and disable single step.
- After each instruction is executed, the processor will check the TF flag and sink the execution stream into the kernel when enabling single step.
- The operating system kernel will send SIGTRAP signal to inform the process, and the process will receive the signal processing single step process.
Come on, let's add some logic to simple.c above to realize single step tracking:
// ssdebug.c #include <stdio.h> #include <sys/mman.h> #include <signal.h> #include <asm/processor-flags.h> // RIP offset, 192 bytes from stack top, see RT for details_ sigframe #define PC_OFFSET 192 // CFLAGS offset, 200 bytes from stack top, see RT for details_ sigframe #define F_OFFSET 200 int value = 0; void trap(int unused); // force inline to avoid ret instruction void __attribute__((always_inline)) inline breakpoint() { unsigned long rip = 0, f = 0; unsigned char *page; signal(SIGTRAP, trap); pass: // Get own RIP, break point asm volatile("mov $., %0" : "=r"(rip)); if (f == 0) { page = (unsigned char *)((unsigned long)rip & 0xffffffffffff1000); mprotect((void *)page, 4096, PROT_WRITE|PROT_READ|PROT_EXEC); #define I_BRK 0xcc *(unsigned char *)(rip) = I_BRK; f ++; goto pass; } } void trap(int unused) { unsigned long *p; static int fbrk = 0; p = (unsigned long*)((unsigned char *)&p + F_OFFSET); if (!fbrk) { // Breakpoint processing, single step on *p = *p | X86_EFLAGS_TF; p = (unsigned long*)((unsigned char *)&p + PC_OFFSET); printf("Break at [RIP:0x%lx]\n", *p); fbrk ++; } p = (unsigned long*)((unsigned char *)&p + PC_OFFSET); printf("current RIP: 0x%lx value:%d\n", *p, value); #define I_RET 0xc3 if (*(unsigned char *)*p == I_RET) { // Function return, single step end p = (unsigned long*)((unsigned char *)&p + F_OFFSET); *p &= ~X86_EFLAGS_TF; } } int main(int argc, char **argv) { breakpoint(); // Break point, single step value ++; value ++; value ++; value ++; value ++; value ++; }
OK, let's see the effect:
[root@localhost probe]# ./a.out Break at [RIP:0x40070f] current RIP: 0x40070f value:0 current RIP: 0x400715 value:0 current RIP: 0x400719 value:0 current RIP: 0x40071e value:0 current RIP: 0x400752 value:0 current RIP: 0x400758 value:0 current RIP: 0x40075b value:0 current RIP: 0x400761 value:1 current RIP: 0x400767 value:1 current RIP: 0x40076a value:1 current RIP: 0x400770 value:2 current RIP: 0x400776 value:2 current RIP: 0x400779 value:2 current RIP: 0x40077f value:3 current RIP: 0x400785 value:3 current RIP: 0x400788 value:3 current RIP: 0x40078e value:4 current RIP: 0x400794 value:4 current RIP: 0x400797 value:4 current RIP: 0x40079d value:5 current RIP: 0x4007a3 value:5 current RIP: 0x4007a6 value:5 current RIP: 0x4007ac value:6 current RIP: 0x4007ad value:6
It's true that the change process of value is tracked in one step. If only the assembly instructions can be printed out, it's not difficult.
Wenzhou leather shoes in Zhejiang Province are wet, and they will not be fat if it rains and floods.