很多人都知道x86 CPU的断点指令,即著名的INT 3,机器码为0xCC。在Nvidia的GPU中,比如著名的伏特微架构,也有一条断点指令,叫BPT,是Breakpoint的缩写。
那么,在AMD GPU中是否也有这样一条指令呢?
在回答这个问题前,先看一段AMD GPU的指令吧。
s_addc_u32 s17, s17, s19 // 000000000168: 82111311s_lshr_b64 s[16:17], s[16:17], 16 // 00000000016C: 8F909010s_mul_i32 s5, s5, s13 // 000000000170: 92050D05s_add_u32 s5, s5, s16 // 000000000174: 80051005s_mul_i32 s4, s4, s8 // 000000000178: 92040804v_add_u32 v3, vcc, s4, v0 // 00000000017C: 32060004=> s_nop0x0000 // 000000000180: BF800000s_load_dword s4, s[6:7], 0x18// 000000000184: C003 00000018s_nop0x0000 // 00000000018C: BF800000s_load_dword s5, s[6:7], 0x40// 000000000190: C003 00000040s_nop0x0000 // 000000000198: BF800000s_load_dword s12, s[6:7], 0x20// 00000000019C: C0020303 00000020s_nop0x0000 // 0000000001A4: BF800000s_load_dword s13, s[6:7], 0x48// 0000000001A8: C0020343 00000048s_waitcnt lgkmcnt(0) // 0000000001B0: BF8C007Fs_nop0x0000 // 0000000001B4: BF800000v_add_u32 v9, vcc, s4, v3 // 0000000001B8: 32120604s_nop0x0000 // 0000000001BC: BF800000v_add_u32 v13, vcc, s5, v3 // 0000000001C0: 321A0605v_mov_b32 v5, s8 // 0000000001C4: 7E0A0208
AMD的GPGPU已经发展了几轮,比较著名的有Terascale微架构,发展了三代后,被GCN(Graphics Core Next)微架构所取代,GCN已经发展了6代,预计退役。
上面这段汇编代码就是GCN微架构的指令。
GCN的指令是从Terascale的VLIW指令演变而来的。VLIW的字面意思就是“非常长的指令字”。不过GCN的指令已经不再是VLIW风格。不仅指令的长度不是那么长了(32或者64位),更重要的是每条指令的操作都是针对当前算核的单一数据项,操作比较单纯,不再强调指令级别的并行。
上面清单右侧注释部分就是指令的机器码。可以看到大多数是32位,个别是64位。
GCN指令有一个非常大的特色,那就是把所有操作分为标量和向量两个大类。上面s_开头的都是标量指令,v_开头的都是向量指令。在GPU内部,标量指令会送给标量ALU(sALU)来执行,向量指令会送给向量ALU执行(vALU)。AMD GPU也有与Nvidia的WARP相同的概念,叫wavefront。每个wavefront是64个线程。考虑到在每个算核(kernel)的开头结尾,以及某些逻辑部分常常只需要一个处理器执行,比如对于上面的那么多条s_指令,就只要一个sALU执行就可以了,不需要浪费vALU,因为vALU一行动,就是64个一排碾压过去。这样考虑,AMD的设计是非常聪明的。
再回头来说断点指令。浏览GCN的指令集,没有专门的断点指令。但这并代表真的没有。因为没有这样的基本功能,顶层的调试功能怎么工作的啊。在AMD的工具链中,无论是CodeXL还是ROCm-GDB都是支持断点功能的。
那么是哪条指令呢?我觉得是S_TRAP。这是可以在GPU程序中触发陷阱的指令,与x86的INT n指令如出一辙。
GCN定义了十几种指令编码格式,S_TRAP属于SOPP类型,其编码格式如下图所示。
可见,S_TRAP指令的长度是一个DWORD(32位),低16位为立即数,用来指示陷阱号(TRAP_ID)。因为高位部分是固定的,所以S_TRAP指令的机器码总是0xBF92xxxx这样的编码。
迷信一点说,我第一次看到S_TRAP指令就觉得亲切,觉得它可以用来实现断点功能。但是调试器中是否真的使用这个指令设置断点,还是另有妙方呢?
这个周末在写作《软件调试》中的相关内容。虽然凭经验和直觉,觉得上面的推测把握很大。但是没有官方文档描述,也没有源代码证实,把推测的结论写进书里让我很不安。
怎么办呢?无奈之时,突然灵光一现。可以上IDA啊。IDA是著名的反汇编工具。引起名称与著名的程序员之母Ada Lovelace相似,所以其图标是就是Ada。
几年前曾经购买过正版的IDA,但是光盘和注册码不知在何处了。只好临时到官网下载一个免费版本。
安装很顺利,启动IDA,美丽的图标一闪,就到主界面了。打开AMD官方调试SDK中的核心调试模块(DBE)。然后找到用来设置软件断点的API——HwDbgCreateCodeBreakpoint。
HwDbgCreateCodeBreakpoint内部经过几次调用,最终执行实际断点设置和恢复的是下面这个方法:
bool HwDbgBreakpoint::Set(HwDbgBreakpoint *const this, bool enable)
在这个函数内部,看到多处0xBFxxxxxx这样的“身影”:
cmp eax, 0BF800000h
cmp eax, 0BF920000h
上面一个是S_NOP指令的机器码,后一个就是S_TRAP。特别地,下面这几条指令让我如释重负:
.text: 00A8 mov edx, 0BF920007h
.text: 00AD mov rax, [rdi]
.text: 00B0 call qword ptr [rax+18h]
其中的0BF920007h是触发TRAP_ID为7的陷阱,结合另一处看到的常量定义:
var TRAP_ID_DEBUGGER = 0x07
于是,可以确定无疑,官方调试SDK中设置代码断点的方法就是使用S_TRAP指令,更确切地说,是S_TRAP 0x7。这与INT 3何其相似。
感谢IDA,感谢Ada!
AI时代,我们一起学习GPU。希望深入学习GPU的格友,欢迎垂询第二期GPU研习班。
***********************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生。
欢迎关注格友公众号