RISCV 汇编指令调试

RISCV 汇编指令调试

RISCV汇编指令调试,立即数使用分析

新建文件 test.s

1
2
3
4
5
6
.global main

.text
main:
li a5, 0x12345678
ret

riscv.....gcc test.s 编译成汇编代码
riscv.....gdb test.s 用调试器运行

进入调试器

1
2
3
4
5
(gdb) disassemble main
Dump of assembler code for function _init:
0x000000000001019c <+182>: lui a5,0x12345
0x00000000000101a0 <+186>: addiw a5,a5,1656
0x00000000000101a4 <+190>: ret

在windows 和Ubuntu上打印的汇编信息由所区别,Windows上会打印额外的很多信息,
但是从0x0000000001019c开始的3行信息都是和Ubuntu上一致。一般情况下RISCV程序地址从0x10000开始
其余信息可能是一些stdio程序,并不是很清楚

用Spike 调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$: $RISCV/bin/spike -d pk a.out
: until pc 0 0x1019c
: reg 0 a5
0x0000000000000002
: reg 0 #查看core0 的所有32个寄存器
zero: 0x0000000000000000 ra : 0x00000000000100e2 sp : 0x000000007f7e9b50 gp : 0x00000000000123d0
tp : 0x0000000000000000 t0 : 0x00000000000100c8 t1 : 0x0000000000000000 t2 : 0x0000000000000000
s0 : 0x0000000000000000 s1 : 0x0000000000000000 a0 : 0x0000000000000001 a1 : 0x000000007f7e9b58
a2 : 0x0000000000000000 a3 : 0x0000000000000001 a4 : 0x0000000000000010 a5 : 0x0000000000000002
a6 : 0x000000000000001f a7 : 0x0000000000000000 s2 : 0x0000000000000000 s3 : 0x0000000000000000
s4 : 0x0000000000000000 s5 : 0x0000000000000000 s6 : 0x0000000000000000 s7 : 0x0000000000000000
s8 : 0x0000000000000000 s9 : 0x0000000000000000 s10 : 0x0000000000000000 s11 : 0x0000000000000000
t3 : 0x0000000000000000 t4 : 0x0000000000000000 t5 : 0x0000000000000000 t6 : 0x0000000000000000
: help #查看调试命令
: run 1 #单步执行
core 0: 0x000000000001019c (0x123457b7) lui a5, 0x12345
: reg 0 a5
0x0000000012345000 #可以看到将原来的 0x0000000000000002 覆盖成 0x12345000 ,低12bit覆盖为0
: run 1
core 0: 0x00000000000101a0 (0x6787879b) addiw a5, a5, 1656
: reg 0 a5
0x0000000012345678 #成功写入

试验

1
2
3
4
5
main:
li a5, 0x12345678
li a1, 0xabc
li a2, 0x12345
ret

编译后的汇编指令

1
2
3
4
5
6
7
8
9
10
(gdb) disassemble main 
Dump of assembler code for function main:
0x000000000001019c <+0>: lui a5,0x12345
0x00000000000101a0 <+4>: addiw a5,a5,1656
0x00000000000101a4 <+8>: lui a1,0x1 # a1=0x00001000 , 低12bit=0
0x00000000000101a8 <+12>: addiw a1,a1,-1348 # hex(0x1000 - 1348) = 0xabc
0x00000000000101ac <+16>: lui a2,0x12
0x00000000000101b0 <+20>: addiw a2,a2,837
0x00000000000101b4 <+24>: ret
End of assembler dump.

即便是小于12bit 的立即数也不能被立即load到,因为LUI指令加载的最小数是0x1000(除了0),

所以所有小于12bit的数val还是要用两条指令完成

1
2
LUI   rd, 0x1  #load rd 0x1000
ADDIW rd, rs, (val-0x1000)

C代码编译调试

例子1

