C语言的机器表示

简介: 《基础系列》

C语言等高级语言编写的程序必须经过编译器转换为汇编语言,再由汇编器转换为指令码才能在CPU上执行。本节简要介绍高级语言转换为指令码涉及的一些问题,为方便起见,选择C语言和LoongArch汇编码进行介绍。

2.6.1 过程调用

过程调用是高级语言程序中的一个关键特性,它可以让特定程序段的内容与其他程序和数据分离。过程接受参数输入,并通过参数返回执行结果。C语言中过程和函数的概念相同,本节后面也不进行区分。过程调用中,调用者和被调用者必须遵循同样的接口约定,包括寄存器使用、栈的使用和参数传递的约定等。这部分涉及内容较多,将在第4章中进行详细的介绍。本节中,主要介绍过程调用的流程和其中与指令集相关的内容。

在LoongArch指令集中,负责函数调用的指令是BL,这是一条相对转移指令。该指令在跳转的同时还将其下一条指令的地址放入1号通用寄存器(记为$ra)中,作为函数返回地址。负责函数返回的指令是JR2,属于间接跳转指令,该指令的操作数为寄存器,因此LoongArch汇编中最常见的函数返回指令是jr $ra。

除了调用和返回的指令外,函数调用和执行过程中还需要执行一系列操作:

  • 调用者(S)将参数(实参)放入寄存器或栈中;
  • 使用BL指令调用被调用者(R);
  • R在栈中分配自己所需要的局部变量空间;
  • 执行R过程;
  • R释放局部变量空间(将栈指针还原);
  • R使用JR指令返回调用者S。

默认情况下,通用寄存器$r4~$r11(记为$a0~$a7)作为参数输入,其中$r4和$r5同时也作为返回值,通用寄存器$r12~$r20(记为$t0~$t8)作为子程序的暂存器无须存储和恢复。LoongArch中没有专门的栈结构和栈指针,通用寄存器$r3(记为$sp)通常作为栈指针寄存器,指向栈顶。

一个简单的C语言过程调用程序及其LoongArch汇编码如表2.11所示。

表 2.11: 过程调用及其LoongArch机器表示

C代码

LoongArch汇编

int add(int a,int b)
{
 return a+b;
}

int ref(void)
{

 int t1 = 12;
 int t2 = 34;

 return add(t1,t2);
}

add:
 add.w $a0, $a0, $a1 //a+b
 jr $ra //return
ref:
 addi.d $sp, $sp, -16 //stack allocate
 addi.w $a1, $r0, 34 //t2=34
 addi.w $a0, $r0, 12 //t1=12
 st.d $ra, $sp, 8 //save $ra
 bl add //call add()
 ld.d $ra, $sp, 8 //restore $ra
 addi.d $sp, $sp, 16 //stack release
 jr $ra //return

add程序是被调用的子程序,由于程序功能很简单,因此无须使用栈来存储任何信息,其输入参数存放在$a0、$a1两个寄存器中,计算的结果存放在$a0寄存器中。

ref程序是add程序的调用者,通过BL指令进行调用,BL指令会修改$ra寄存器的值,因此在ref中需要将$ra寄存器的值保存到栈中,栈顶指针和RA值存放的位置遵循LoongArch函数调用规范,这部分内容将在4.1节中进行介绍。add程序的返回值放在$a0寄存器中,这同时也是ref程序的返回值,因此无须进行更多搬运。

2.6.2 流程控制语句

C语言中的控制流语句共有9种,可分为三类:辅助控制语句、选择语句、循环语句,如表2.12所示。

表 2.12: C语言控制流语句

控制流语句

选择语句

if ~ else

switch ~ case

循环语句

for

while

do ~ while

辅助控制语句

break

continue

goto

return

(1)辅助控制语句

goto语句无条件地跳转到程序中某标号处,其作用与无条件相对跳转指令相同,在LoongArch指令集中表示为B指令跳转到一个标号。break、continue语句的作用与goto类似,只是跳转的标号位置不同。return语句将过程中的变量作为返回值并直接返回,在编译器中对应于返回值写入和返回操作。

(2)选择语句

if~else语句及其对应的LoongArch汇编码如表2.13所示。

表 2.13: if~else语句及其LoongArch汇编表示

C代码

LoongArch汇编

if (cond_exp)
 then_statement
else
 else_statement

 move $t0, cond_exp
 beqz $t0, .L1
 <then_statement>
 b  .L2
.L1:
 <else_statement>
.L2:

这里的if ~ else实现采用了BEQZ指令,当$t0寄存器的值等于0时进行跳转,跳转到标号.L1执行“else”分支中的操作,当$t0寄存器的值不等于0时,则顺序执行“then”分支中的操作并在完成后无条件跳转到标号.L2处绕开“else”分支。

switch ~ case语句的结构更为复杂,由于可能的分支数较多,通常会被映射为跳转表的形式,如表2.14所示。如果在编译选项中加入-fno-jump-tables的选项,那么switch ~ case语句还可以被映射为跳转级联的形式,如表2.15所示。表中“alsl.d rd, rj, rk, sa”所进行的操作是:GR[rd] = (GR[rj] << sa) + GR[rk]。即将rj号通用寄存器中的值先左移sa位再与rk号通用寄存器中的值相加,结果写入rd号通用寄存器中。

表 2.14: switch~case语句及其跳转表形式的LoongArch机器表示

C代码

LoongArch汇编

