Linux gdb单步调试的原理

news2024/10/7 10:15:06

文章目录

  • 一、demo演示
  • 二、原理分析
  • 参考资料

一、demo演示

.section .data
message:
	.string "Hello, World!\n"
len = . - message

.section .text
.globl _start
_start:
	# 调用 write() 函数输出 "Hello, World!"
	mov $1, %rax            # 系统调用号为 1 表示 write()
	mov $1, %rdi            # 文件描述符为 1 表示标准输出
	lea message(%rip), %rsi # 输出的字符串地址
	mov $len, %rdx          # 输出的字符串长度
	syscall                 # 调用系统调用

	# 调用 exit() 函数退出程序
	mov $60, %rax           # 系统调用号为 60 表示 exit()
	xor %rdi, %rdi          # 返回值为 0
	syscall                 # 调用系统调用

这段汇编代码是在标准输出上输出 “Hello, World!”,然后退出程序:

as -o hello.o hello.s
ld -o hello hello.o
# ./hello
Hello, World!
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <errno.h>
#include <sys/user.h>
#include <stdint.h>

void fprint_wait_status(FILE *stream, int status)
{
    if( WIFSTOPPED(status) ) {
        fprintf(stream, "Child stopped: %d\n", WSTOPSIG(status));
    }
    if( WIFEXITED(status) ) {
        fprintf(stream, "Child exited: %d\n", WEXITSTATUS(status));
    }
    if( WIFSIGNALED(status) ) {
        fprintf(stream, "Child signaled: %d\n", WTERMSIG(status));
    }
    if( WCOREDUMP(status) ) {
        fprintf(stream, "Core dumped.\n");
    }
}

int ptrace_instruction_pointer(int pid, uint64_t *rip)
{
    //获取指令指令的值
    struct user_regs_struct regs;
    if( ptrace(PTRACE_GETREGS, pid, NULL, (void*)&regs) ) {
        fprintf(stderr, "Error fetching registers from child process: %s\n",
            strerror(errno));
        return -1;
    }
    if(rip)
        *rip = regs.rip;
    return 0;
}

int singlestep(int pid)
{
    int retval, status;
    //通过ptrace发送单步调试的指令
    retval = ptrace(PTRACE_SINGLESTEP, pid, 0, 0);
    if( retval ) {
        return retval;
    }

    //阻塞在这里--等待子进程停止
    //子进程停止发送信号唤醒父进程 -- 父进程对子进程进行调试
    waitpid(pid, &status, 0);

    return status;
}

int main(int argc, char ** argv)
{
    uint64_t rip;
    pid_t pid;
    int status;
    char *program;
    if (argc < 2) {
        fprintf(stderr, "Usage: %s elffile arg0 arg1 ...\n", argv[0]);
        exit(-1);
    }

    pid = fork();
    if( pid == -1 ) {
        fprintf(stderr, "Error forking: %s\n", strerror(errno));
        exit(-1);
    }
    if( pid == 0 ) {
        /* child */
        if( ptrace(PTRACE_TRACEME, 0, 0, 0) ) {
            fprintf(stderr, "Error setting TRACEME: %s\n", strerror(errno));
            exit(-1);
        }
         execvp(argv[1], argv + 1);
    } else {
        /* parent */

        //阻塞在这里--等待子进程停止
        waitpid(pid, &status, 0);
        fprint_wait_status(stderr,status);
        //WIFSTOPPED在处理子进程状态时判断子进程是否处于停止状态
        while( WIFSTOPPED(status) ) {
            if(ptrace_instruction_pointer(pid, &rip) ) {
                break;
            }
            fprintf(stderr, "RIP: %p\n", (void*)rip);
            status = singlestep(pid);
        }
        fprint_wait_status(stderr, status);
        fprintf(stderr, "Detaching\n");
        ptrace(PTRACE_DETACH, pid, 0, 0);
    }

    return 0;
}

在这里插入图片描述

二、原理分析