1
2
3
4
void main() {
int val = 1 ;
val+=3 ;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    .file   "a.c"
.option nopic
.text
.align 1
.globl main
.type main, @function
main:
addi sp,sp,-32 #sp指针默认从比较大的地址开始,反向增长开辟了32byte的空间
sd s0,24(sp) #将s0的8个字节(64bit)压入栈指针-32+24=-8
addi s0,sp,32 #然后将栈BASE地址放入s0 (s0 被用来当中间寄存器)
li a5,1 #加载立即数1到a5 (小于32位的立即数会直接通过立即数指令加载) 否则就需要在内存开辟双字节整形,然后通过ld 加载
sw a5,-20(s0) #给val开辟栈空间,变量都会在栈里面,只有申请内存空间才会在dynamicdata(堆)
lw a5,-20(s0) #再读回来
addiw a5,a5,3 #+3
sw a5,-20(s0) #再写回去,(不开优化,每一次操作都会存取)
nop
ld s0,24(sp) #恢复s0 寄存器
addi sp,sp,32 #恢复栈指针
jr ra #main函数返回
.size main, .-main
.ident "GCC: (GNU) 8.1.0"

编译后进gdb调试 a.out

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) disassemble  main 
Dump of assembler code for function main:
0x000000000001019c <+0>: addi sp,sp,-32
0x000000000001019e <+2>: sd s0,24(sp) #表示sp寄存器的内容+立即数偏移24
0x00000000000101a0 <+4>: addi s0,sp,32
0x00000000000101a2 <+6>: li a5,1
0x00000000000101a4 <+8>: sw a5,-20(s0)
0x00000000000101a8 <+12>: lw a5,-20(s0)
0x00000000000101ac <+16>: addiw a5,a5,1
0x00000000000101ae <+18>: sw a5,-20(s0)
0x00000000000101b2 <+22>: nop
0x00000000000101b4 <+24>: ld s0,24(sp)
0x00000000000101b6 <+26>: addi sp,sp,32
0x00000000000101b8 <+28>: ret
End of assembler dump.

定义大于32bit的整数

1
2
3
4
5
long main(){
long val = 0x876543210; //超过32位的长整数
val+=1;
return val;
}

不开启优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
        .file   "a.c"
.option nopic
.text
.align 1
.globl main
.type main, @function
main:
addi sp,sp,-32
sd s0,24(sp)
addi s0,sp,32
lui a5,%hi(.LC0)
ld a5,%lo(.LC0)(a5) #load doubleworld from (.LC0)地址
sd a5,-24(s0)
ld a5,-24(s0)
addi a5,a5,1
sd a5,-24(s0)
ld a5,-24(s0)
mv a0,a5
ld s0,24(sp)
addi sp,sp,32
jr ra
.size main, .-main
.section .rodata
.align 3
.LC0:
.dword 36344967696 #开辟一块双字节的MEM空间,
.ident "GCC: (GNU) 8.1.0"

发现超过32bit的整数,不能通过立即数加载到寄存器(立即数受限于指令的长度最大只能是32bit,而且需要两条指令完成), 只能在内存中开辟一块地址先存放0x876543210,然后ld a5,该地址

例子2

1
2
3
4
#include <stdio.h>
void main(){
printf("hello world");
}

hello.s 查看汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    .file   "hello.c"
.option nopic
.text
.section .rodata
.align 3
.LC0:
.string "hello world"
.text
.align 1
.globl main
.type main, @function
main:
addi sp,sp,-16 #sp 开辟16byte栈空间 sp=-16
sd ra,8(sp) #将ra的8Byte(64bit)内容从sp=-8处压栈 , 0 ~-8 存ra的值
sd s0,0(sp) #将s0的8Byte(64bit)内容从sp=-16处压栈,-8 ~-16存s0的值
addi s0,sp,16 #将sp=offset0的值存入s0
lui a5,%hi(.LC0) #计算string .LC0的地址,存入a0
addi a0,a5,%lo(.LC0) #这两条指令等价于 伪指令 li &(.LC0)
call printf #调用 printf 函数 ,printf 使用a0的内容作为参数
nop
ld ra,8(sp) #恢复栈 0~-8 的内容到ra
ld s0,0(sp) #恢复栈-8~-16的内容到s0
addi sp,sp,16 #栈指针恢复到0
jr ra #返回ra
.size main, .-main
.ident "GCC: (GNU) 8.1.0"

gdb hell.out 查看汇编以后的机器码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000010198 <+0>: addi sp,sp,-16
0x000000000001019a <+2>: sd ra,8(sp)
0x000000000001019c <+4>: sd s0,0(sp)
0x000000000001019e <+6>: addi s0,sp,16
0x00000000000101a0 <+8>: lui a5,0x1a # (.LC0)的起始地址就是0x19a10
0x00000000000101a2 <+10>: addi a0,a5,-1520 # 0x19a10
0x00000000000101a6 <+14>: jal ra,0x10354 <printf> # pc+4 存入ra, 跳转到pc+0x10354*2的printf函数地址
0x00000000000101aa <+18>: nop
0x00000000000101ac <+20>: ld ra,8(sp) # 恢复ra
0x00000000000101ae <+22>: ld s0,0(sp) # 恢复s0
0x00000000000101b0 <+24>: addi sp,sp,16 # 释放栈
0x00000000000101b2 <+26>: ret
End of assembler dump.

print内容稍加修改

