为了账号安全,请及时绑定邮箱和手机立即绑定

一步步调试汇编代码:使用EDB调试器的经验分享

调试汇编感觉就像时光旅行——现代工具确实帮了忙,但逐行解析底层指令的乐趣依然像魔法一样迷人。

记得90年代用我的Amiga 1200写汇编代码时,我还挺开心的。那时我并不是职业做这个,只是一个十几岁的孩子。精心编写的代码经常无法如预期般运行,所以我得调试找出哪里出错。这跟现在用高级编程语言时的感觉差不多。

现在开发者还会调试汇编代码吗?看看现在可用的工具,我可以肯定地说“是的”——但我不是不清楚它是一个小众需求。

今天试试EDBhttps://github.com/eteran/edb-debugger),一个可以在我的Linux机器上调试x86–64代码的调试器。让我们玩得尽兴!

有两种方式可以安装调试器。第一种方式很简单,因为大多数Linux发行版都已经包含了调试器。我使用我的软件包管理器安装调试器。

    $ sudo apt update  
    $ sudo apt install edb-debugger

不幸的是,Ubuntu包并不是最新版本,所以我决定从源代码编译EDB。这个过程看上去很简单,但缺少的依赖很快让我的主机操作系统变得一团糟。以下是我如何完成的:

以下是我是如何完成的:
    $ git clone git@github.com:eteran/edb-debugger.git  # 克隆代码仓库
    $ cd edb-debugger  # 切换到 edb-debugger 目录
    $ mkdir build  # 创建一个名为 build 的文件夹
    $ cd build  # 切换到 build 目录
    $ cmake ..  # 使用 cmake 配置项目
    $ make  # 构建项目
    $ sudo make install  # 使用 sudo 权限安装项目

虽然我已经解决了问题,但我不确定最新版本中的小改进是否值得费劲。

我通常都在Docker容器里运行所有的开发工具,但这次我直接在我的主机上安装了EDB工具。为什么呢?带有图形界面的调试器能否在容器里运行,并且在Linux主机上显示界面?现在就来试试看。

结果是,准备这样的容器相对简单来说——或者至少对于每天处理容器的人来说是相对简单的。我将基于 ubuntu:24.04 来制作新的容器,使其与我当前的主机一致。以下是初稿:

# 从 Microsoft 容器镜像仓库拉取基础 Ubuntu 24.04 镜像  
FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu-24.04  
# 设置用户为 vscode  
USER vscode  