int st(int a, int b, int c)
{
 switch (a) {
  case 15:
   c = b & 0xf;
  case 10:
   return c + 50;
  case 12:
  case 17:
   return b + 50;
  case 14:
   return b;
  default:
   return a;
 }
}

st:
 addi.w $t0,$a0, -10 //a-10
 sltui $t1,$t0, 8
 beqz $t1, default //if (a-10)>=8
       //goto default
 la  $t2, jr_table
 alsl.d $t1, $t0, $t2, 3
       //(a-10)*8+jr_table
 ld.d $t0, $t1, 0
 jr  $t0
default:
 or  $a1,$a0,$r0
case_14:
 or  $a0,$a1,$r0
 jr  $ra  //return b for case_14,
     //return a for default
case_15:
 andi $a2,$a1,0xf  //b & 0xf
case_10:
 addi.w $a1,$a2,50  //c+50
 b  case_14
case_12_17:
 addi.w $a1,$a1,50  //b+50
 b  case_14
       # jump table
 .section .rodata
 .align 3
jr_table:
 .dword case_10
 .dword default
 .dword case_12_17
 .dword default
 .dword case_14
 .dword case_15
 .dword default
 .dword case_12_17

表 2.15: switch-case语句及其跳转级联形式的LoongArch机器表示

C代码

LoongArch汇编

int st(int a, int b, int c)
{
 switch (a) {
  case 15:
   c = b & 0xf;
  case 10:
   return c + 50;
  case 12:
  case 17:
   return b + 50;
  case 14:
   return b;
  default:
   return a;
 }
}

st:
 addi.w $t0,$r0,14
 beq  $a0,$t0,.L7  //(a==14)?
 blt  $t0,$a0,.L3  //(a>14)?
 addi.w $t0,$r0,10
 beq  $a0,$t0,.L4  //(a==10)?
 addi.w $t0,$r0,12
 beq  $a0,$t0,.L5  //(a==12)?
 jr  $ra    //return a
.L3:
 addi.w $t0,$r0,15
 beq  $a0,$t0,.L6  //(a==15)?
 addi.w $t0,$r0,17
 beq  $a0,$t0,.L5  //(a==17)?
 jr  $ra    //return a
.L6:
 andi $a2,$a1,0xf  //b & 0xf
.L4:
 addi.w $a0,$a2,50  //c + 50
 jr  $ra
.L5:
 addi.w $a0,$a1,50  //b + 50
 jr  $ra
.L7:
 or  $a0,$a1,$r0  //return b
 jr  $ra

在这个例子中,$t0寄存器存放各case分支的值并依次与第一个参数a(存放在$a0寄存器中)进行比较,根据比较的结果分别跳转到指定标号。读者可自行分析各case分支的执行流。通过比较表2.14和2.15中的汇编代码可以看到,在case分支较多时,采用跳转表实现有助于减少级联的转移指令。

2.6.3 循环语句

循环语句均可映射为条件跳转指令,与选择语句的区别就在于跳转的目标标号在程序段已执行过的位置(backward)。三种循环语句的C语言及其对应的LoongArch汇编码如表2.16所示。

表 2.16: 循环语句及其LoongArch机器表示

C代码

LoongArch汇编

int test_for(int a) {
 int sum = 0;
 int i = 0;

 for (i = 0; i < a; i++) {
  sum += i;
 }

 return sum;
}


int test_while(int a) {
 int sum = 0;
 int i = 0;

 while (i < a) {
  sum += i;
  i++;
 }

 return sum;
}


int test_dowhile(int a) {
 int sum = 0;
 int i = 0;

 do {
  sum += i;
  i++;
 } while (i < a);

 return sum;
}

test_for:
 or  $t0,$r0,$r0
 or  $t1,$r0,$r0
.L2:
 blt  $t0,$a0,.L3
 or  $a0,$t1,$r0
 jr  $ra
.L3:
 add.w $t1,$t1,$t0
 addi.w $t0,$t0,1
 b  .L2


test_while:
 or  $t0,$r0,$r0
 or  $t1,$r0,$r0
.L2:
 blt  $t0,$a0,.L3
 or  $a0,$t1,$r0
 jr  $ra
.L3:
 add.w $t1,$t1,$t0
 addi.w $t0,$t0,1
 b  .L2

test_dowhile:
 // a : $a0
 // sum : $t0
 // i : $t1
 or $t0,$r0,$r0
 or $t1,$r0,$r0
.L1:
 add.w $t0,$t0,$t1
 addi.w $t1,$t1,1
 blt $t1,$a0,.L1
 or $a0,$t1,$r0
 jr $ra

2.7 本章小结

本章介绍了指令系统在整个计算机系统中位于软硬件界面的位置,讨论了指令系统设计的原则和影响因素,并从指令内容、存储管理、运行级别三个角度介绍指令系统的发展历程。

本章首先介绍了指令集的关键要素——地址空间定义、指令操作数、指令操作码,随后对几种不同的RISC指令集进行了比较,最后以LoongArch指令集为例给出了C语言和指令汇编码之间的对应关系。

相关文章
|
C语言
C语言测试机器大小端的方法
C语言测试机器大小端的方法
88 0
|
存储 小程序 C语言
【C语言】请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序
简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序
|
存储 网络协议 C语言
【CSAPP】x86-64的机器代码和原始的C代码差别巨大,一些常在C语言中隐藏的处理器状态
【CSAPP】x86-64的机器代码和原始的C代码差别巨大,一些常在C语言中隐藏的处理器状态
82 0
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
32 3
|
4天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
19 6
|
24天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
33 10
|
17天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
23天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
53 7
|
23天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
29 4