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语言测试机器大小端的方法
115 0
|
存储 小程序 C语言
【C语言】请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序
简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序
|
存储 网络协议 C语言
【CSAPP】x86-64的机器代码和原始的C代码差别巨大,一些常在C语言中隐藏的处理器状态
【CSAPP】x86-64的机器代码和原始的C代码差别巨大,一些常在C语言中隐藏的处理器状态
94 0
|
1月前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
37 3
|
1月前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
20 2
|
1月前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
66 16
|
1月前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
52 1
|
1月前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
61 24
|
1月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
63 23

热门文章

最新文章