PTRACE_SINGLESTEP:重新启动被跟踪进程,并在执行一条指令后停止。当使用PTRACE_SINGLESTEP选项时,被跟踪进程将在执行完一条指令后立即停止,以供跟踪进程进行单步调试或其他操作。

这个选项都会使被跟踪进程看起来好像是接收到了一个SIGTRAP信号而停止执行。跟踪进程可以在被跟踪进程停止时进行进一步的检查或操作。

以下是这个选项的使用方式:

ptrace(PTRACE_SINGLESTEP, pid, NULL, data);

pid是被跟踪进程的进程ID。
data参数如果非零,表示要发送给被跟踪进程的信号编号;如果为零,表示不发送任何信号。

在停止时,被跟踪进程会看起来好像是接收到了一个SIGTRAP信号。

原理图如下:
在这里插入图片描述

内核源码分析:

SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
		unsigned long, data)
{
	//根据被跟踪进程的pid获取其struct task_struct结构体
	struct task_struct *child;
	child = ptrace_get_task_struct(pid);
	if (IS_ERR(child)) {
		ret = PTR_ERR(child);
		goto out;
	}

	//对被跟踪进程发起request请求
	arch_ptrace(child, request, addr, data);

}

这是一个和处理器架构相关的函数:

long arch_ptrace(struct task_struct *child, long request,
		 unsigned long addr, unsigned long data)
{
	ptrace_request(child, request, addr, data);
}
int ptrace_request(struct task_struct *child, long request,
		   unsigned long addr, unsigned long data)
{
	#ifdef PTRACE_SINGLESTEP
		case PTRACE_SINGLESTEP:
	#endif
			return ptrace_resume(child, request, data);
}
#ifdef PTRACE_SINGLESTEP
#define is_singlestep(request)		((request) == PTRACE_SINGLESTEP)

