本文记录了探索NVIDIA CUDA SASS语法对应的二进制位的过程。
1. CUDA二进制文件
1.1 SASS
NVCC编译过程和解读CUDA汇编PTX(二) SASS nvdisasm工具提过CUDA的汇编SASS,使用cuobjdump工具反编译出的SASS格式如下:
/* 0x083fc400e3e007f6 */
/*0008*/ MOV R1, c[0x0][0x20]; /* 0x4c98078000870001 */
/*0010*/ S2R R2, SR_TID.X; /* 0xf0c8000002170002 */
/*0018*/ ISCADD R4.CC, R2.reuse, c[0x0][0x150], 0x2; /* 0x4c18810005470204 */
第二列为反编译出的human-friendly code,包含了opcode(指令)、modifer(修饰符)、oprand(操作数)。
第三列即为实际二进制中的内容。需要注意的是,在反编译出的SASS文件中,第三列是以大端模式显示,但实际文件存储中是小端模式。
1.2 ELF格式
cu文件部分最后被编译为cubin文件,是一种ELF格式的可执行文件。
左边是ELF的链接视图,可以理解为是目标代码文件的内容布局。右边是ELF的执行视图,可以理解为可执行文件的内容布局。注意目标代码文件的内容是由section组成的,而可执行文件的内容是由segment组成的。
我们目前只需要关注linking view下文件结构即可。
1.3 example
使用readelf -a ./vector_add.sm_50.cubin
获得的部分内容如下(可以使用-w 宽模式来显示输出):
ELF 头:
Magic: 7f 45 4c 46 02 01 01 33 07 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: <未知:33>
ABI 版本: 7
类型: EXEC (可执行文件)
系统架构: NVIDIA CUDA architecture
版本: 0x5a
入口点地址: 0x0
程序头起点: 1696 (bytes into file)
Start of section headers: 1184 (bytes into file)
标志: 0x320532
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 3
节头大小: 64 (字节)
节头数量: 8
字符串表索引节头: 1
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .shstrtab STRTAB 0000000000000000 00000040
00000000000000b2 0000000000000000 0 0 1
[ 2] .strtab STRTAB 0000000000000000 000000f2
00000000000000cf 0000000000000000 0 0 1
[ 3] .symtab SYMTAB 0000000000000000 000001c8
0000000000000060 0000000000000018 2 2 8
[ 4] .nv.info LOPROC+0 0000000000000000 00000228
0000000000000024 0000000000000000 3 0 4
[ 5] .nv.info._Z9vecto LOPROC+0 0000000000000000 0000024c
000000000000005c 0000000000000000 3 7 4
readelf:警告:[ 6]: Unexpected value (7) in info field.
[ 6] .nv.constant0._Z9 PROGBITS 0000000000000000 000002a8
000000000000015c 0000000000000000 A 0 7 4
readelf:警告:[ 7]: Unexpected value (83886083) in info field.
[ 7] .text._Z9vectorAd PROGBITS 0000000000000000 00000420
0000000000000080 0000000000000000 AX 3 83886083 32
关于ELF文件格式不再赘述,资料很多。我们关心的是节头列表中[7]text._Z9vectorAdd的偏移量00000420,这实际上表明了kernel函数在cubin文件中的位置。使用sublime等编辑器打开cubin文件,下面即为cubin文件0x420字节处的内容:
f607 e0e3 00c4 3f08 0100 8700 8007 984c
可以发现恰好是0x083fc400e3e007f6 0x4c98078000870001
的小端模式
2. 探索指令对应的二进制位
一个比较经典的方法是将cubin文件的二进制位按位翻转,翻转一个二进制位后,再次使用cuobjdump反编译出sass,看修改的对应代码行是否改变。
以/*0008*/ MOV R1, c[0x0][0x20]; /* 0x4c98078000870001 */
为例,将对应的cubin文件行二进制翻转一位,解析成新的SASS文件,如果新的文件中不再是MOV指令,则说明翻转的这一位属于MOV的控制位。通过循环翻转每一位,可以得出MOV指令到底由哪几位决定。
Zhang的论文便使用了这种方法,并将代码开源到了github:
https://github.com/PAA-NCIC/DeepPerf/tree/master/Solver
同理,modifer和oprand也可以按照这种方法得出。具体可以参看Reference中的论文。
3. CC 7.0+
Jia的论文中探索了Volta和Turing架构的变化。一条指令编码由原来的64-bit,变为128bit,原来的架构是三条指令编码配一条控制编码。
我修改了Zhang论文开源的代码,对CC 7.0+做了适配,更新中:
https://github.com/FindHao/nv_bin_explorer
需要注意的是,opcode的编码受modifer和oprand的影响,因此不能只跑opcoder,还需要解析出modifer和oprand才能真正搞清楚opcode的编码模式。
Reference中的论文都需要仔细研读。
Reference
Zhang X, Tan G, Xue S, et al. Understanding the gpu microarchitecture to achieve bare-metal performance tuning[J]. ACM SIGPLAN Notices, 2017, 52(8): 31-43.
Jia Z, Maggioni M, Smith J, et al. Dissecting the NVidia Turing T4 GPU via Microbenchmarking[J]. arXiv preprint arXiv:1903.07486, 2019.
Jia Z, Maggioni M, Staiger B, et al. Dissecting the nvidia volta gpu architecture via microbenchmarking[J]. arXiv preprint arXiv:1804.06826, 2018.
Ari B. Hayes etc.Decoding CUDA binary, CGO 2019
Comments