1
2
3
4
#include <stdio.h>
void main(){
printf("Hello %s!\n","World");
}

查看效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000010198 <+0>: addi sp,sp,-16
0x000000000001019a <+2>: sd ra,8(sp)
0x000000000001019c <+4>: sd s0,0(sp)
0x000000000001019e <+6>: addi s0,sp,16
0x00000000000101a0 <+8>: lui a5,0x1a
0x00000000000101a2 <+10>: addi a1,a5,-1504 # 0x19a20 #加载字符串"Hello %s!\n" 的地址
0x00000000000101a6 <+14>: lui a5,0x1a
0x00000000000101a8 <+16>: addi a0,a5,-1496 # 0x19a28 #加载字符串"Wrold"的地址
0x00000000000101ac <+20>: jal ra,0x1035a <printf> #调用printf, printf会从a0,a1内获取参数
0x00000000000101b0 <+24>: nop
0x00000000000101b2 <+26>: ld ra,8(sp)
0x00000000000101b4 <+28>: ld s0,0(sp)
0x00000000000101b6 <+30>: addi sp,sp,16
0x00000000000101b8 <+32>: ret
End of assembler dump.

字符串的只需要知道起始地址即可,结束位由隐藏的固定格式构成

例3

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
int a = 2 ;
printf("Hello %s!\n","World");
a++;
return a ;
}

查看机器码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000010198 <+0>: addi sp,sp,-32 # 开辟32Byte栈空间
0x000000000001019a <+2>: sd ra,24(sp) # 0 ~-8 存ra
0x000000000001019c <+4>: sd s0,16(sp) #-9 ~-16存s0
0x000000000001019e <+6>: addi s0,sp,32 # 栈根给s0
0x00000000000101a0 <+8>: li a5,2
0x00000000000101a2 <+10>: sw a5,-20(s0) # 2存入a5 压栈到-16~-20
0x00000000000101a6 <+14>: lui a5,0x1a # a0
0x00000000000101a8 <+16>: addi a1,a5,-1488 # 0x19a30 #加载字符串"Hello %s!\n" 的地址到a1
0x00000000000101ac <+20>: lui a5,0x1a
0x00000000000101ae <+22>: addi a0,a5,-1480 # 0x19a38 #加载字符串"World" 的地址到a0
0x00000000000101b2 <+26>: jal ra,0x1036e <printf>
0x00000000000101b6 <+30>: lw a5,-20(s0) #从栈-20 读出32bit=4Byte 出来
0x00000000000101ba <+34>: addiw a5,a5,1
0x00000000000101bc <+36>: sw a5,-20(s0)
0x00000000000101c0 <+40>: lw a5,-20(s0)
0x00000000000101c4 <+44>: mv a0,a5 #a++结果从栈中读出给a0 ,a0 a1 寄存器常用作返回值
0x00000000000101c6 <+46>: ld ra,24(sp)
0x00000000000101c8 <+48>: ld s0,16(sp)
0x00000000000101ca <+50>: addi sp,sp,32
0x00000000000101cc <+52>: ret #返回
End of assembler dump.

开启优化选项 -O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000010198 <+0>: addi sp,sp,-16
0x000000000001019a <+2>: sd ra,8(sp)
0x000000000001019c <+4>: lui a1,0x1a
0x00000000000101a0 <+8>: addi a1,a1,-1520 # 0x19a10
0x00000000000101a4 <+12>: lui a0,0x1a
0x00000000000101a8 <+16>: addi a0,a0,-1512 # 0x19a18
0x00000000000101ac <+20>: jal ra,0x10358 <printf>
0x00000000000101b0 <+24>: li a0,3 # 2 + 1 =3 在编译阶段就计算完
0x00000000000101b2 <+26>: ld ra,8(sp)
0x00000000000101b4 <+28>: addi sp,sp,16
0x00000000000101b6 <+30>: ret
End of assembler dump.

总结

LUI 指令会讲20比特的立即数放到 寄存器的31:12位置,并且将低12bit覆盖为0

ADDIW 指令会将 rs1 寄存器的值导入到rd 寄存器,并且用立即数[11:0] 覆盖 rd的低12比特

LU是伪指令,因为汇编指令的立即数肯定小于32bit,所以为了实现完整的32bit的立即数load

编译器使用了2条指令LUI,ADDIW 实现了LU的功能

  • 对于一般变量比如int类型,会分配给栈来存放
  • 如果比较大的常数变量,超过32bit或者64bit的数, 地址会分配堆内存给地址
  • 对于数字串分配堆内存地址

riscv存储空间分配

参考

-RISC-V Reference Card(指令绿卡片)

-RISC-V 指令集编码

# riscv

评论