# 更新和升级系统包,并安装必要的开发工具  
RUN sudo apt-get update \  
    && sudo apt-get upgrade -y \  
    && sudo apt-get install -y \  
        xxd wget tar make cmake libwayland-client0 \  
        xdg-utils libgvc6 pkg-config git build-essential \  
        libcapstone-dev qtbase5-dev libqt5xmlpatterns5-dev \  
        libqt5svg5-dev qttools5-dev \  
    && sudo rm -rf /var/lib/apt/lists/*  

# 下载并安装 edb-debugger  
RUN cd /tmp \  
    && wget https://github.com/eteran/edb-debugger/releases/download/1.5.0/edb-debugger-1.5.0.tgz \  
    && tar -xvzf edb-debugger-1.5.0.tgz \  
    && cd edb-debugger \  
    && mkdir build \  
    && cd build \  
    && cmake .. \  
    && make \  
    && sudo make install  

# 下载并安装 NASM  
RUN cd /tmp \  
    && wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/nasm-2.16.03.tar.gz \  
    && tar -xvzf nasm-2.16.03.tar.gz \  
    && cd nasm-2.16.03 \  
    && ./configure \  
    && make \  
    && sudo make install \  
    && nasm --version

这个过程很简单:

  • 安装所需的构建工具。
  • 从GitHub发布的源代码构建EDB。
  • 从官方提供的源代码构建NASM。

很简单,对吧?但,它会起作用吗?暂时还不行。首先,我们需要配置vscode以特权模式运行,并且我们还需要一些主机和容器之间的映射关系:

{
    "name": "admac-dev",
    "build": {
        "dockerfile": "Dockerfile",
        "context": ".."
    },
    "runArgs": [
        "--privileged",
        "--env=DISPLAY=${env:DISPLAY}", // 环境变量DISPLAY设置为当前DISPLAY值
        "--volume=/tmp/.X11-unix:/tmp/.X11-unix" // 映射X11的unix套接字文件
    ]
}

这里的关键在于使用特权权限运行,并重定向X11文件套接字。那么现在可以运行了吗?还不行呢。我们还需要让主机的X11服务器接受容器的连接请求。这只需要一行代码就能搞定。

    $ xhost +local:*

这命令允许本地机器上的X服务器进行网络访问。

现在呢,所有事情都应该会都能正常运行了。

现在是用汇编写代码的时候了。我们可以编写一个非常简单的程序,它什么也不做。这听起来很有趣,对吧?我们可以使用以下几行来实现:

; 汇编代码示例
; 这里应该放入实际的汇编代码,例如优雅地退出程序的操作

(注释部分已被删除,以符合原文要求。)

    section .text  
    global _start  

_start:  
    ; 设置rax寄存器的值为60
    mov rax, 60  
    ; rdi寄存器清零
    xor rdi, rdi  
    ; 执行系统调用,用于正常退出程序
    syscall

它会在 x86-64 Linux 上运行。我可以手动编译它,但我不想重复这个过程。我将创建一个 Makefile 来自动编译所有的 NASM 和 C 文件,并把它们链接成一个可执行文件:

    LD=ld  
    CC=gcc  
    NASM=nasm  
    CFLAGS=-Wall -Wextra -O2 -g -fno-pic  
    NASMFLAGS=-f elf64  
    LDFLAGS=-z noexecstack -nostdlib -e _start  
    OUTPUT=bin/admac  

    SRCDIR=src  
    OBJDIR=obj  
    SRCS_C=$(wildcard $(SRCDIR)/*.c)  
    SRCS_ASM=$(wildcard $(SRCDIR)/*.s)  
    OBJS=$(patsubst $(SRCDIR)/%.c, $(OBJDIR)/%.o, $(SRCS_C)) \  
         $(patsubst $(SRCDIR)/%.s, $(OBJDIR)/%.o, $(SRCS_ASM))  

    # 编译目标文件
    build: $(OUTPUT)  

    # 运行生成的程序
    run: build  
     @$(OUTPUT)  

    # 使用edb调试器运行程序
    debug: build  
     @edb --run $(OUTPUT) 2> /dev/null  

    # 将目标文件链接生成输出文件
    $(OUTPUT): $(OBJS)  
     # 确保输出目录存在
     @mkdir -p bin  
     $(LD) $(LDFLAGS) -o $@ $^  

    $(OBJDIR)/%.o: $(SRCDIR)/%.c  
     @mkdir -p $(OBJDIR)  
     $(CC) $(CFLAGS) -c $< -o $@  

    $(OBJDIR)/%.o: $(SRCDIR)/%.s  
     @mkdir -p $(OBJDIR)  
     $(NASM) $(NASMFLAGS) $< -o $@  

    # 清除生成的目标文件和输出文件
    clean:  
     @rm -rf $(OBJDIR) $(OUTPUT)  

    .PHONY: build run debug clean

当我运行 make run 时,目前没有任何明显的反应。但运行 make debug 则非常有意思。它会在我的容器内环境内启动一个调试会话,但调试窗口却显示在我的主机屏幕上。我们来运行它吧。

EDB 调试

它显示了四个窗格。最大的一个显示了反汇编的代码。右边我们可以查看所有寄存器的状态,包括AVX2。在底部,我们可以查看内存和栈的内容。看起来简直完美。几乎就像我90年代用过的MonAm调试器一样。

https://developer-blog.net/amiga-assembler-programmieren/ 编写Amiga汇编程序

我真的被它的简洁性所震撼。我可以看到我可以“单步步入”,“单步跃过”,甚至可以设置断点。这样已经足够让我玩得开心了。

我想EDB是如何处理代码分支和函数的呢?咱们写点更多的代码试试看。

     .text
    声明外部 stdout_print
    全局声明 _start

_start:
    lea 指令 rdi, [rel msg_hello]
    调用 stdout_print()

    mov 指令 rax, 60
    执行 xor rdi, rdi
    系统调用

     .rodata

    msg_hello:
    dq 14  ; 定义一个双字数据
    db "Hello, World!", 10  ; 定义一个字节数据

; .text 段用于存放可执行的机器指令
; .rodata 段用于存放只读数据

上面的代码会调用一个这样的函数来输出文本。该函数可能类似如下所示:

例如:

    ; 错误码  
        ERR_ZERO_WRITE equ -33         ; 没有写入字节时的错误  
        ERR_BUFFER_OVFLW equ -35       ; 缓冲区溢出错误  
        ERR_STACK_ALIGN equ -36        ; 栈未对齐错误  

        section .text  
        global stdout_print  

    ; 从字符串中打印指定数量的字符到标准输出  
    ; rdi - 指向[length (8 bytes), 字符串 (ascii, null终止)]的指针  
    ; rax - 如果没有错误返回0,否则返回负值表示错误码  
    stdout_print:  
        lea rax, [rsp + 8]             ; 计算 RSP + 8  
        test rax, 15                   ; 检查栈对齐  
        jnz .unaligned                 ; 栈不对齐时跳转到.unaligned作为错误  

        mov rdx, [rdi]                 ; 加载字节计数  
        lea rsi, [rdi + 8]             ; 加载字符串指针  
        mov rdi, 1                     ; 标准输出描述符  

        test rdx, rdx                  ; 检查要写入的字节数  
        jz .completed                  ; 如果为零则直接完成,不执行系统调用  

    .loop:  
        mov rax, 1                     ; sys_write 系统调用  
        syscall                        ; 执行系统调用  

        test rax, rax                  ; 比较系统调用结果中的 RAX  0  
        jz .zero                       ; 如果没有进展则跳转到 .zero  
        js .end                        ; 如果内核失败则跳转到 .end  

        sub rdx, rax                   ; 减少剩余字节数  
        add rsi, rax                   ; 移动缓冲区指针  

        test rdx, rdx                  ; 检查是否有剩余字节数  
        jnz .loop                      ; 如果所有字节已写则不跳转  

    .completed:  
        xor rax, rax                   ; 没有错误  
        jmp .end  

    .unaligned:  
        mov rax, ERR_STACK_ALIGN       ; 设置错误表示栈未对齐  
        jmp .end  

    .zero:  
        mov rax, ERR_ZERO_WRITE        ; 设置错误表示零写入  
        jmp .end  

    .end:  
        ret                            ; 返回 RAX 中的错误码

这个函数稍微有点复杂,因为它处理了一些特殊情况。目的是检查所有分支在反汇编器中的显示情况如何。我们可以再试一次运行。

EDB 调试会话

我们现在可以看到更多功能。其中之一是箭头指示代码可能跳转的地方。我可以看到,在执行这一行之前,当前的跳转将不会被执行。很好。这个视图还可以可视化各种寄存器指向的位置。这里,RSP保存了该函数的返回地址(在调用时压入了栈中),并且很好地可视化了。做得不错,EDB团队!

在90年代,调试汇编程序是一项考验耐心的任务——每次崩溃后都得重启,有时候还得完全重写。今天,我们有了可视化调试器,可以在实时环境中显示一切,但核心技能依然没有改变:理解CPU在做什么及原因。不论是Amiga上的MonAm还是Linux容器里的EDB,调试仍然是一门科学和艺术的结合。也许如今使用汇编语言的开发者少了,但对于那些仍在从事这项工作的人来说,控制机器的魅力依然和几十年前一样让人兴奋。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

0 评论

作者其他优质文章

正在加载中
手记
粉丝
104
获赞与收藏
607

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消