VS Code 编写一个最简单 C 语言程序代码: a.c
int main(){
int c = 0;
c++;
return c;
}
使用 VC2022 编译器命令行编译,运行,显示计算结果
cl .\a.c
.\a.exe
$LASTEXITCODE
cl
是VC编译器的编译程序,PowerShell
里使用$LASTEXITCODE
用来显示之前运行程序的返回值
VC2022 编译使用 dumpbin.exe
工具可以用来显示程序的汇编代码
dumpbin.exe /DISASM .\a.obj
Dump of file .\a.obj
main:
0000000000000000: 48 83 EC 18 sub rsp,18h
0000000000000004: C7 04 24 00 00 00 mov dword ptr [rsp],0
00
000000000000000B: 8B 04 24 mov eax,dword ptr [rsp]
000000000000000E: FF C0 inc eax
0000000000000010: 89 04 24 mov dword ptr [rsp],eax
0000000000000013: 8B 04 24 mov eax,dword ptr [rsp]
0000000000000016: 48 83 C4 18 add rsp,18h
000000000000001A: C3 ret
- 下面是对你提供的汇编指令的逐行解释:
汇编代码解析
main:
0000000000000000: 48 83 EC 18 sub rsp,18h
sub rsp, 18h
: 从栈指针 (rsp
) 中减去0x18
(24十进制)。这为函数的局部变量和返回地址留出空间。
0000000000000004: C7 04 24 00 00 00 mov dword ptr [rsp],0
00
mov dword ptr [rsp], 0
: 将0
移动到栈顶(即刚刚调整的rsp
指向的位置),初始化局部变量为0
。
000000000000000B: 8B 04 24 mov eax, dword ptr [rsp]
mov eax, dword ptr [rsp]
: 将栈顶的值(即之前存储的0
)加载到寄存器eax
中。
000000000000000E: FF C0 inc eax
inc eax
: 将eax
中的值加1
,所以此时eax
的值变为1
。
0000000000000010: 89 04 24 mov dword ptr [rsp], eax
mov dword ptr [rsp], eax
: 将eax
中的值(即1
)存回栈顶。
0000000000000013: 8B 04 24 mov eax, dword ptr [rsp]
mov eax, dword ptr [rsp]
: 再次将栈顶的值(此时为1
)加载到eax
中。
0000000000000016: 48 83 C4 18 add rsp, 18h
add rsp, 18h
: 释放之前为局部变量分配的栈空间,恢复rsp
的值。
000000000000001A: C3 ret
ret
: 返回到调用该函数的地方。此时,eax
中的值(1
)通常作为函数的返回值。
总结
这段汇编代码实现了一个简单的函数,它:
- 在栈上分配空间。
- 初始化一个局部变量为
0
。 - 将该变量的值加
1
。 - 最后返回这个值(
1
)。
这个过程展现了如何操作栈以及基本的寄存器使用。
使用 Risc-V linux
来学习 C语言代码,转成 RV 汇编代码
把C语言代码保存成
001.c
int main(){ int c = 0; c++; return c; }
我们可以使用
gcc
命令把源码转成汇编代码来学习gcc -S 001.c gcc -c -march=rv64g 001.c objdump -d 001.o
这三行命令的逐行解释:
1. gcc -S 001.c
- 命令:
gcc -S 001.c
解释:
gcc
是 GNU C 编译器。-S
选项告诉编译器生成汇编代码,而不是生成目标文件或可执行文件。001.c
是输入的 C 源文件。
- 结果: 该命令会生成一个名为
001.s
的汇编源文件,包含了从 C 代码转换而来的汇编代码。
2. gcc -c -march=rv64g 001.c
- 命令:
gcc -c -march=rv64g 001.c
解释:
-c
选项表示编译源文件为目标文件(即生成.o
文件),而不进行链接。-march=rv64g
指定生成代码的架构为 RISC-V 64 位通用架构(RV64G)。001.c
是输入的 C 源文件。
- 结果: 该命令会生成一个名为
001.o
的目标文件,其中包含编译后的机器代码。
3. objdump -d 001.o
- 命令:
objdump -d 001.o
解释:
objdump
是一个用来显示目标文件信息的工具。-d
选项表示反汇编目标文件,显示机器代码对应的汇编指令。001.o
是输入的目标文件。
- 结果: 该命令会输出
001.o
文件中机器代码的汇编表示,帮助开发者理解编译后的代码。
总结
这三行命令的流程是:
- 将 C 源代码转换为汇编代码(
gcc -S
)。 - 将 C 源代码编译为特定架构的目标文件(
gcc -c
)。 - 反汇编目标文件以查看机器代码的汇编指令(
objdump -d
)。
如图我们看到C语言转成后的 RISC-V 汇编代码
汇编代码解析
main:
.LFB0:
main:
: 定义一个名为main
的函数。.LFB0:
: 这是一个局部符号,通常用于调试信息。
addi sp, sp, -32
addi sp, sp, -32
: 将栈指针 (sp
) 向下移动32
字节,为局部变量和返回地址分配空间。
sd s0, 24(sp)
sd s0, 24(sp)
: 将寄存器s0
的值存储到栈中,位置为sp + 24
。这是保存调用者的s0
的值,以便在返回时恢复。
addi s0, sp, 32
addi s0, sp, 32
: 将s0
的值设置为sp + 32
。这意味着s0
现在指向一个局部变量的地址。
sw zero, -20(s0)
sw zero, -20(s0)
: 将0
存储到s0 - 20
的位置,即初始化一个局部变量为0
。
lw a5, -20(s0)
lw a5, -20(s0)
: 从s0 - 20
的位置加载值到a5
。此时a5
中的值为0
。
addiw a5, a5, 1
addiw a5, a5, 1
: 将a5
的值加1
,并存储回a5
。现在a5
的值为1
。
sw a5, -20(s0)
sw a5, -20(s0)
: 将a5
中的值(1
)存储回s0 - 20
的位置。
lw a5, -20(s0)
lw a5, -20(s0)
: 再次从s0 - 20
的位置加载值到a5
,此时a5
的值为1
。
mv a0, a5
mv a0, a5
: 将a5
的值移动到a0
。在 RISC-V 中,a0
通常用于函数的返回值。因此,a0
现在也为1
。
ld s0, 24(sp)
ld s0, 24(sp)
: 从栈中恢复之前保存的s0
的值。
addi sp, sp, 32
addi sp, sp, 32
: 恢复栈指针 (sp
),将其向上移动32
字节,释放之前分配的栈空间。
jr ra
jr ra
: 跳转到返回地址,结束函数的执行并返回到调用该函数的位置。
总结
这段 RISC-V 汇编代码实现了一个简单的函数,它:
- 在栈上分配空间。
- 保存寄存器
s0
的值。 - 初始化一个局部变量为
0
。 - 将该变量的值加
1
。 - 将结果(
1
)作为返回值返回。
这个过程展示了 RISC-V 中栈的使用、寄存器的操作以及函数返回值的传递。