x86_ Principle and example of single step debugging for 64 platform

Keywords: Linux

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.

Posted by yshaf13 on Thu, 04 Jun 2020 08:24:06 -0700