static int ptrace_resume(struct task_struct *child, long request,
			 unsigned long data)
{
	//设置单步调试标志
	if (is_singlestep(request){
		user_enable_single_step(child);
	}

	//唤醒子进程
	wake_up_state(child, __TASK_TRACED);
	
}
void user_enable_single_step(struct task_struct *child)
{
	//这里传递的参数是0
	enable_step(child, 0);
}

user_enable_single_step 函数接受一个参数 child,表示要启用单步调试的任务结构体指针。该函数调用 enable_step 函数,并将 block 参数设置为 0,即不启用块步调试。这样,enable_step 函数将尝试启用任务的单步调试,而不启用块步调试。

/*
 * Enable single or block step.
 */
static void enable_step(struct task_struct *child, bool block)
{
	//传入的参数 block = 0
	/*
	 * Make sure block stepping (BTF) is not enabled unless it should be.
	 * Note that we don't try to worry about any is_setting_trap_flag()
	 * instructions after the first when using block stepping.
	 * So no one should try to use debugger block stepping in a program
	 * that uses user-mode single stepping itself.
	 */
	if (enable_single_step(child) && block)
		set_task_blockstep(child, true);
	else if (test_tsk_thread_flag(child, TIF_BLOCKSTEP))
		set_task_blockstep(child, false);
}
#define TIF_SINGLESTEP		4	/* reenable singlestep on user return*/

#define X86_EFLAGS_TF	0x00000100 /* Trap Flag */

#define TIF_FORCED_TF		24	/* true if TF in eflags artificially */

/*
 * Enable single-stepping.  Return nonzero if user mode is not using TF itself.
 */
static int enable_single_step(struct task_struct *child)
{
	struct pt_regs *regs = task_pt_regs(child);
	unsigned long oflags;

	/*
	 * If we stepped into a sysenter/syscall insn, it trapped in
	 * kernel mode; do_debug() cleared TF and set TIF_SINGLESTEP.
	 * If user-mode had set TF itself, then it's still clear from
	 * do_debug() and we need to set it again to restore the user
	 * state so we don't wrongly set TIF_FORCED_TF below.
	 * If enable_single_step() was used last and that is what
	 * set TIF_SINGLESTEP, then both TF and TIF_FORCED_TF are
	 * already set and our bookkeeping is fine.
	 */
	if (unlikely(test_tsk_thread_flag(child, TIF_SINGLESTEP)))
		regs->flags |= X86_EFLAGS_TF;

	/*
	 * Always set TIF_SINGLESTEP - this guarantees that
	 * we single-step system calls etc..  This will also
	 * cause us to set TF when returning to user mode.
	 */
	set_tsk_thread_flag(child, TIF_SINGLESTEP);

	oflags = regs->flags;

	/* Set TF on the kernel stack.. */
	regs->flags |= X86_EFLAGS_TF;

	/*
	 * ..but if TF is changed by the instruction we will trace,
	 * don't mark it as being "us" that set it, so that we
	 * won't clear it by hand later.
	 *
	 * Note that if we don't actually execute the popf because
	 * of a signal arriving right now or suchlike, we will lose
	 * track of the fact that it really was "us" that set it.
	 */
	if (is_setting_trap_flag(child, regs)) {
		clear_tsk_thread_flag(child, TIF_FORCED_TF);
		return 0;
	}

	/*
	 * If TF was already set, check whether it was us who set it.
	 * If not, we should never attempt a block step.
	 */
	if (oflags & X86_EFLAGS_TF)
		return test_tsk_thread_flag(child, TIF_FORCED_TF);

	set_tsk_thread_flag(child, TIF_FORCED_TF);

	return 1;
}

enable_single_step 函数,用于启用单步调试模式。以下是代码说明:

(1)调用 task_pt_regs 宏获取子进程任务的struct pt_regs:

struct pt_regs 是一个在Linux内核中用于保存进程或线程上下文中寄存器值的数据结构。

它定义了一个包含了各种寄存器的成员的结构体,用于保存任务在进行上下文切换时的寄存器状态,以及在进行异常处理或调试时用于保存当前执行指令的上下文信息。

struct pt_regs {
	unsigned long r15;
	unsigned long r14;
	unsigned long r13;
	unsigned long r12;
	unsigned long rbp;
	unsigned long rbx;
/* arguments: non interrupts/non tracing syscalls only save up to here*/
	unsigned long r11;
	unsigned long r10;
	unsigned long r9;
	unsigned long r8;
	unsigned long rax;
	unsigned long rcx;
	unsigned long rdx;
	unsigned long rsi;
	unsigned long rdi;
	unsigned long orig_rax;
/* end of arguments */
/* cpu exception frame or undefined */
	unsigned long rip;
	unsigned long cs;
	unsigned long eflags;
	unsigned long rsp;
	unsigned long ss;
/* top of stack page */
};
struct pt_regs *regs = task_pt_regs(child);
struct thread_struct {
	/* Cached TLS descriptors: */
	struct desc_struct	tls_array[GDT_ENTRY_TLS_ENTRIES];
	unsigned long		sp0;
	unsigned long		sp;
	......
};

struct task_struct {
/* CPU-specific state of this task */
	struct thread_struct thread;
}

#define task_pt_regs(tsk)	((struct pt_regs *)(tsk)->thread.sp0 - 1)

将 tsk 的内核栈指针减去 1,然后将结果转换为 struct pt_regs* 类型的指针。
在给定的宏定义中,将任务的内核栈指针 (tsk)->thread.sp0 减去 1 的目的是将指针向前移动一个偏移量,使其指向寄存器上下文结构体 pt_regs 的起始位置。

在x86架构中,寄存器上下文结构体 pt_regs 被存储在任务的内核栈的顶部。所以,通过将内核栈指针减去 1,指针将移动到 pt_regs 结构体的位置。这种偏移一般是由于栈的增长方向的约定造成的。在x86架构中,栈从高地址向低地址增长,而栈顶部位于较高的地址。因此,为了指向位于栈顶的 pt_regs 结构体,需要将栈指针减去 1。

函数检查任务的 TIF_SINGLESTEP 线程标志。这里用 unlikely 修饰表示这是一个小概率事件。
如果该标志已设置,说明在内核模式下发生了 sysenter/syscall 指令,do_debug() 函数已经清除了 TF(Trap Flag)并设置了 TIF_SINGLESTEP 标志。但如果用户模式自己设置了 TF 标志,那么 TF 仍然被 do_debug() 清除,因此需要重新设置 TF 标志来恢复用户模式的状态,以避免错误地设置 TIF_FORCED_TF。

(2)函数使用 set_tsk_thread_flag 函数将任务的 TIF_SINGLESTEP 线程标志设置为真,以确保在系统调用等情况下仍能进行单步调试。

(3)保存当前子进程寄存器 flags 的值到 oflags 变量中。

(4)在内核栈上设置 TF 标志,即将 TF 标志设置为 1。

(5)如果要执行的指令改变了 TF 标志的值,说明不是由我们自己设置的,所以不应该将其标记为 “us” 设置的,以免后续手动清除该标志。如果发生这种情况,函数通过 clear_tsk_thread_flag 函数清除 TIF_FORCED_TF 标志,并返回 0。

(6)如果 TF 标志已经设置,并且之前设置 TF 的不是我们自己,说明我们不应该尝试 block step ,因此返回 0。

(7)如果 TF 标志之前未设置,函数使用 set_tsk_thread_flag 函数将任务的 TIF_FORCED_TF 标志设置为真,并返回 1。

struct thread_info {
		__u32			flags;
}
/*
 * flag set/clear/test wrappers
 * - pass TIF_xxxx constants to these functions
 */

static inline void set_ti_thread_flag(struct thread_info *ti, int flag)
{
	set_bit(flag, (unsigned long *)&ti->flags);
}

#define task_thread_info(task)	((struct thread_info *)(task)->stack)
/*
 * thread information flags
 * - these are process state flags that various assembly files
 *   may need to access
 * - pending work-to-be-done flags are in LSW
 * - other flags in MSW
 * Warning: layout of LSW is hardcoded in entry.S
 */

#define TIF_BLOCKSTEP		25	/* set when we want DEBUGCTLMSR_BTF */

static inline int test_tsk_thread_flag(struct task_struct *tsk, int flag)
{
	return test_ti_thread_flag(task_thread_info(tsk), flag);
}

在这里插入图片描述
在 x86 架构中,TRAP(Trap Flag,陷阱标志)位于 EFLAGS 寄存器的第 8 位(bit 8)。该位用于启用或禁用单步调试模式。下面是关于 TRAP 位的解释:

当 TRAP 位被设置为 1 时,即启用单步调试模式,处理器会在每条指令执行后生成一个调试异常。这样可以在每条指令执行后检查程序的执行状态,实现逐指令调试。单步调试模式允许程序的执行被暂停以进行调试操作。

当 TRAP 位被清除为 0 时,即禁用单步调试模式,处理器不会生成调试异常,程序会正常连续执行,无需逐条指令地暂停。

如果一个应用程序使用 POPF、POPFD 或 IRET 指令设置 TF(Trap Flag)标志,那么在执行这些指令后的下一条指令之后会生成一个调试异常。这意味着程序可以通过设置 TF 标志来实现在指令级别上的单步调试。

TRAP 位用于控制处理器是否在每条指令执行后生成调试异常,从而实现单步调试。通过设置或清除 TF 标志,程序可以启用或禁用单步调试模式,并在需要时触发调试异常以进行调试操作。

参考资料

Linux 3.10.0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/971188.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

海域可视化监管:浅析海域动态远程视频智能监管平台的构建方案

一、方案背景 随着科技的不断进步&#xff0c;智慧海域管理平台已经成为海洋领域监管的一种重要工具。相比传统的视频监控方式&#xff0c;智慧海域管理平台通过建设近岸海域视频监控网、海洋环境监测网和海上目标探测网络等&#xff0c;可实现海洋管理的数字化转型。 传统的…

『好书推荐』|《Effective软件测试》

作者简介 《Effective软件测试》 是一本由清华大学出版社出版的图书&#xff0c;作者是[荷]毛里西奥阿尼什&#xff08;Maurcio Aniche&#xff09;&#xff0c;译者是朱少民、李洁、张元。是2023年6月新推出的一本书籍。 Maurcio Aniche博士是荷兰代尔夫特理工大学软件工程系的…

页面页脚部分CSS分享

先看效果&#xff1a; CSS部分&#xff1a;&#xff08;查看更多&#xff09; <style>body {display: grid;grid-template-rows: 1fr 10rem auto;grid-template-areas: "main" "." "footer";overflow-x: hidden;background: #F5F7FA;min…

使用LightPicture开源搭建私人图床:详细教程及远程访问配置方法

文章目录 1.前言2. Lightpicture网站搭建2.1. Lightpicture下载和安装2.2. Lightpicture网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1.前言 现在的手机越来越先进&#xff0c;功能也越来越多&#xff0c;而手机…

urllib库

目录 1、简介 2、请求模块 3、解析模块 1、简介 urllib是python内置的标准库&#xff0c;无需下载&#xff0c;导入即可使用。 2、请求模块 urllib包里有一个request模块 from urllib import request# 1.request模块# 1.1发送网络请求 # urlopen() : 打开url地址 resp re…

进程的概念、组成、特征

1.概念 进程是操作系统进行资源分配的最小的单位。 2.组成 进程由PCB、程序段、数据段组成。PCB是操作系统需要的&#xff0c;而程序段和数据段是用户所需要的。 PCB是一种数据结构&#xff0c;操作系统所需的进程资源都存储在PCB中&#xff0c;PCB也是进程存在的唯一标识。…

【快应用】后台运行的快应用如何自动前台打开

【关键词】 Onhide、router、后台 【问题背景】 快应用退到后台运行后&#xff0c;隔几秒钟后&#xff0c;会自动打开跳转到某个页面&#xff0c;这种情形应该如何去定位处理&#xff1f; 【问题分析】 退到后台运行&#xff0c;再自动拉起看似很诡异&#xff0c;以为是快应…

Autofac使用(3)---AOP支持

1、Nuget引入程序集 2、扩展IInterceptor public class CusotmInterceptor : IInterceptor{/// <summary>/// 切入者逻辑/// /// 使用了Intercept 方法把 要调用的Call方法给包裹起来了/// </summary>/// <param name"invocation"></param>p…

架构设计基础设施保障IaaS弹性伸缩和无服务器计算

目录 1 高可用弹性伸缩实践2 无服务器计算&#xff08;FaaS&#xff09; 1 高可用弹性伸缩实践 背景 弹性伸缩是云服务架构的重要优势&#xff0c;能够很好的解决高并发场景下的性能瓶颈&#xff0c; 同时节省运营成本。 在 IaaS 端&#xff0c;能够弹性伸缩的最实用的产品形…

AIoT+5G改变智慧城市:揭秘智慧公厕的奇妙魅力

AIoT5G的新型智慧城市应用带来了智慧公厕的全新体验。通过智能监测、高速网络、智能调控、智慧管理等技术应用&#xff0c;公厕的舒适性、便捷性和智慧化程度得到了极大提升。可以看到的是&#xff0c;智慧公厕正逐渐激活智慧城市的生活场景&#xff0c;为城市居民带来更好的生…

无需租用云服务器:使用Linux本地搭建web服务并实现内网穿透发布公网访问的详细教程

文章目录 前言1. 本地搭建web站点2. 测试局域网访问3. 公开本地web网站3.1 安装cpolar内网穿透3.2 创建http隧道&#xff0c;指向本地80端口3.3 配置后台服务 4. 配置固定二级子域名5. 测试使用固定二级子域名访问本地web站点 前言 在web项目中,部署的web站点需要被外部访问,则…

Mac电脑其他文件占用超过一大半的内存如何清理?

mac的存储空间时不时会提示内存已满&#xff0c;查看内存占用比例最大的居然是「其他文件」&#xff0c;「其他文件」是Mac无法识别的格式文件或应用插件扩展等等...如果你想要给Mac做一次彻底的磁盘空间清理&#xff0c;首当其冲可先对「其他文件」下手&#xff0c;那么我们该…

Mactracker for mac,让您轻松掌握Mac电脑硬件信息的利器

Mactracker for mac是一款运行在MacOS平台上的Mac硬件信息查询工具。它能够方便地显示您电脑所有硬件的信息&#xff0c;包括处理器速度、内存、光盘驱动器、图形卡、支持的macOS版本和扩展选项等。此外&#xff0c;它还提供了有关Apple鼠标、键盘、显示器、打印机、扫描仪、数…

TensorFlow(R与Python系列第四篇)

目录 一、TensorFlow介绍 二、张量 三、有用的TensorFlow运算符 四、reduce系列函数实现约减 1-第一种理解方式&#xff1a;引入轴概念后直观可理 2-第二种理解方式&#xff1a;按张量括号层次的方式 参考&#xff1a; 一、TensorFlow介绍 TensorFlow是一个强大的用于数…

通过HFS低成本搭建NAS,并内网穿透实现公网访问

文章目录 前言1.下载安装cpolar1.1 设置HFS访客1.2 虚拟文件系统 2. 使用cpolar建立一条内网穿透数据隧道2.1 保留隧道2.2 隧道名称2.3 成功使用cpolar创建二级子域名访问本地hfs 总结 前言 云存储作为一个新概念&#xff0c;在前些年炒的火热&#xff0c;虽然伴随一系列黑天鹅…

打不开github,解决方案

国内加载Githup会很慢&#xff0c;有时候会自己连接超时进不去&#xff0c; 那么如何访问呢&#xff0c;下面就带大家来一起操作 这篇文章借鉴于&#xff08;解决国内 github.com 打不开的最最最准确方法_杨大脸I的博客-CSDN博客&#xff09; 原作者是它&#xff0c;我这里也是…

同步推送?苹果计划本月推出 iOS17和iPadOS17,你的手机支持吗?

据报道&#xff0c;苹果公司计划在本月推出 iOS 17 和 iPadOS 17 正式版更新。与去年不同的是&#xff0c;这次更新将同时发布&#xff0c;而不是分别发布。根据彭博社的一位消息人士马克・古尔曼的说法&#xff0c;苹果公司认为 iOS 17 和 iPadOS 17 的第八个测试版已经非常接…

若伊代码分析(前端 vue2 登录页)

目录 前端项目搭建 项目调整及element引入 登录界面样式 获取验证码 全局变量 vue中利用.env文件存储全局环境变量&#xff0c;以及配置vue启动和打包命令 配置.env文件 获取.env中的全局变量 实际用处 --------项目代码------- 跨域配置 配置代理方式一 配置代理…

更健康舒适更科技的照明体验!书客SKY护眼台灯SUKER L1上手体验

低价又好用的护眼台灯是多数人的需求&#xff0c;很多人只追求功能性护眼台灯&#xff0c;显色高、无频闪、无蓝光等基础需求。但是在较低价格中很难面面俱到&#xff0c;然而刚发布的SUKER书客L1护眼台灯却是一款不可多得的性价比护眼台灯&#xff0c;拥有高品质光源&#xff…

【2023】CompletableFuture使用代码案例实习使用场景介绍

CompletableFuture 一、介绍1、概述2、常用方法 二、方法使用1、异步操作1.1、创建任务&#xff08;runAsync | supplyAsync&#xff09;runAsyncsupplyAsync 1.2、获取结果&#xff08;get | join&#xff09;1.3、异常处理&#xff08;whenComplete | exceptionally&#xff…