《 汇编语言编程基础 基于 LoongArch 》读书与实践笔记

《 汇编语言编程基础 基于 LoongArch 》读书与实践笔记


更新记录

       1. 第一章笔记,2023.02.10;
       2. 第二章笔记,2023.02.13;
       3. 第三章笔记,2023.02.17;
       4. 第四章笔记,2023.02.18;
       5. 第五章笔记,2023.02.23;
       6. 第六章笔记,2023.02.28;
       6. 第七章笔记,2023.03.06;
       6. 第八章笔记,2023.03.08;
       6. 第九章笔记,2023.03.09;
       6. 第十章笔记,2023.03.10;


前言

       指令系统就是计算机软硬件的语言,就像英语、汉语一样。中国人可以用英文写文章,用英文写一本书去卖,可以有版权,可以赚钱,但不可能用英文发展出自己的民族文化来。指令系统也一样,中国企业可以用国外的指令系统,通过授权或者通过合资,做产品卖,但是不可能基于国外的指令系统做自己的生态。       —胡伟武

       俗话说,好记性不如烂笔头,在此记录与分享一下《 汇编语言编程基础 基于 LoongArch 》读书与实验笔记。如文中出现错误,欢迎在评论区留言讨论,我会尽快修改更新 😃


声明

       该书作者是孙国云、敖琪、王锐。博文中会引用书中原文,非商业目的,请从正规渠道购买正版书籍。
       如果转载,请标明转载出处。


准备

1. 文档

       建议读者在阅读此书时,参考以下龙芯官方发布的参考手册:

     (1)龙芯架构参考手册 - 卷一:基础架构

     (2)龙芯架构 ELF psABI 规范

     (3)计算机体系结构基础 第 3 版

2. 环境

     (1)硬件

               主板:江苏航天龙梦信息技术有限公司  A2201 主板( CPU:Loongson-3A5000 BRIDGE:LS7A2000 )

     (2)软件

               系统:Loongnix-20.3.livecd.mate.loongarch64.en.iso


第一章 汇编语言和龙芯架构简介

1.1 计算机语言

1.1.1 机器语言

     (1)计算机呈现给程序员的全部指令的集合称为指令集指令系统。可以说,指令系统是软件和硬件的接口层,我们就是通过这个接口层指导计算机处理器为我们工作的;

     (2)龙芯指令系统中一条指令占用 32位4字节

     (3)一条机器指令长度固定(例如龙芯指令长度为 32 位),由操作码操作数两部分组成,操作数又分为源操作数目的操作数

     (4)一条指令的文本表达与实际存储格式如下表所示:

文本表达助记符目的操作数第二个源操作数第一个源操作数
文本表达ADDI.Drd,rj,si12
实际存储操作码第一个源操作数第二个源操作数目的操作数
实际存储ADDI.Dsi12rjrd

1.1.2 汇编语言

     (1)龙芯指令架构下实现两个数的加法操作,其对应的机器指令和汇编指令如下(机器指令解析请参考龙芯架构参考手册 - 卷一:基础架构 附录B 指令码一览表):

机器指令:0000 0010 1100 0001 0000 0000 0110 0011
汇编指令:addi.d r3, r3, 64

     (2)一条汇编指令通常由助记符操作数两部分组成。助记符对应机器指令中的操作码,例如上面的 addi.d 就是助记符,代表这是一个 64 位加法操作,操作数代表指令的计算对象,例如上面的两个 r3 寄存器和常数 64。

     (3)汇编语言和机器语言一样,都是和计算机体系架构强绑定的低级语言,不具有移植性。

1.1.3 高级语言

     (1)高级语言是一个相对概念,通常可解读为越是易于程序员高效编写的语言越高级。例如刚开始出现 C 语言时,人们认为 C 语言比汇编语言高级,故称 C 语言为高级语言,而汇编语言为低级语言,当 Java、Python 语言出现后,人们认为 C 语言不够高级。

     (2)高级语言设计思想发展的主旨是更便于程序员快速编程。编程思想经历了面向过程(将一个功能块定义为一个函数或方法,如 C 语言)、面向对象(把相关的数据、函数或方法组织为一阶函数,如 Java)、面向函数(即高阶函数的出现,很多语言都在“拥抱”高阶函数,如 Java、Groovy、Scala、JavaScript 等)。未来还可能有 面向应用 的设计思想转变,也就是说:只需要告诉程序你要干什么,程序就能自动生成算法,自动进行处理。高级语言设计思想的不断变化,让计算机语言越来越接近人类语言,也更智能,编程效率也越来越高,使得程序员可把更多时间花费在解决复杂业务场景上。

1.2 汇编语言的使用场景

       学习汇编语言有什么用处?下面列举几个汇编语言的使用场景。

1.2.1 场景1   快速定位和分析问题

     (1)浮点例外,当程序执行除法运算语句时,如果被除数为 0,那么系统就会毫不留情地发送给你一个信号 SIGFPE(中文或许显示 “‘浮点数列外”),以下是实践过程:

(1.1)代码如下:

# include <stdio.h> 
 
int test(int a, int b) { 
    return a/b; 
} 
 
int main() { 
    test(1, 0); 
    return 0; 
}

(1.2)运行结果:

$ gcc test.c -o test
$ ls
test  test.c
$ ./test 
浮点数例外

(1.3)调试过程:

$ sudo apt install gdb -y
$ ls
test  test.c
$ gdb test 
GNU gdb (Loongnix 8.1.50-1.lnd.vec.6) 8.1.50.20190122-git
(gdb) run
Starting program: /home/loongson/asm_loongarch/ch1/1.2.1/test 

Program received signal SIGFPE, Arithmetic exception.
0x0000000120000704 in test ()
(gdb) bt
#0  0x0000000120000704 in test ()
#1  0x0000000120000738 in main ()
(gdb) x/5i $pc-12
   0x1200006f8 <test+40>:       ld.w    $r12,$r22,-24(0xfe8)
   0x1200006fc <test+44>:       div.w   $r14,$r13,$r12
   0x120000700 <test+48>:       bne     $r12,$r0,8(0x8) # 0x120000708 <test+56>
=> 0x120000704 <test+52>:       break   0x7
   0x120000708 <test+56>:       move    $r12,$r14
(gdb)

       上述第 18 行代码,break 0x7,无条件触发断点异常,参数 0x7 对应SIGFPE;

(1.4)用到的 gdb 命令记录

gdb bt(backtrace) 指令,打印当前的函数调用栈的所有信息。
gdb x(examine)/<n/f/u>  <addr>

<n>:
是正整数,表示需要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,
一个内存单元的大小由第三个参数u定义。

<f>:
表示addr指向的内存内容的输出格式,s对应输出字符串,此处需特别注意输出整型数据的格式:
  x 按十六进制格式显示变量.
  d 按十进制格式显示变量。
  u 按十进制格式显示无符号整型。
  o 按八进制格式显示变量。
  t 按二进制格式显示变量。
  a 按十六进制格式显示变量。
  i 按指令地址格式显示变量。
  c 按字符格式显示变量。
  f 按浮点数格式显示变量。

<u>:
就是指以多少个字节作为一个内存单元-unit,默认为4。u还可以用被一些字符表示:
  如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes.

<addr>:表示内存地址。

1.2.2 场景2   性能分析和优化

     (1)了解计算机体系架构和汇编语言有助于我们深入分析软件性能瓶颈。

     (2)多数处理器中都实现了单指令流多数据流( Single-Instruction stream Multiple-Data stream, SIMD )功能的汇编指令,亦称向量指令,其可实现一条指令操作多组数据。LoongArch 包括向量扩展( Loongson SIMD Extension,LSX )和高级向量扩展( Loongson Advanced SIMD Extension,LASX ),LSX 128 位LASX 256 位向量位宽。

       以下是使用LoongArch LASX 优化的例子:

(2.1)C语言代码如下

for (int i = 0; i < 10000; i++)
	c[i] = a[i] + b[i];

(2.2)优化前汇编代码如下

//LoongArch 汇编指令
L:
	ld.w	t1, a1, 0	# 加载数组 a[i] 值到寄存器 t1
	ld.w	t2, a2, 0	# 加载数组 b[i] 值到寄存器 t2, 注意:原书中没有这一行代码
	add.w	t3, t2, t1	# 实现 a[i] + b[i], 将结果存入寄存器 t3
	st.w	t3, t4, 0	# t3 数据写回 c[i], t4 寄存器的值指向 c[i]
	addi.d	a1, a1, 4	# 数组 a[] + 4, 即指向 a[i + 1]
	addi.d	a2, a2, 4	# 数组 b[] + 4, 注意:原书中此处是 addi.d	a1, a2, 4
	addi.d	t4, t4, 4	# 数组 b[] + 4

	bne a5, a6, L		# 判断若 for () 没有结束,跳转到L,继续执行

(2.3)优化后汇编代码如下:

//LoongArch 汇编指令
L:
	xvld	x1, a1, 0	# 加载数组 a[] 中的 8 组整形值到向量寄存器 x1
	xvld	x2, a2, 0	# 加载数组 b[] 中的 8 组整形值到向量寄存器 x2
	xvadd.w	x3, x1, x2	# 实现 a[i...i+8] + b[i...i+8], 将结果存入寄存器 x3
	xvst	x3, t4, 0	# x3 数据写回 c[i...i+8], t4 寄存器的值指向 c[i]
	addi.d	a1, a1, 32	# 数组 a[] + 32, 即指向 a[i + 9]
	addi.d	a2, a2, 32	# 数组 b[] + 32, 注意:原书中此处是 addi.d	a1, a2, 32
	addi.d	t4, t4, 32	# 数组 b[] + 32

	bne a5, a6, L		# 判断若 for () 没有结束,跳转到L,继续执行

       龙芯 LASX 指令是 256 位宽(即向量寄存器的长度),故循环一次可以完成 8 组整型值(8 x 32位)的加法运算。优化前是优化后性能的 8 倍。

1.2.3 场景3   完成高级语言无法实现的功能

     (1)在一些基础软件的源代码中,比如数据库、GCC 编译器、OpenJDK 等,我们能频繁看到汇编语言的身影。因为它们作为应用软件的支撑或工具,相对于应用软件在运行逻辑上更靠近CPU,也就更可能出现和计算机体系架构相关的功能需求。

     (2)在 C 语言中如何获取程序运行的当前 PC 值?

(2.1)代码如下

static long *get_PC() {
    unsigned long *val;
    __asm__ volatile ("move %0, $r1" : "=r"(val));
    return val;
}

int main() {
    unsigned long *val;

    val = get_PC();
    printf("PC is 0x%llx\n", (unsigned long)val);
    return 0;
}

(2.2)运行结果:

$ ./test 
PC is 0x12000076c

(2.3)汇编如下

loongson@A2201-devel:~/asm_loongarch/ch1/1.2.3$ objdump -d test 
0000000120000730 <get_PC>:                            
   120000730:   02ff8063        addi.d  $r3,$r3,-32(0xfe0)
   120000734:   29c06076        st.d    $r22,$r3,24(0x18) 
   120000738:   02c08076        addi.d  $r22,$r3,32(0x20)
   12000073c:   0015002c        move    $r12,$r1 # 将返回地址存储到临时寄存器 r12 中         
   120000740:   29ffa2cc        st.d    $r12,$r22,-24(0xfe8)
   120000744:   28ffa2cc        ld.d    $r12,$r22,-24(0xfe8)
   120000748:   00150184        move    $r4,$r12 # 将返回地址存储到返回值寄存器 r4 中  
   12000074c:   28c06076        ld.d    $r22,$r3,24(0x18)                                 
   120000750:   02c08063        addi.d  $r3,$r3,32(0x20)   
   120000754:   4c000020        jirl    $r0,$r1,0             
                                                                                           
0000000120000758 <main>:                               
   120000758:   02ff8063        addi.d  $r3,$r3,-32(0xfe0)             
   12000075c:   29c06061        st.d    $r1,$r3,24(0x18)
   120000760:   29c04076        st.d    $r22,$r3,16(0x10)
   120000764:   02c08076        addi.d  $r22,$r3,32(0x20)      
   120000768:   57ffcbff        bl      -56(0xfffffc8) # 120000730 <get_PC> # bl 无条件跳转到 get_PC 函数处,并将函数返回地址( 当前 PC + 4 )存储到返回地址寄存器 r1 中,也就是0x12000076c
   12000076c:   29ffa2c4        st.d    $r4,$r22,-24(0xfe8)
   120000770:   28ffa2cc        ld.d    $r12,$r22,-24(0xfe8)
   120000774:   00150185        move    $r5,$r12    
   120000778:   1c000004        pcaddu12i       $r4,0
   12000077c:   02c30084        addi.d  $r4,$r4,192(0xc0) 
   120000780:   57fdf3ff        bl      -528(0xffffdf0) # 120000570 <printf@plt>
   120000784:   0015000c        move    $r12,$r0           
   120000788:   00150184        move    $r4,$r12        
   12000078c:   28c06061        ld.d    $r1,$r3,24(0x18)      
   120000790:   28c04076        ld.d    $r22,$r3,16(0x10)   
   120000794:   02c08063        addi.d  $r3,$r3,32(0x20)      
   120000798:   4c000020        jirl    $r0,$r1,0 

       上述汇编代码第 6 行中的 move 指令,龙芯官方手册中并不存在,但 move 指令的操作码与 or 指令操作码相同。于是 move rd, rj <=> or rd, rj, rk ,所以 move $r12, $r1 <=> or $r12, $r1, $r0

1.3 龙芯系列处理器和龙芯架构介绍

1.3.1 龙芯系列处理器

     (1)龙芯系列处理器

1.3.2 龙芯自主指令系统

     (1)龙芯架构参考手册 - 卷一:基础架构

1.4 龙芯汇编语言程序编写示例

(1)汇编如下:

# file: add.S
# interface: int add_f(int a, int b, int c, int d)
# function: return (a+b+c+d)

        .text
        .align 2
        .globl add_f
        .type add_f,@function
add_f:
        add.w   $a0, $a0, $a1
        add.w   $a0, $a0, $a2
        add.w   $a0, $a0, $a3
        jr              $ra
        .size   add_f, .-add_f

(2)C语言代码如下:

#include <stdio.h>

extern int add_f(int a, int b, int c, int d); // 外部函数引用

int main() {

        int ret = add_f(1, 2, 3, 4); // 调用 add.S 中的汇编函数 add_f

        printf("ret = %d\n", ret); // 输出结果

        return 0;
}

(3)编译命令如下

$ gcc test.c add.S -o test_add

(4)运行结果如下:

$ ./test_add 
ret = 10

1.5 TODO

     (1)为什么会有 break 指令?
     (2)break 指令运行后会发生什么?
     (3)系统是如何实现 SIGFPE 的?


第二章 一窥 LoongArch 指令风貌

2.1 LoongArch 指令特性

     (1)LoongArch 基础指令集具有 RISC 指令架构的典型特征。 RISC 的核心思想就是简单化:指令功能简单,所以 CPU 执行完一条指令的周期短;抛弃变长指令编码格式,统一使用定长指令,CPU 译码比较简单,符合“常用的做得快、少用的只要对”的原则;采用 Load-Store 结构,只有访存指令能够访问内存其他指令的操作对象均是寄存器或立即数。 精简的优势不仅有利于硬件的高效实现,还有利于通过流水线、多发射、乱序执行等技术来提高效率。这样的优势让非 RISC 架构的代表 x86 架构也不断趋于 RISC 风格,把比较复杂的指令预译码成很多简单的操作,以便更有效地使用流水线等手段。

2.1.1 指令组成和指令分类

     (1)组成:
       一般来说,一条指令包含操作码操作数
       操作码:定义了指令功能,例如加、减功能等;
       操作数:定义了要完成指定功能所需要的对象,例如寄存器、常数、地址等;

     (2)分类:
       龙芯基础指令集包含约 300 条指令,按指令功能可分为运算指令访存指令转移指令特殊指令四大类。
       运算指令:实现加、减、乘、除、移位、逻辑与、逻辑或、逻辑非等运算功能;
       访存指令:实现处理器从内存读取寄存器或从寄存器数据到内存的操作功能;
       转移指令:实现控制程序执行的流向的功能,在机理上类似 C 语言中的 if-else、switch、goto 语句;
       特殊指令:实现操作系统的特殊用途的功能,例如系统调用获取处理器特征触发断点例外等;

2.1.2 寄存器

     (1)32 个整数通用寄存器( General-purpose Register,GR ),$r0 ~ $r31;
     (2)32 个浮寄存器( Floatingl-point Register,FR ), $f0 ~ $f31;

架构LA64LA32
GR 寄存器64 bit32 bit
FR 寄存器64 bit64 bit

     (3)LA32 是 LA64 的子集。例如加法指令,在 LA32 架构上仅有 32 位加法指令,而在 LA64 架构上既有 32 位又有 64 位加法指令。

     (4)LA64 的 32 位加法指令,CPU 只取寄存器的低 32 位,再将运算结果的低 32 位经过符号扩展到 64 位后写入目的寄存器中。

     (5)仅当指令中操作单精度浮点数和字整数( 32 位整型)时,浮点寄存器的位宽为 32 位,即数据只在浮点寄存器的 [31:0] 位上,而 [63:32] 位是无效值。

     (6)与浮点数指令编程相关的还有两类寄存器:条件标志寄存器( Condition Flag Register, CFR )和浮点控制状态寄存器( Float-point Control and Status Register, FCSR ),分别用于存放浮点比较的结果和存放浮点运算非法操作、除零、溢出等异常状态。

2.1.3 指令长度和编码格式

     (1)龙芯架构参考手册 - 卷一:基础架构 1.2 指令编码格式

2.1.4 指令汇编助记格式

       汇编助记格式 = 文本指令名 + 操作数, 例如 vadd.b v8, v1, v2
       文本指令名 = 指令名前缀 + 指令名 + (i) + 指令名后缀
       指令名前缀 :区分指令名前缀 、整数指令和浮点数指令;
       指令名:指令功能的英文单词或英文单词缩写;
       指令名后缀:区分指令操作对象的类型;
       (i) : 操作数中有立即数的,例如,addi.w r8, r1, 16;

指令种类文本指令名指令名前缀指令名指令名后缀寄存器名
向量指令vadd.b、vxadd.wv <=> LSX
xv <=> LASX
add,sub,等.b、.h、.w、.d、(有符号)
.bu、.hu、.wu、.du(无符号)
vN、xN
整数指令add.wadd,sub,等.b、.h、.w、.d、(有符号)
.bu、.hu、.wu、.du(无符号)
rN
浮点数指令fadd.sfadd,sub,等.s(单精度浮点)
.d(双精度浮点)
fN

2.1.5 符号扩展

     (1)所有计算指令中的立即数都需要进行符号扩展或无符号扩展(零扩展)参与计算

符号扩展种类描述0x8000 ( 16 位立即数 )0x1000 ( 16 位立即数 )
32 位符号扩展将 n ( n < 32 ) 位立即数的高 32 - n 位填充为立即数的最高位0xFFFF80000x00001000
32 位无符号(零)扩展将 n ( n < 32 ) 位立即数的高 32 - n 位填充 00x000080000x00001000
64 位符号扩展将 n ( n < 64 ) 位立即数的高 64 - n 位填充为立即数的最高位0xFFFFFFFFFFFF80000x0000000000001000
64 位无符号(零)扩展将 n ( n < 64 ) 位立即数的高 64 - n 位填充 00x00000000000080000x0000000000001000

     (2)LA64 架构下的 32 位操作数计算指令中,计算结果通常也需要先进行 64 位符号扩展再写入目的寄存器。例如:

addi.w rd, rj, si12

tmp = GR[rj][31:0] + SignExtend(si12, 32) # 取 rj 低 32 位加上 si12 32 位符号扩展
GR[rd] = SignExtend(tmp[31:0], GRLEN) # GRLEN = 64,将结果 tmp 64 位符号扩展后,存入 rd

2.1.6 寻址方式

     (1)所谓寻址方式,就是 CPU 寻找操作数的存储地址的方式。例如,寄存器寻址,操作数在寄存器中,操作数的地址 = 寄存器的地址。

       符号 # 代表立即数,数组 regs[ ] 表示寄存器,数组 mem[ ] 表示存储器。

寻址方式指令示例格式说明寻址方式描述
寄存器寻址add r1, r1, r2regs[r1] = regs[1] + regs[2]操作数在寄存器中,
操作数的地址 = 寄存器的地址
立即数寻址add r1, r1, #2regs[r1] = regs[1] + 2操作数在指令中,
操作数的地址 = 立即数的地址
由于指令大小固定( 32 位),立即数大小会有限制,一般 12 位
基址 + 立即数偏移寻址ld r1, r2, #100regs[r1] = mem[ regs[r2] + 100 ]操作数在存储器中,
操作数的地址 = 基地址 + 偏移,
基地址在寄存器中,偏移是立即数,在指令中
基址 + 寄存器偏移寻址ld r1, r2, r3regs[r1] = mem[ regs[r2] + regs[r3] ]操作数在存储器中,
操作数的地址 = 基地址 + 偏移,
基地址在寄存器中,偏移也在寄存器中
相对寻址bl #100PC = mem[ PC + 100] ]操作数在存储器中,
是基址 + 立即数偏移寻址的一种特例,基地址是PC寄存器

2.2 C 语言到 LoongArch 的编译过程

     (1)编译流程包括词法分析、语法分析、语义分析、中间代码生成、汇编指令生成、目标机器指令生成、链接等阶段。

(1.1)C语言代码如下:

/* This is my test file. */
#include <stdio.h>
#define STR	"Hello World!"

int main() {
	printf("%s\n", STR);
	return 0;
}

(1.2)编译命令如下

$ gcc -v --save-temps hello.c -o hello

gcc version 8.3.0 (Loongnix 8.3.0-6.lnd.vec.34) 

cc1 -E -quiet -v hello.c -mabi=lp64d -o hello.i

as -v -mabi=lp64 -o hello.o hello.s

collect2  -o hello crt1.o crti.o crtbegin.o hello.o crtend.o crtn.o

$ ls
hello  hello.c  hello.i  hello.o  hello.s

       -v : 显示编译器具体的编译过程;
       --save-temps : 保留编译过程中的临时中间文件,hello.i hello.o hello.s;
       -o : 指定目标文件的名字,不指定默认为 a.out

       cc1 是编译器,首先预处理,生成.i,然后生成汇编源文件,生成.s;
       as 是汇编器,将汇编源文件生成包含机器指令的目标文件,生成.o;
       collect2 是链接器,将多个目标文件链接成最终的目标文件;

+-------------------------------------+
| +-----------+         +-----------+ |        +-----------+        +-----------+            +-----------+  
| |  hello.c  |   cc1   |           | |  cc1   |           |   as   |           |  collect2  |           |
| |  stdio.h  |-------->|  hello.i  |-+------->|  hello.s  |------->|  hello.o  |----------->|   hello   |      
| |   ...     |         |           | |   ^    |           |        |           |     ^      |           |
| +-----------+         +-----------+ |   |    +-----------+        +-----------+     |      +-----------+
+-------------------------------------+   |                                           |
                                          |                                           |
                                          |                                     +-----+-----+ 
                                          |                                     |   crtl.o  |
                                          |                                     |   crti.o  |
                                          |                                     | crtbegin.o|
                                          |                                     | crtend.o  |
                                          |                                     |   crtn.o  |
			+-----------+                 |                                     +-----------+
			|           |                 |
			|  hello.S  |-----------------+
			|           |                
			+-----------+

2.2.1 预处理和编译阶段

     (1)预处理阶段
       主要是对预处理命令( .c 文件中以 # 开头的语句)进行处理,包括头文件包含、宏定义展开、条件编译的选择、删除注释、添加编译调试信息等,最终产生 .i 文件。预处理的主要规则如下:

     (1.1)处理所有以 # 开头的语句
       将 #include 语句中的文件的绝对路径替换掉 #include 语句;
       将 #define 宏定义扩展后,删除宏定义;
       保留 #if、#else、#endif 宏判断语句条件成立的部分,其他删除;

     (1.2)删除源文件中的所有注释

     (1.3)为调试添加行号和文件名标识
       编译阶段产生调试用的行号信息及产生错误或警告时显示行号;

     (2)编译阶段
       编译阶段对 .i 中的内容进行语法分析、语义分析、汇编代码生成、代码优化

     (2.1)语法分析的主要任务是检查源程序是否符合程序设计语言的语法规则,比如括号是否匹配、语句是否以“ ; ”结束等;
     (2.2)语义分析的主要任务是类型检查,即确认每个运算符是否使用恰当,例如数据索引是否是不合法的小数、函数调用时参数类型是否不匹配、局部变量是否有重复定义的情况等;

     (2.3)汇编代码生成的工作是将前面语法、语义分析后产生的中间表示翻译成计算机体系架构相关的汇编代码,并输出到 .s 文件。
     (2.4)代码优化手段有方法内联( Inlining )、循环展开( Unrolling )、死代码消除( DeadCode Elimination )等。GCC 提供 -O0(默认)、-O1、-O2、-O3 优化选项,数字越大、优化程度越高,但编译时间越长。循环展开是指编译器在编译过程中,多次复制循环体内部指令、使循环次数减少或消除,以此降低循环分支指令带来的性能开销,死代码消除是指,识别并删除那些永远不会被执行的代码。

     (3)如果仅想生成汇编源代码文件,编译使用 -S

$ gcc -S hello.c -o hello.s

2.2.2 机器指令生成阶段

     (1)机器指令生成阶段使用的工具叫汇编器( as ),主要任务是解析源文件,并将内部的汇编语句按照指令码表编码成处理器可识别的机器指令,生成目标文件。

$ objdump -d hello.o | vim -
0000000000000000 <main>:
   0:	02ffc063 	addi.d	$r3,$r3,-16(0xff0)
   4:	29c02061 	st.d	$r1,$r3,8(0x8)
   8:	29c00076 	st.d	$r22,$r3,0
   c:	02c04076 	addi.d	$r22,$r3,16(0x10)
  10:	1c000004 	pcaddu12i	$r4,0
  14:	02c00084 	addi.d	$r4,$r4,0
  18:	54000000 	bl	0 # 18 <main+0x18>
  1c:	0015000c 	move	$r12,$r0
  20:	00150184 	move	$r4,$r12
  24:	28c02061 	ld.d	$r1,$r3,8(0x8)
  28:	28c00076 	ld.d	$r22,$r3,0
  2c:	02c04063 	addi.d	$r3,$r3,16(0x10)
  30:	4c000020 	jirl	$r0,$r1,0

       此阶段每个函数的起始地址都为 0 ,待后面链接阶段才能确定最终在内存中的位置。

     (2)生成目标文件

$ as hello.s -o hello.o
$ gcc -c hello.c -o hello.o

2.2.3 链接阶段

     (1)链接阶段使用的工具是 collect2 ( collect2 是链接器 ld 的封装 )。这个阶段的主要工作就是将机器指令生成阶段的多个 .o 文件正确地链接起来形成一个文件。对于不能链接进来的动态库(如 libc.so ),也要在引用它的位置计算好地址,以便程序运行时可以正确找到并动态加载它。

     (2)链接阶段有两个核心的工作符号解析重定位
符号包括函数名和变量名,每个待链接的目标文件都有一个符号表,表内包含当前文件中定义和使用到的所有符号,可以用以下命令查看。

$ objdump -t hello.o 

hello.o:     文件格式 elf64-loongarch

SYMBOL TABLE:
0000000000000000 l    df *ABS*	0000000000000000 hello.c
0000000000000000 l    d  .text	0000000000000000 .text
0000000000000000 l    d  .data	0000000000000000 .data
0000000000000000 l    d  .bss	0000000000000000 .bss
0000000000000000 l    d  .rodata	0000000000000000 .rodata
0000000000000000 l       .text	0000000000000000 L0 
0000000000000000 l    d  .note.GNU-stack	0000000000000000 .note.GNU-stack
0000000000000000 l       .rodata	0000000000000000 .LC0
0000000000000000 l    d  .comment	0000000000000000 .comment
0000000000000000 l    d  .eh_frame	0000000000000000 .eh_frame
0000000000000000 g     F .text	0000000000000034 main
0000000000000000         *UND*	0000000000000000 puts

       符号解析会对每个符号表中的每个符号定义和符号的引用确定关联,并形成一个全局符号表。重定位就是负责把所有输入文件中的信息重新排列,并根据全局符号表来重新计算里面符号的最终位置。

0000000120000570 <puts@plt>:
   120000570:	1c00010f 	pcaddu12i	$r15,8(0x8)
   120000574:	28ea81ef 	ld.d	$r15,$r15,-1376(0xaa0)
   120000578:	1c00000d 	pcaddu12i	$r13,0
   12000057c:	4c0001e0 	jirl	$r0,$r15,0

0000000120000730 <main>:
   120000730:	02ffc063 	addi.d	$r3,$r3,-16(0xff0)
   120000734:	29c02061 	st.d	$r1,$r3,8(0x8)
   120000738:	29c00076 	st.d	$r22,$r3,0
   12000073c:	02c04076 	addi.d	$r22,$r3,16(0x10)
   120000740:	1c000004 	pcaddu12i	$r4,0
   120000744:	02c30084 	addi.d	$r4,$r4,192(0xc0)
   120000748:	57fe2bff 	bl	-472(0xffffe28) # 120000570 <puts@plt>
   12000074c:	0015000c 	move	$r12,$r0
   120000750:	00150184 	move	$r4,$r12
   120000754:	28c02061 	ld.d	$r1,$r3,8(0x8)
   120000758:	28c00076 	ld.d	$r22,$r3,0
   12000075c:	02c04063 	addi.d	$r3,$r3,16(0x10)
   120000760:	4c000020 	jirl	$r0,$r1,0

       main 函数的起始地址由之前的 0 变为 0x120000730。这个地址是 hello 程序运行时的有效虚拟地址。而且经过重定位后,puts 函数已确定。

2.3 TODO

     (1)符号解析的详细过程是什么?全局符号表是什么?
     (2)重定位的详细过程是什么?
     (3)main 函数地址为什么与书中的不一样?而在实验系统中其他程序的 main 地址又是一样的?为啥每个程序都有个 main?
     (4)书中 printf 函数,为什么实验程序中是 puts 函数?printf("%s\n") 等价于 puts(),puts() 是 printf() 的一种特例。


第三章 LoongArch 基础整数指令集

       权限角度:非特权指令和特权指令
       数据类型角度:基础整数指令和基础浮点数指令
       功能角度:运算指令(加减乘除、移位、逻辑运算)、访存指令(负责向内存或者 Cache 等存储器取数或存数)、转移指令(用于控制程序执行流向)、其他杂项指令(无法归类的指令和给 OS 用的)


                                                             运算指令      
                                        +--- 基础整数指令 <---访存指令      
                                        |                    转移指令      
                                        |                    其他杂项指令                                                                    
                                        |                                     
                     +-- 非特权指令 <---|                                        
                     |                  |                                                                                                           
                     |                  |                                    
                     |                  |                    运算指令      
                     |                  +--- 基础浮点数指 <---访存指令      
                     |                                       ...           
  龙芯基础指令集 <---|                                                                                                     
                     |                                         
                     |                                         
                     |                                         
                     |                                         
                     |                CSR访问指令     
                     +-- 特权指令 <--- Cache维护指令
                                      TLB维护指令    
                                      ...            
                                                 

3.1 运算指令

       运算指令(使用的频率最高的指令)包括:
     (1)算术运算(加、减、乘、除)
     (2)逻辑运算(与、或、或非、异或等)
     (3)条件赋值
     (4)移位运算(逻辑左移、逻辑右移、循环移位等)
     (5)位操作(位提取、位替换、半字逆序等)

3.1.1 算术运算指令

       算术运算指令:加、减、乘、除、取余数、立即数加载、带移位加法运算

       龙芯架构参考手册 - 卷一:基础架构 2.2.1 算术运算类指令

指令格式 功能简述
 add.w       rd,rj,rk32 / 64 位数据加减法,rd = rj +/- rk
 add.d       rd,rj,rk
 sub.w       rd,rj,rk
 sub.d       rd,rj,rk
 addi.w      rd,rj,si12带立即数的 32 / 64 位加法,rd = rj + si12
 addi.d      rd,rj,si12
 addu16i.d      rd,rj,si16带立即数的 64 位加法,rd = rj + (si16 << 16)
 alsl.w     rd,rj,rk,sa232 / 64 位带移位加法,rd = rk + rj << (sa2 + 1),  sa2 [0, 3]   
 alsl.d     rd,rj,rk,sa2
 alsl.wu     rd,rj,rk,sa2
 lu12i.w      rd,si20立即数加载,rd = SignExtend(si20 << 12)
 lu32i.d      rd,si20 立即数加载,rd = SignExtend(si20 << 32 | rd[31 : 0])
 lu52i.d      rd,rj,si12 立即数加载,rd = (si12 << 52) | rj[51 : 0],注意: 书中是 (si12 << 51)
 mul.w      rd,rj,rk32 / 64 位乘法,rd = rj * rk  
 mul.d      rd,rj,rk
 mulh.w      rd,rj,rk32 / 64 位乘法,取结果的高 32 / 64 位    
 mulh.wu      rd,rj,rk
 mulh.d      rd,rj,rk
 mulh.du      rd,rj,rk
 mulw.d.w      rd,rj,rk保留溢出乘法,rd = rj[31:0] * rk[31:0] 
 mulw.d.wu      rd,rj,rk
 div.w      rd,rj,rk32 / 64 位除法,rd = rj / rk
 div.wu      rd,rj,rk
 div.d      rd,rj,rk
 div.du      rd,rj,rk
 mod.w      rd,rj,rk 32 / 64 位取余数,rd = rj % rk
 mod.wu      rd,rj,rk
 mod.d      rd,rj,rk
 mod.du      rd,rj,rk

     (1)LA64 架构下 add.w 运算结果需要符号扩展后写入目的寄存器。

add.w	r5, r2, r1		# LA32: r5 = r2 +1
add.w	r5, r2, r1		# LA64: r5[63:0] = SignExtend( r2[31:0] + r1[31:0] ) 

     (2)溢出问题

       寄存器的最高位为符号位。
       LA32:[ - 231 + 1,231 - 1] <=> [ 0x80000000 ,0x7FFFFFFF ]
       LA64:[ - 263 + 1,263 - 1] <=> [ 0x800000000000000 ,0x7FFFFFFFFFFFFFF ]

add.w	r5, r1, r1		# LA32: 假如 r1 = 0x7fffffff r5 = 0xfffffffe ( -2 )
add.w	r5, r1, r1		# LA62: 假如 r1 = 0x7fffffff r5 = 0xfffffffffffffffe ( -2 )

        ARM 有专门的程序状态寄存器( Current Program Status Register,CPSR )用于保存包括进位、正负、溢出标志在内的一些运算结果状态信息。而 LoongArch 没有,但可以通过目的寄存器和源寄存器的符号位是否一致来判断。

     (3)立即数( imm )的 32 位加法运算
     (3.1)imm < 12 bit

addi.w	r5, r2, imm[11:0] # addi.w rd, rj, si12

     (3.2)12 bit < imm < 32 bit

lu12i.w	r1, imm[31:12]
ori		r1, r1, imm[11:0]
add.w	r5, r2, r1

     (4)立即数( imm )的加载
     (4.1)imm < 12 bit

addi.w	rd, r0, imm[11:0] # LA32
addi.d	rd, r0, imm[11:0] # LA64

     (4.2)12 bit < imm < 32 bit

lu12i.w	rd, 	imm[31:12]
ori		rd, rd, imm[11:0]

     (4.3)32 bit < imm < 52 bit

lu12i.w	rd, 	imm[31:12]
ori		rd, rd, imm[11:0]
lu32i.d	rd, 	imm[51:32]

     (4.4)52 bit < imm

lu12i.w	rd, 	imm[31:12]
ori		rd, rd, imm[11:0]
lu32i.d	rd,		imm[51:32]
lu52i.d	rd, rd, imm[63:52]

     (4.5)汇编器支持立即数加载伪指令(根据上述情况展开):

li.w	rd,	imm32
li.d	rd,	imm64

     (5)带移位的 64 位数加法运算

alsl.d	rd, rj, rk, sa2	# rd = rj << (sa2 + 1 ) + rk, sa2 取[0, 3]
alsl.d	r5, r4, r5, 3	# r5[63:0] = ( r4[63:0] << (3+1) ) + r5[63:0]

3.1.2 逻辑运算和条件赋值指令

 指令格式 功能简述
 and       rd,rj,rk 逻辑与,rd = rj & rk
 or       rd,rj,rk 逻辑或,rd = rj | rk
 nor       rd,rj,rk 逻辑或非,rd = ~ (rj | rk)
 xor       rd,rj,rk 逻辑异或,rd = rj ^ rk
 andn       rd,rj,rk 取反逻辑与,rd = rj & (~ rk)
 orn       rd,rj,rk 取反逻辑或,rd = rj | (~ rk)
 andi       rd,rj,ui12 带立即数的逻辑与,rd = rj & ui12
 ori       rd,rj,ui12 带立即数的逻辑或,rd = rj | ui12
 xori       rd,rj,ui12 带立即数的逻辑异或,rd = rj ^ ui12
 slt       rd,rj,rk 条件赋值,rd = (rj < rk) ? 1 : 0
 sltu       rd,rj,rk
 slti       rd,rj,si12 带立即数的条件赋值,rd = (rj < si12) ? 1 : 0
 sltui       rd,rj,si12
 maskeqz       rd,rj,rk 条件赋值,rd = (rk == 0) ? 0 : rj
 masknez       rd,rj,rk 条件赋值,rd = (rk != 0) ? 0 : rj
 nop 空指令

     (1)下面的代码是取两个数的最小值, a = (b < c) ? b : c ;

if r6 == a, r4 == b, r5 == c

slt		r12, r4, r5 	# r12 = (r4 < r5) ? 1 : 0, 		tmp = (b < c) ? 1 : 0
maskeqz	r4,  r4, r12	# r4 = (r12 == 0) ? 0 : r4, 	b = (tmp == 0) ? 0 : b
masknez r12, r5, r12	# r12 = (r12 != 0) ? 0 : r5, 	tmp = (tmp != 0) ? 0 : c
or		r6,  r4, r12	# r6 = r4 | r12, 				a = b | tmp

     (1.1)书中上面的代码是错误的,取得是最小值,取最大值如下,a = (b > c) ? b : c

if r6 == a, r4 == b, r5 == c

slt		r12, r4, r5 	# r12 = (r4 < r5) ? 1 : 0, 		tmp = (b < c) ? 1 : 0
maskeqz	r5,  r5, r12	# r5 = (r12 == 0) ? 0 : r5, 	c = (tmp == 0) ? 0 : c
masknez r12, r4, r12	# r12 = (r12 != 0) ? 0 : r4, 	tmp = (tmp != 0) ? 0 : b
or		r6,  r5, r12	# r6 = r5 | r12, 				a = c | tmp

     (2)带立即数的逻辑与运算

if r2 == 0x7f0

andi	r5, r2, 3 # r5 = 0

       使用 andi 可以高效判断一个地址是否满足特定对齐要求。例如判断一个地址是否满足 8 字节对齐,只需判断这个地址与 0x7 做逻辑与运算的结果是否为 0。

     (3)空指令 nop 的使用

nop		# andi r0, r0, 0

       看上去没有什么功能实现,仅为占据 4 字节的指令码位置并将 PC 加 4,但是 nop 指令却有实际的使用意义,例如编译器中常用 nop 指令来保证内存对齐。在龙芯平台,为了提高程序运行效率,通常要求一个函数的起始地址是 16 字节对齐(即可以被 16 整除),故当编译器发现一个函数中最后一条指令(通常为一条跳转指令)所在地址不是 16 字节对齐,那么就会在最后一条指令的后面添加 1 ~ 3 条 nop 指令,来确保下一个函数的起始地址是 16 字节对齐的。

3.1.3 移位运算指令

       逻辑左移:将源操作数向高位移动 N 位,高 N 位丢失,低 N 位填 0 ;
       逻辑右移:将源操作数向低位移动 N 位,低 N 位丢失,高 N 位填 0 ;
       算术右移:将源操作数向高位移动 N 位,高 N 位丢失,高 N 位填符号位;
       循环右移:将移位的低 N 位放置在高 N 位,高 N 位放到低 N位,这里的 N 的取值范围依具体指令而定;

 指令格式 功能简述
 sll.w       rd,rj,rk 逻辑左移,rd = rj << rk[4 : 0]
 sll.d       rd,rj,rk 逻辑左移,rd = rj << rk[5 : 0]
 slli.w       rd,rj,ui5 带立即数的逻辑左移,rd = rj << ui5
 slli.d       rd,rj,ui6 带立即数的逻辑左移,rd = rj << ui6
 srl.w       rd,rj,rk 逻辑右移,rd = rj >> rk[4 : 0]
 srl.d       rd,rj,rk 逻辑右移,rd = rj >> rk[5 : 0]
 slri.w       rd,rj,ui5 带立即数的逻辑右移,rd = rj >> ui5
 slri.d       rd,rj,ui6 带立即数的逻辑右移,rd = rj >> ui6
 sra.w       rd,rj,rk 算术右移,rd = rj >> rk[4 : 0]
 sra.d       rd,rj,rk 算术右移,rd = rj >> rk[5 : 0]
 srai.w       rd,rj,ui5 带立即数的算术右移,rd = rj >> ui5
 srai.d       rd,rj,ui6 带立即数的算术右移,rd = rj >> ui6
 rotr.w       rd,rj,rk 循环右移
 rotr.d       rd,rj,rk
 rotri.w       rd,rj,ui5 带立即数的循环右移
 rotri.d       rd,rj,ui6

3.1.4 位操作指令

       位操作:高位符号扩展、按条件计数、指定位数的数据拼接和提取、按条件逆序、指定位置的替换等。
       位操作指令仅在 LA64 架构下被支持。

 指令格式 功能简述
 ext.w.b       rd,rj 符号扩展,rd = SignExtend( rj [7 : 0] )
 ext.w.h       rd,rj 符号扩展,rd = SignExtend( rj [15 : 0] )
 clo.w       rd,rj 计量 rj [31 : 0](从高到低位)中连续 1 的数量
 clz.w       rd,rj 计量 rj [31 : 0](从高到低位)中连续 0 的数量
 cto.w       rd,rj 计量 rj [31 : 0](从低到高位)中连续 1 的数量
 ctz.w       rd,rj 计量 rj [31 : 0](从低到高位)中连续 0 的数量
 clo.d       rd,rj 计量 rj [63 : 0](从高到低位)中连续 1 的数量
 clz.d       rd,rj 计量 rj [63 : 0](从高到低位)中连续 0 的数量
 cto.d       rd,rj 计量 rj [63 : 0](从低到高位)中连续 1 的数量
 ctz.d       rd,rj 计量 rj [63 : 0](从低到高位)中连续 0 的数量
 bytepick.w       rd,rj,rk,sa2 左右拼接 rk[31 : 0] 和 rj[31 : 0] 成一个 64 位数,再从左侧第 sa2 位开始截取 32 位,将所得 32 位数进行符号扩展再写入 rd
 bytepick.d       rd,rj,rk,sa3 左右拼接 rk[63 : 0] 和 rj[63 : 0] 成一个 128 位数,再从左侧第 sa3 位开始截取 64 位写入 rd
 revb.2h       rd,rj

 将 32 位数以半字为组按字节逆序 

 将 rj[15 : 0] 中的 2 字节逆序,将rj[31 : 16] 中的 2 字节逆序,将结果进行符号扩展存入 rd

 revb.4h       rd,rj

 将 64 位数以半字为组按字节逆序 

 revb.2w       rd,rj

 将 64 位数以字为组按字节逆序 

 将 rj[31 : 0] 中的 4 字节逆序,将 rj[63 : 32] 中的 4 字节逆序,将结果写入 rd

 revb.d       rd,rj

 将 64 位数以双字为组按字节逆序 

 将 rj[63 : 0] 中的 8 字节逆序排列,将结果写入 rd

 revh.2w       rd,rj

 将 64 位数以字为组按半字逆序 

 将 rj[31 : 0] 中的 2 个半字逆序排列,将 rj[63 : 32] 中的 2 半字逆序排列,将结果写入 rd

 revh.d       rd,rj

 将 64 位数以双字为组按半字逆序 

 将 rj[63 : 0] 中的 4 个半字逆序排列,将结果写入 rd

 bitrev.4b       rd,rj

 将 32 位数以字节为组按位逆序

 将 rj[7 : 0] 逆序,将 rj[15 : 8] 逆序,将 rj[23 : 16] 逆序,将 rj[31 : 24] 逆序,结果写入 rd

 bitrev.8b       rd,rj 将 64 位数以字节为组按位逆序
 bitrev.w       rd,rj

 将 32 位数以字为组按位逆序

 将 rj[31 : 0] 中的 32 个位逆序,结果符号扩展后写入 rd

 bitrev.d       rd,rj

 将 64 位数以双字为组按位逆序

 将 rj[63 : 0] 中的 64 个位逆序,结果写入 rd

 bstrins.w       rd,rj,msbw,lsbw

 将 32 / 64 位数的位替换

 把 rj 中的 [(msbw - lsbw) : 0] 位 替换到 rd 的 [msbw : lsbw]

 bstrins.d      rd,rj,msbd,lsbd
 bstrpick.w       rd,rj,msbw,lsbw 将 32 位数的位提取

     (1)符号扩展运算,可用于 C 的byte、short 数据类型到 int 类型的转换

if r4 == 0x000000000000FFFA

ext.w.h		r5, r4	# r5 = 0xFFFFFFFFFFFFFFFA

     (2)以字为组按字节逆序,可用于尾端转换

if r1 == 0xEE00FF11_CC22DD33

revb.2w		r5, r1	# r5 = 0x11FF00EE_33DD22CC

3.2 访存指令

        LoongArch 中访存指令:
     (1)普通访存指令
     (2)边界检查访存指令(访存前会对地址的合法性进行检查)
     (3)原子访存指令(能够原子性地完成对某个内存地址的读-修改-写的操作序列)

       栅障指令不属于访存指令。

3.2.1 普通访存指令

 指令格式 功能简述
 ld.b       rd,  rj,  si12 从内存地址 (rj + si12)加载一字节/半字/字/双字到 rd 寄存器,写入前需符号扩展
 ld.h       rd,  rj,  si12
 ld.w       rd,  rj,  si12
 ld.d       rd,  rj,  si12
 ld.bu       rd,  rj,  si12 从内存地址 (rj + si12)加载一字节/半字/字到 rd 寄存器,写入前需零扩展
 ld.hu       rd,  rj,  si12
 ld.wu       rd,  rj,  si12
 st.b       rd,  rj,  si12 将寄存器 rd 中的 [7 : 0]/[15 : 0]/[31 : 0]/[63 : 0] 位数据写入内存地址(rj + si12)   
 st.h       rd,  rj,  si12
 st.w       rd,  rj,  si12
 st.d       rd,  rj,  si12
 ldx.b       rd,  rj,  rk 从内存地址 (rj + rk)加载一字节/半字/字/双字到 rd 寄存器,写入前需符号扩展
 ldx.h       rd,  rj,  rk
 ldx.w       rd,  rj,  rk
 ldx.d       rd,  rj,  rk
 ldx.bu       rd,  rj,  rk 从内存地址 (rj + rk)加载一字节/半字/字到 rd 寄存器,写入前需零扩展
 ldx.hu       rd,  rj,  rk
 ldx.wu       rd,  rj,  rk
 stx.b       rd,  rj,  rk  将寄存器 rd 中的 [7 : 0]/[15 : 0]/[31 : 0]/[63 : 0] 位数据写入内存地址(rj + rk)
 stx.h       rd,  rj,  rk
 stx.w       rd,  rj,  rk
 stx.d       rd,  rj,  rk
 ldptr.w       rd,  rj,  si14  从内存地址(rj + si14<<2)加载一个字/双字的数据,写入前需符号扩展
 ldptr.d       rd,  rj,  si14
 stptr.w       rd,  rj,  si14  将寄存器 rd 中的 [31 : 0] / [63 : 0] 位数据写入内存地址(rj + si14<<2)
 stptr.d       rd,  rj,  si14
 preld       hint,  rj,  si12  从内存预取一个 Cache 行的数据到 Cache 中

     (1)以 .b、.h、.w 为后缀的指令都需要进行符号扩展之后再加载到指定寄存器。当使用基址加立即数偏移的访问指令时,偏移量是 si12,即所能表达的偏移范围为 [-2048, 2047],4 KB。

     (2)对于满足自然对齐的访存地址,可用 ldptr.w、ldptr.d、stptr.w、stptr.d 访存指令,其地址偏移量是 (si14 << 2),64 KB。

     (3)指令 “ preld hint, rj, si12” 用于从指定内存地址提前加载一个 Cache 行的数据到 Cache 中。内存地址 = rj + si12 。指令中的 hint 有 0 ~ 31 ,共 32 个可选值,表示预取类型,目前 hint = 0 定义为 load 预取至一级数据 Cache,hint = 8 定义为 store 预取至一级数据 Cache ,其他 hint 值暂未定义,等于 nop 指令。

     (4)在编译器内部,通常会将数组下标 0 所在的地址、类对象所在地址、字符串首字符所在地址、堆栈指针寄存器 SP 等当作基址,然后通过偏移量来对其他数据进行索引,base = &a[0],&a[3] = base + sizeof(int) * 3 。

     (5)当访存地址低两位为 0 时(可解读为满足内存地址自然对齐),相较于 ld/st 指令,可以使用此类指令实现偏移范围在 16 位 [-32768, 32767] 的地址访问。

     (6)预取数据

preld	8, r6, 0

       这条指令实现从地址 r6+0 的内存位置读取一个 Cache 行(龙芯 3A5000 系列芯片中一个 Cache 行是 64 字节)的数据到 Cache 中。hint=8 意味着预取的数据接下来会有写(Store)的处理。

       合理地使用预取指令可以减少程序运行中的 Cache Miss(缓存未命中)带来的延迟,提升程序效率。

3.2.2 边界检查访存指令

       对指定内存地址做读写操作之前,边界检查访存指令会进行条件检查,确认这个地址是否大于(小于或等于)给定的地址范围,如果条件不满足则会终止读写操作并触发边界检查例外

 指令格式 功能简述
 ldgt.b       rd,  rj,  rk

     从内存地址 rj 加载一个字节、半字、字、双字的数据写入寄存器 rd,需要符号扩展。
当 rj <= rk ,触发边界检查例外

 ldgt.h       rd,  rj,  rk
 ldgt.w       rd,  rj,  rk
 ldgt.d       rd,  rj,  rk
 ldle.b       rd,  rj,  rk     从内存地址 rj 加载一个字节、半字、字、双字的数据写入寄存器 rd,需要符号扩展。
当 rj > rk,触发边界检查例外
 ldle.h       rd,  rj,  rk
 ldle.w       rd,  rj,  rk
 ldle.d       rd,  rj,  rk
 stgt.b       rd,  rj,  rk     写寄存器  rd 中的一个字节、半字、字、双字到内存地址 rj 。
当 rj <= rk ,触发边界检查例外。
  stgt.h       rd,  rj,  rk
 stgt.w       rd,  rj,  rk
 stgt.d       rd,  rj,  rk
 stle.b       rd,  rj,  rk     写寄存器  rd 中的一个字节、半字、字、双字到内存地址 rj 。
当 rj > rk ,触发边界检查例外。
 stle.h       rd,  rj,  rk
 stle.w       rd,  rj,  rk
 stle.d       rd,  rj,  rk

     (1)在什么情况会用到这些指令?最常见的就是数组下标取值越界。C 语言并不具有类似 Java 语言中对程序员友好的动态防御功能,可以对程序中数组下标取值范围进行严格检查(一旦发现数组越界访问就会抛出异常而终止程序)。如果越界访问的内存区域是不可写的(例如恰好是只读的代码区),那么会马上出发异常(通常异常信号为 SIGBUS)。如果用带边界检查的指令,会发出 SIGSEGV 例外,事实上一些高级语言编译器,例如 Java 虚拟机,其内部动态防御功能的异常处理机制的实现原理也是与此类似的。
       TODO2:举例验证上述俩个异常信号

3.2.3 栅障指令

     (1)栅障类型分为数据栅障指令栅障
       数据栅障:防止处理器核对某些访存指令的乱序执行
       指令栅障:保证被修改的指令得以执行。

       乱序执行:当 CPU 在准备执行到某条需要等待的指令(例如访存指令的读操作数因为 Cache Miss 还没有准备好数据或比较耗时的乘法指令)时,可以先腾出指令执行通路,让排在后面的没有数据相关的指令先执行,从而避免流水线阻塞带来的性能下降。

 指令格式 功能简述
 dbar hint 数据栅障
 ibar hint 指令栅障

     (2)数据栅障的类型
       读栅障( LoadLoad ):用于确保数据栅障指令前后读内存指令的有序性,即不会被处理器乱序执行,只有数据栅障指令前的读内存指令执行完成后,数据栅障指令后面的读内存指令才可以执行
       写栅障( StoreStore ):用于确保数据栅障指令前后写内存指令的有序性,即不会被处理器乱序执行,只有数据栅障指令前的写内存指令执行完成后,数据栅障指令后面的写内存指令才可以执行
       完全栅障( AnyAny ):用于确保数据栅障指令前后所有访存指令的有序性,即不会被处理器乱序执行,只有数据栅障指令前的所有访存指令执行完成后,数据栅障指令后面的访存指令才可以执行

       表中的 “dbar hint” 指令中,操作数 hint 用于指示栅障的同步对象和同步程度。hint 默认值为 0 ,代表完全栅障。目前,龙芯仅实现了完全栅障,其他后续支持。

     (3)数据栅障的使用方法

# 写进程

st.d	val, data	# 先写数据到共享区域 data
st.d	1, tag		# 后写 1 到共享区域 tag,告知读进程,数据已准备好,可以读
# 读进程

ld.d	reg, tag	# 读标识区 tag
beqz	reg, L		# if tag == 0, nop
ld.d	val, data	# else val = data
L: nop

       这段程序运行在弱一致性模型的处理器上会存在隐患。由于乱序执行技术,写进程这俩条没有数据相关的写指令是有可能被乱序执行的,那么如果写进程第二条指令先执行,那么读进程就可能会读到旧数据,同理读进程可能会提前读数据,读到的也是有可能是旧的数据,所以读写进程都需要数据栅障指令。

# 写进程

st.d	val, data	# 先写数据到共享区域 data
dbar	0			# 确保顺序执行
st.d	1, tag		# 后写 1 到共享区域 tag,告知读进程,数据已准备好,可以读
# 读进程

ld.d	reg, tag	# 读标识区 tag
dbar	0			# 确保顺序执行
beqz	reg, L		# if tag == 0, nop
ld.d	val, data	# else val = data
L: nop

       弱一致性模型:是存储一致性模型中的一种,同步操作和普通访存需要区分开来,当程序中有写共享单元(或变量)存在时,程序员必须用架构所定义的同步操作把对写共享单元的访问保护起来,以保证多个处理器核对于写共享单元的访问是互斥的,即保证程序的正确性。这里所提的 “架构所定义的同步操作” 即栅障指令。
       TODO:存储一致性模型

       数据相关:在程序中,如果两条指令访问同一个寄存器或内存单元,且这两条指令中至少有一条是写该寄存器或内存单元的指令,则认定这两条指令存在数据相关。例如指令 “add.w r5, r4, r3;” 和指令 “sub.w r7, r5, r6;” 是数据相关的,因为同时用到了 r5,且指令 sub.w 的执行依赖指令 add.w 执行对 r5 的写完成。

     (4)指令栅障命令具有完成处理器核内部 store 操作和取指之间的同步,现代处理器基本都是多级 Cache 结构,其中处理器核内私有的一级 Cache 又分为 DCache 和 ICache,DCache 和 ICache 没有直接联系,故遇到指令被动态修改时,需要软件来保证修改后的指令(程序已经执行)回写到内存且对应的 ICache 位置上的旧指令作废。操作数 hint 为 0。
       TODO:指令被动态修改是什么?计算机动态类型语言?后续挖掘

3.2.4 原子访存指令

       原子访存指令用于确保对指定内存的 “读 - 修改 - 写” 操作序列执行的原子性(即从执行效果来看,读 - 修改 - 写整个过程不可分割且不会被中断)。其中修改动作包括对两个源操作数的交换、加法运算、与、或、异或、取最大值、取最小值,甚至自定义的动作等。

       LoongArch支持的原子访存指令有两类:
       内存原子操作(Atomic Memory Operation,AMO)
       连锁加载 / 条件存储(Load-Linked / Store-Conditional,LL-SC)

 指令格式 功能简述
 amswap.w       rd, rk, rj

32 / 64 位交换(赋值)<br/>

将 rk 的值写入内存地址 rj,内存地址 rj 旧值存入 rd  <br/>

rd = *rj;  *rj = rk;

 amswap.d       rd, rk, rj
 amswap_db.w       rd, rk, rj
 amswap_db.d       rd, rk, rj
 amadd.w       rd, rk, rj

32 / 64 位加法 <br/>

rd = *rj;  *rj = rk + *rj;

 amadd.d       rd, rk, rj
 amadd_db.w       rd, rk, rj
 amadd_db.d       rd, rk, rj
 amand.w       rd, rk, rj

32 / 64 位与 <br/>

rd = *rj;  *rj = rk & *rj;

 amand.d       rd, rk, rj
 amand_db.w       rd, rk, rj
 amand_db.d       rd, rk, rj
 amor.w       rd, rk, rj

32 / 64 位或 <br/>

rd = *rj;  *rj = rk | *rj;

 amor.d       rd, rk, rj
 amor_db.w       rd, rk, rj
 amor_db.d       rd, rk, rj
 amxor.w       rd, rk, rj

32 / 64 位异或 <br/>

rd = *rj;  *rj = rk ^ *rj;

 amxor.d       rd, rk, rj
 amxor_db.w       rd, rk, rj
 amxor_db.d       rd, rk, rj
 ammax.w       rd, rk, rj

32 / 64 位取最大值 <br/>

rd = *rj;  *rj = max(rk,  *rj);

 ammax.d       rd, rk, rj
 ammax_db.w       rd, rk, rj
 ammax_db.d       rd, rk, rj
 ammax.wu       rd, rk, rj

32 / 64 位取最大值 <br/>

无符号操作数

 ammax.du       rd, rk, rj
 ammax_db.wu       rd, rk, rj
 ammax_db.du       rd, rk, rj
 ammin.w       rd, rk, rj

32 / 64 位取最小值 <br/>

rd = *rj;  *rj = min(rk,  *rj);

 ammin.d       rd, rk, rj
 ammin_db.w       rd, rk, rj
 ammin_db.d       rd, rk, rj
 ammin.wu       rd, rk, rj

32 / 64 位取最小值 <br/>

无符号操作数

 ammin.du       rd, rk, rj
 ammin_db.wu       rd, rk, rj
 ammin_db.du       rd, rk, rj
 ll.w       rd, rk, si14ll 和 sc 这两对指令一同实现原子的 “读 - 修改 - 写”
 ll.d       rd, rk, si14
 sc.w       rd, rk, si14
 sc.d       rd, rk, si14

       表中 AMO 指令中寄存器 rj 为目的寄存器,存放待操作的内存地址,rj 所指向的内存地址的数据旧值保存到 rd ,rj 所指向的内存地址的数据新值来自 rj 所指向的内存地址的数据旧值和 rk 寄存器值的某种计算。这里的新旧指定是 “某种计算” 的前后。表中 AMO 指令带 _db 标识的指令,除了可以完成原子内存操作外,还能实现数据栅障(AnyAny 类型)功能。

     (1)连锁加载 / 条件存储(Load-Linked / Store-Conditional,LL-SC)
       表中 LL-SC 中的 ll 指令用于从内存地址为 rj + si14 加载数据到寄存器 rd ,同时记录这个内存地址并标记 LLbit = 1;sc 指令用于将 rd 的值写回内存地址为 rj + si14 ,该指令会检查 LLbit,若 LLbit == 1,写回并 rd = 1 并 LLbit = 0,若 LLbit == 0,不写并 rd = 0。在配对的 LL-SC 执行期间,当其他处理器核对该地址执行了写操作时,会导致 LLbit 置 0。
       LL-SC 对一个内存单元的原子操作的维护需要软件来完成。需构建循环实现 “读 - 修改 - 写” 访存操作序列。

     (1.1)使用 LL-SC 实现 a=a+1 的原子操作

if (rj + si14) == &a
	b = &a;

1:						# label 1
	ll.w	r4, b		# r4 = *b
	addi.w	r6, r4, 1	# r6 = a + 1
	sc.w	r6, b		# *b = r6
	beqz	r6, 1b		# if 写回失败 r6 = 0,跳转到 label 1,重复读 - 修改 - 写,Load-Linked 连续、连锁加载意思,Store-Conditional 有条件的写回

     (1.2)什么情况 a=a+1 需要原子操作?

       假定有两个线程都实现对同一个共享变量 a = 3 进行加 1 操作,那么每个线程的代码是相同的,都需要 3 条指令完成,代码如下:

 线程 1                                                                                                       线程 2                                                                                                                     
 ld.w       r4,  addr(a) ld.w       r4,  addr(a)
 addi.w       r5,  r4,  1 addi.w       r5,  r4,  1
 st.w       r5,  addr(a) st.w       r5,  addr(a)

       假定连个线程都被执行一次,我们期望是 5 ,但是实际运行结果可能是 5 ,也可能是 4。

       实际运行结果可能是 5 :

 线程 1                                                                                                       线程 2                                                                                                                     
 ld.w       r4,  addr(a)       # a = 3 - -
 addi.w       r5,  r4,  1       # a = 4 - -
 st.w       r5,  addr(a)       # a = 4 - -
  ld.w       r4,  addr(a)       # a = 4
  addi.w       r5,  r4,  1       # a = 5
  st.w       r5,  addr(a)       # a = 5

       实际运行结果可能是 4 :

 线程 1                                                                                                       线程 2                                                                                                                     
 ld.w       r4,  addr(a)       # a = 3 - -
 - - ld.w       r4,  addr(a)       # a = 3
 - - addi.w       r5,  r4,  1       # a = 4
 - - st.w       r5,  addr(a)       # a = 4
 addi.w       r5,  r4,  1       # a = 4 
 st.w       r5,  addr(a)       # a = 4 

       对于这种对数据同步有要求的情况,就可以使用 LL-SC,这个问题的关键点是线程 2 修改了 a = 4 并把结果回写后,线程 1 并没有感知到,如果线程 1 能感知到的话,那么线程 1 就必须重新加载 a 的值并重新计算了。这里的 “感知” 就是上文中的标记 LLbit

       LL-SC解决如下 :

 线程 1                                                                                                       线程 2                                                                                                                     
 L:    ll.w       r4,  addr(a)                                       - -
 - - L:    ll.w       r4,  addr(a)                          
 - - addi.w       r5,  r4, 1                                
 - - sc.w       r5,  addr(a)    # 写回成功         
 - - beqz       r5,  L    # 写回成功,无需跳转
 addi.w       r5,  r4, 1                                                 - -
 sc.w       r5,  addr(a)    # 写回失败                          - -
beqz       r5,  L    # 跳转 L 处,重新 读 - 修改 - 写  - -

     (2)内存原子操作(AMO)

       AMO 指令覆盖了大部分的简短且常用的运算。

     (2.1)使用 LL-SC 实现 a=a+1 的原子操作

li.w	r2, 1		# 加载立即数 1
li.w	r4, &a		# 加载变量 a 的地址
amadd.w	r0, r2, r4	# a=a+1 并写回。旧值不保存故用 r0

     (2.2)使用 _db 标识的 AMO 优化

(2.2.1)优化前代码:

# 写进程

st.d	val, data	
dbar	0			# 确保顺序执行
st.d	1, tag		

(2.2.2)优化后代码:

# 写进程

st.d	val, data	# 先写数据到共享区域 data
amswap_db.d		r0, 1, tag

       注意 AMO 仅值 32 / 64 位数据的简单算术和逻辑原子计算。对于 8 、16 位数据的原子运算或者更复杂的一些原子操作时,只能用 LL-SC 原子访存指令对来实现相应功能。rd 和 rj 的寄存器号不能相同,rd 和 rk 也不行。

3.3 转移指令

       转移指令用于执行有条件或无条件的分支跳转、函数调用、函数返回和循环的等。

 指令格式 功能简述
 beq       rj,  rd,  offs16相对于 PC 的分支转移 if( rj == rd )    PC = PC + offs16 << 2
 bne       rj,  rd,  offs16 if( rj != rd )    PC = PC + offs16 << 2
 blt       rj,  rd,  offs16 if( rj < rd )    PC = PC + offs16 << 2
 bge       rj,  rd,  offs16 if( rj >= rd )    PC = PC + offs16 << 2
 bltu       rj,  rd,  offs16 if( rj < rd )    PC = PC + offs16 << 2 
  rj, rd 都是 unsigned
 bgeu       rj,  rd,  offs16 if( rj >= rd )    PC = PC + offs16 << 2
  rj, rd 都是 unsigned
 beqz       rj,  rd,  offs21 if( rj == 0 )    PC = PC + offs21 << 2
 bnez       rj,  rd,  offs21 if( rj != 0 )    PC = PC + offs21 << 2
 b       offs26 PC = PC + offs26 << 2
 bl       offs26 r1 = PC + 4; PC = PC + offs26 << 2
 jirl       rd,  rj,  offs16绝对跳转 rd = PC + 4; PC = rj + offs16 << 2

       相对跳转( 地址计算靠 PC ),称为 “分支” ( Branch ),助记符以 b 开头;
       绝对跳转( 地址计算不靠 PC ),称为 “跳转” ( Jump ),助记符以 j 开头;

       这里的 PC 为程序计数器,用于控制程序中指令的执行顺序。程序正常运行时,PC 总是指向 CPU 运行的下一条指令。

3.3.1 有条件的分支指令

(1)用汇编实现下面 C 语言的程序:

if (a == 0) b++;
else b--;
beqz	r4, 8
addi.w	r5, r5, -1	# b--
b 4
addi.w	r5, r5, 1	# b++
nop

3.3.2 无条件分支指令和跳转指令

       无条件分支指令 bl 通常被用作函数调用,跳转指令 jirl 通常被用作函数返回

(1)用汇编实现 C 语言中的函数调用与函数返回:

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

int main() {
	add(1, 2);
}
add:
	...
	add.w	r4, r4, r5
	jirl	r0, r1, 0	# 函数返回,寄存器 r1 存放函数的返回地址

main:
	...
	bl	add
	...
	jirl	r0,	r1,	0	# main 函数返回,寄存器 r1 存放函数的返回地址

3.3.3 跳转范围

       表中有条件分支指令的偏移量为 offs16 << 2,[PC - 128K,PC + 128K];
       表中无条件分支指令的偏移量为 offs26 << 2,[PC - 128M,PC + 128M];

       跳转范围的增大可以减少地址加载所带来的指令开销。

假设程序中需要无条件跳转到 [PC - 128M,PC + 128M] 中的一个地址(0x40,那么就仅需要一条指令:

b	0x10	# PC+(0x10 << 2)

但是当要跳转的地址超出这个范围,那么就得使用 jirl 完成跳转

li	r7,	(PC+offsets)	# 加载目标地址到寄存器 r7,由于 li 是个宏指令,汇编器会扩展成 1 ~ 4 条汇编
jirl	r0, r7, 0	# 跳转到目标地址

3.4 其他杂项指令

读取恒定频率计时器信息

 指令格式 功能简述
 syscall      code 系统调用
 break       code 断点例外
 asrtle.d       rj, rk 当寄存器 rj 中的值小于或等于(le)/ 大于(gt)寄存器 rk 的条件不成立时,触发例外   
 asrtgt.d       rj, rk
 rdtimel.w       rd, rj 读取恒定频率计时器信息
rd = StableCounter,rj = CounterID
 rdtimeh.w       rd, rj
 rdtime.d       rd, rj
 cpucfg      rd, rj 读取 CPU 特性
 crc.w.b.w       rd, rj, rk  CRC IEEE 8023
 crc.w.h.w       rd, rj, rk
 crc.w.w.w       rd, rj, rk
 crc.w.d.w       rd, rj, rk
 crcc.w.b.w       rd, rj, rk  CRC Castagnoli
 crcc.w.h.w       rd, rj, rk
 crcc.w.w.w       rd, rj, rk
 crcc.w.d.w       rd, rj, rk

3.4.1 系统调用指令

       处理器执行 syscall 指令将立即无条件触发系统调用例外,使程序进入内核态。操作数 code 所携带的信息可供例外处理例程作为所传递的参数使用,一般为 0 即可。

       内核提供的进程退出功能接口函数为 sys_exit(int error_code),LoongArch ABI 规定此函数的系统调用号为 93 ,使用寄存器 r11 来传递系统调用号,r4 ~ r10 来传递系统调用参数。

(1)实现 sys_exit 系统调用的指令如下:

li.w	r11, 93	# 加载系统调用号 93 到寄存器 r11
li.w	r4, 0	# 将错误吗值 0 作为第一个参数,加载到 r4
syscall 0		# 系统调用,程序陷入内核态

       上述代码其功能相当于执行了 libc 库中的 exit(0) 函数。更多内核提供的接口功能和其系统调用号见 unistd.h

3.4.2 断点例外指令

       断点例外指令 break 用于无条件地触发断点例外。指令码中的 code 域携带的信息为例外类型,具体类型定义在 break.h 文件。在调试汇编指令代码时,break 指令是很有用的调试手段

break	5

       程序执行到这条指令时,就会收到一个 SIGTRAP 信号,提示信息为 “Trace / breakpointtrap”,同时程序会停到当前指令位置。我们常用的 GDB 调试工具中,软件断点功能就是通过 break 指令来实现的。

3.4.3 读取恒定频率计时器信息指令

       StableCounter:64 位的恒定频率计时器
       CounterID:每个恒定频率计时器都有一个软件可配置的全局唯一编号
       每个处理器核都会对应一个恒定频率计时器。

3.4.4 读取 CPU 特性指令

       龙芯架构参考手册 - 卷一:基础架构 2.2.10.5 CPUCFG

3.4.5 CRC 指令

       龙芯架构参考手册 - 卷一:基础架构 2.2.9 CRC 校验指令

3.4.6 地址边界检查指令

       龙芯架构参考手册 - 卷一:基础架构 2.2.10.3 ASRT{LE/GT}.D

3.5 特权等级和特权指令概述

       龙芯架构参考手册 - 卷一:基础架构 4 特权资源架构概述


第四章 LoongArch 基础浮点数指令集

4.1 浮点数存储方式和数值范围

       由于浮点数的特殊性,无法采用整数的补码存储方式,故 IEEE 规定了两种基本的浮点数格式:单精度( float )和双精度( double )。组织格式如下:


 31  30          23 22                        0
 +---+-------------+--------------------------+
 | S |      E      |            F             |
 +---+-------------+--------------------------+

 63  62                 52 51                                             0
 +---+--------------------+-----------------------------------------------+
 | S |         E          |                     F                         |
 +---+--------------------+-----------------------------------------------+

       S(ign)位域: 符号位,1 位,0 表示正数,1表示负数;
       E(xponent)位域: 偏置指数,8 位;
       F(raction)位域: 尾数,23 位;

4.1.1 规格化的值

       当偏置指数 e 区域不是全 0 ,也不是全 1 时,将其计算出来的浮点数值定义为规格化的值。
       计算公式为:
       单精度浮点数:(-1)S x 2E-127 x 1.F    移码 = 127
       双精度浮点数:(-1)S x 2E-1023 x 1.F    移码 = 1023

     (1)给定 10 进制浮点数( 例如,5.75D),求其 2 进制浮点数

     (1.1)S 位域:由于5.75D是正数,故 S 位为 0;
     (1.2)E 位域:
                 23, 22,  21,  20 .  2-1,   2-2,   2-3
                 8,   4,   2,   1,  .  0.5,  0.25, 0.125
                 5.75D = 101.11B

                 转换以 2 为底指数的形式(1.xyzB x 2e):
                 101.11B = 101.11B x 20 = 1.01_11B x 22 ,于是 e = 2,E = 指数 + 移码 = 2 + 127 = 129D = 1000_0001

     (1.3)F 位域:
                 上述中的1.01_11B 去掉小数点(.)和小数点前的整数后(01_11B)剩下的低位补全 0 后就是 F 位域,F = 011_1000_0000_0000_0000

     (1.3)5.75D 浮点数 2 进制存储如下:


         31  30          23 22                        0
         +---+-------------+--------------------------+
5.75D =  | S |      E      |            F             |
         +---+-------------+--------------------------+
           0    1000_0001    011_1000_0000_0000_0000

     (1.4)给定 2 进制浮点数,求其 10 进制浮点数:
       可以先求 e = E - 127 = 129 - 127 = 2,再将 F 域转换,即小数点和小数点前的 1 补上并去掉低位连续的 0,故 F 域转换为1.011_1,由于e = 2,于是1.0111B x 22 = 101.11B = 5.75D

4.1.2 非规格化的值(略)

4.1.3 正负无穷大或者 NaN(略)

4.2 浮点寄存器 ~ 4.8 浮点搬运指令(略)

       龙芯架构参考手册 - 卷一:基础架构 3 基础浮点数指令


第五章 LoongArch ABI

       ABI 的全称为应用程序二进制接口( Application Binary Interface )的定义了应用程序二进制代码中数据结构和函数模块的格式及其访问方式,它使得不同的二进制模块之间的交互成为可能。可参考如下:
       计算机体系结构基础 第 3 版 4.1 应用程序二进制接口

       LoongArch 共定义了 3 套 ABI:
     (1)LP64:指针、寄存器都是 64 位
     (2)LPX32:指针 32 位、寄存器是 64 位
     (3)LP32:指针、寄存器都是 32 位

5.1 数据类型、数据对齐和字节序列

5.1.1 数据类型

 C/C++ 语言数据类型 LA64 大小 LA64 对齐方式 LA32 大小 LA32 对齐方式
 bool/char 1 1 1 1
 short 2 2 2 2
 int 4 4 4 4
 long 8 8 4 4
 long long 8 8 8 8
 void* 8 8 4 4
 __int128 16 16 16 16
 float 4 4 4 4
 double 8 8 8 8
 long double 16 16 16 16

       __int128 和 long double 的大小都是 16 字节,意味着对这两种数据类型的数据加载或者计算时,在 LA64 上需要 2 个寄存器,在 LA32 上需要 4 个。

5.1.2 数据对齐

       为了简化处理器和内存系统之间的硬件设计,许多计算机系统对访存操作的地址做了限制,要求被访存的地址必须是其数据类型的倍数,又叫自然对齐。

       LoongArch 支持硬件处理非对齐的内存数据访问。但是为了性能更优 ,建议尽量对齐数据。

       编译器会自动帮忙处理对齐的问题。

char	cVar;
int		iVar;
short	sVar;
long	lVar;

 地址 变量
 0x120008070 cVar
 0x120008074 iVar
 0x120008078 sVar
 0x120008080 lVar

5.1.3 字节序列

写个判断 LoongArch 是大小端的 C 程序:

#include <stdio.h>
int main(){
        int a = 1;
        if((char)a)
                printf("Little Endian\n");
        else
                printf("Big Endian\n");
        return 0;
}

5.2 LoongArch 寄存器使用约定

5.2.1 通用寄存器(GR)使用约定

 寄存器名称 别名 使用约定( 功能描述 )
 r0 zero 常量寄存器,其值永远为 0
 r1 ra 函数返回地址( return address )
 r2 tp 用于支持 TLS( Thead-local Storage )
 r3 sp 栈指针( stack pointer )
 r4 ~ r11 a0 ~ a7 参数寄存器( argument )
 r4 ~ r5 v0 ~ v1 函数返回值( return value )
 r12 ~ r20 t0 ~ t8 临时寄存器( temporary )
 r21 - - 保留寄存器
 r22 fp 帧指针( frame pointer )
 r23 ~ r31 s0 ~ s8 保存寄存器( saved )

     (1)寄存器功能介绍

     (1.1)zero 寄存器

       不管对其写入什么值,读取它的值时永远返回 0 。例如要取一个变量的相反数,就可以用 zero 寄存器和这个变量所在的寄存器做减法,从而减少对立即数 0 的加载操作

sub.w	t5, zero, t4

       zero 寄存器对宏指令的作用也是很大的,例如 LoongArch 中的宏指令 move。宏指令是为了方便软件编程或语义直观而定义的一组指令,这些宏指令在编译时会由汇编器转换为真实的机器指令。

move	t0, t1	<=>	or t0, t1, zero	<=>	add.d t0, t1, zero

     (1.2)函数调用与寄存器 v0 ~ v1、a0 ~ a7、ra

       LoongArch ABI 规定发生函数调用时,寄存器 a0 ~ a7 用来传递前 8 个整形参数或指针参数,其中 a0 和 a1 (别名又为 v0 和 v1)也用于返回值,寄存器 ra 用于保存返回地址

int ret = add(2, 3);
add:
	add.w	a0, a0, a1
	jirl	zero, ra, 0

main:
	li.w	a0, 0x2
	li.w	a1, 0x3
	bl	add

     (1.3)临时寄存器 t0 ~ t8 和保存寄存器 s0 ~ s8

       t0 ~ t8(temporary),在函数中充当临时变量的作用,在函数中使用这几个临时寄存器时,不用考虑保存旧值的问题(调用者保存)。
       s0 ~ s8(saved),当前函数应该负责保证这几个寄存器的值在函数返回时和函数入口处一致。将旧值保存到栈上,在函数返回前恢复其旧值。(被调用者保存)

st.d	s0, sp, 32
...
ld.d	s0, sp, 32

     (1.4)tp 寄存器

       tp 寄存器用于支持线程局部存储( Thread-Local Storage,TLS)。TLS 是一种线程局部变量的存储方法,保证变量在线程内是全局可访问的,但是不能被其他线程访问。例如 libc 库中的 _Thread_local errno 变量就是一个典型的线程局部变量,用于标识当前线程最新的错误编号。LoongArch ABI 专门占用一个寄存器来指向当前线程的 TLS 区域,目的就是实现此区域内变量的快速定位和访问,提高程序执行效率。通常 tp 由系统 libc 库维护(负责读写),用户程序最好不要用。

     (1.5)函数栈和寄存器 sp、fp

       在数据结构中,栈( Stack )是只允许在同一端进行插入和删除操作的动态存储空间。它按照先进后出的原则存储数据,即先进入的数据被压在栈底,最后进入的数据在栈顶。函数栈也是一段动态存储空间,用于一个函数内的局部变量和相关寄存器的保存。sp、fp 记录每个函数栈的起始位置。

5.2.2 浮点寄存器(FR)使用约定

 寄存器名称 别名 使用约定
 f0 ~ f7 fa0 ~ fa7 参数寄存器( argument )
 f0 ~ f1 fv0 ~ fv1 函数的返回值( return value)
 f8 ~ f23 ft0 ~ ft15 临时寄存器( temporary )
 f24 ~ f31 fs0 ~ fs7 保存寄存器( saved )

5.3 函数调用约定

5.3.1 函数参数传递

       LoongArch ABI 对基本数据类型作为函数参数传递时,因参数数量和参数类型的不同,使用的寄存器、传递规则也不同。

     (1)标量作为参数传递

       在计算机语言中,标量指的是不可被分解的量。例如 C 语言中的基本数据类型、指针。根据 ABI 规定,标量作为参数传递有以下几种情况。

       当一个标量位宽 <= XLEN 位或者一个浮点 <= FLEN 位时,使用单个参数寄存器传递。若没有可用的参数寄存器,则在栈上传递。

       当 XLEN < 一个标量位宽 <= 2 * XLEN 时,用一对参数寄存器传递,低 XLEN 位在小编号寄存器,高 XLEN 位在大编号寄存器。若没有可用的参数寄存器,则在栈上传递。若只有一个寄存器可用,低 XLEN 位寄存器传,高 XLEN位栈上传。

       当 2 * XLEN < 一个标量位宽时,则通过引用传递,并在参数列表中用地址替换。通过引用传递的实参可以由被调用者修改。

     (1.1)常见的标量参数序列及其使用寄存器情况

 参数列表 参数寄存器
 n1, n2, n3 a0, a1, a2
 d1, d2, d3 fa0, fa1, fa2   
 s1, s2, s3
 s1, d1, d2
 n1, n2, n3, n4, n5, n6, n7, n8, n9 a0, a1, a2, a3, a4, a5, a6, a7, stack(调用者)
 n1, d1 a0, fa0
 d1, n1, d2 fa0, a0, fa1
 n1, n2, d1 a0, a1, fa0
 d1, d2, d3, d4, d5, d6, d7, d8, d9, d10 fa0, fa1, fa2, fa3, fa4, fa5, fa6, fa7, a0, a1

n 代表整型数据类型( byte、short、int、long等 )

s 代表单精度浮点( float )

d 代表双精度浮点( double )

     (1.2)整形和指针类型参数传递

ret = strncmp("hello", "Hello World", 5);
寄存器             内容
       +--------------------------+
  a0   | address of "hello"       |
       +--------------------------+
  a1   | address of "Hello World" |               
       +--------------------------+
  a2   | 5                        |
       +--------------------------+

     (1.3)当实参多于 8 个整型或指针时,将利用函数栈来传剩余的参数

int test (int v0, int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9);

test(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
栈位置   内容       寄存器   内容
       +-----+           +-------+
 sp+0  |  8  |      a0   |   0   |
       +-----+           +-------+
 sp+4  |  9  |      a1   |   1   |
       +-----+           +-------+
                    a2   |   2   |
                         +-------+
                    a3   |   3   |
                         +-------+
                    a4   |   4   |
                         +-------+
                    a5   |   5   |
                         +-------+
                    a6   |   6   |
                         +-------+
                    a7   |   7   |
                         +-------+
注意:这里的 sp 指的是调用者( Caller )函数的栈指针

     (2)聚合体作为参数传递

       聚合体是和标量相对的概念,标量的组合体,例如 C 语言中的结构体、数组等。根据 ABI 规定,聚合体作为参数传递有以下几种情况。

       当一个聚合体的宽度 <= XLEN 位时,使用单个寄存器传递,并且这个聚合体在寄存器中的字段布局同它在内存中的字段布局保持一致;若没有可用的寄存器,则在栈上传递。

       当 XLEN < 一个聚合体的宽度 <= 2 * XLEN 时,用一对寄存器传递;若只有一个寄存器可用,则聚合体的前半部分寄存器传,后半部分栈上传;若没有可用的寄存器,则在栈上传递。由于填充而未使用的位,以及从聚合体的末尾至下一个对齐位置之间的位,都是未定义的

       当 2 * XLEN < 一个聚合体的宽度时,则通过引用传递,并在参数列表中用地址替换。传递到栈上的聚合体会对齐到类型对齐和 XLEN 中的较大者,但不会超过栈对齐要求

       ABI 规定位域( Bitfield )以小端顺序排列。跨越其整型类型的对齐边界的位域将从下一个对齐边界开始。
       计算机体系结构基础 第 3 版 4.1 应用程序二进制接口

     (2.1)小于 2 * XLEN 的结构体作为参数传递

struct things {
	char v1;
	int v2;
	int v3;
} = {'a', 14, 256}
寄存器              内容
       +--------------------------+
  a0   |      'a'    |     14     |
       +--------------------------+
  a1   |            256           |               
       +--------------------------+

     (2.2)大于 2 * XLEN 的结构体作为参数传递

struct things {
	char v1;
	int v2;
	int v3;
	long v4;
} = {'a', 14, 256, 6792}

void fun(things);
桟位置       内容      寄存器       内容
          +-------+            +------------+
 sp+0     |  'a'  |      a0    |    sp+0    |
          +-------+            +------------+
 sp+4     |  14   |
          +-------+
 sp+8     |  256  |
          +-------+
 sp+16    |  6792 |
          +-------+

     (2.3)结构体位域排列方式

struct {
	int x:10;
	int y:12;
} b1;

struct {
	short x:10;
	short y:12;
} b2;
struct b1:      31      21             9        0
                +-------+--------------+--------+
                |       |      y       |   x    |
                +-------+--------------+--------+


struct b2:      31  27            15   9        0
                +---+--------------+---+--------+
                |   |      y       |   |   x    |
                +---+--------------+---+--------+

     (3)可变参数的传递

       计算机体系结构基础 第 3 版 4.1 应用程序二进制接口

5.3.2 函数返回值传递

       一个函数返回的数据类型可以是整型、指针类型、浮点数类型(单、双精度)、结构体(枚举类型归为结构体),或者无返回值(void 类型)。ABI 规定如下:

       当函数没有返回值时,不需要考虑返回寄存器的处理。

       当函数返回类型是整型或指针类型时,返回值存放在整型寄存器 v0 上。

       当函数返回类型是浮点(单、双精度)时,值存放在 fv0 上;当返回一个双精度浮点(C 语言中的 long double )时,值存放在 fv0、fv1 上。

       当函数返回类型是结构体(或者枚举类型)时,还要根据结构体内部成员情况细分:当返回类型是有一个或两个 float 或 double 类型的成员的结构体时,fv0 是返回值的第一个成员,fv1 是返回值的第二个成员(如果有);当返回值类型时有一个或两个整型成员的结构体时,v0返回值的第一个成员,v1 返回值的第二个成员(如果有);当返回值类型大于 16 字节时,通过引用的方式传递返回值。

     (1)返回值类型是 int

int fun() {
	return 100;
}
寄存器     内容
        +-------+
  v0    |  100  |
        +-------+

     (2)返回值类型是 long double

long double fun() {
	return 3.1415;
}
寄存器     内容
 fv0    +-------+
        | 3.1415|
 fv1    +-------+

     (3)返回值类型是 struct

typedef struct {
	char v1;
	int v2;
	long v3;
} Things;

Things fun(...);
寄存器           内容
         +--------+--------+
  v0     |   v1   |   v2   |
         +--------+--------+
  v1     |        v3       |
         +-----------------+
typedef struct {
	char v1;
	int v2;
	long v3;
	long v4;
} Things;

Things fun(...);
寄存器          内容
        +--------------+
  v0    | Things 的地址 | 
        +--------------+

5.4 函数栈布局

       像 C 这样的高级语言通常会用栈来管理函数运行过程使用的一些信息,包括返回地址、参数和局部变量等。

       LoongArch ABI 规定函数栈向下增长(朝向更低的地址),栈指针应该对齐到一个 16 字节的边界上作为函数入口,且在栈上传递的第一个实参位于函数入口的栈指针偏移量为 0 的地方,后面的参数存储在更高的地址中。函数栈在程序运行时动态分配,用来保存一个函数调用时需要维护的信息。这些信息包括函数的返回值地址、临时变量、栈位置。


High_Address  +----------------------+<--+  调用者的栈底
      |       |        GPR[ra]       |   |
      |       +----------------------+   |
      |       |        GPR[fp]       |   |
      |       +----------------------+   |
      |       |     Local variable   |   |
      |       +----------------------|   |
      |       |       (s0 ~ s7)      |   |
      |       +----------------------+   |
      |       |       Argument       |   |
      |       +----------------------+<--|--GPR[fp] 被调用者的栈底
      |       |        GPR[ra]       |   |               
      |       +----------------------+   |                
      |       |        GPR[fp]       |---+         
      |       +----------------------+                         
      |       |     Local variable   |
      |       +----------------------|             
      |       |       (s0 ~ s7)      |             
      |       +----------------------+             
      V       |       Argument       |             
 Low_Address  +----------------------+<-----GPR[sp] 被调用者的栈顶

       有了函数栈空间,当函数内部寄存器不够使用时,或者发生函数调用时,就可以把一些数据存储到栈空间(进栈),需要时再从栈空间加载到寄存器(出栈)。
       栈空间的分配是以进程为单位的。进程是系统进行资源分配和调度的基本单位。系统会在进程启动时指定一个固定大小的栈空间,用于该进程的函数参数和局部变量的存储。sp 的初始值就指向这个固定大小栈的栈底。该进程中的每一次函数调用,都会通过 sp 指针的移动来为函数在此空间划分出一块用作函数栈的空间,sp 指向栈顶。

       计算机体系结构基础 第 3 版 4.1.4 栈帧布局

       大部分函数可以只用 $sp 来管理栈帧。如果在编译时能够确定函数的栈帧大小,编译器可以在函数头分配所需的栈空间(通过调整 $sp),这样在函数栈帧里的内容都有一个编译时确定的相对于 $sp 的偏移,也就不需要栈帧 $fp 了。

C 语言代码示例:

/* test_fp.c */
extern int nested(int a, int b, int c, int d, int e, int f, int g, int h, int i );
int normal(void) {
        return nested(1, 2, 3, 4, 5, 6, 7, 8, 9);
}

默认编译参数汇编代码如下:

$ gcc test_fp.c -S -o test_fp.s

...
normal:
        addi.d  $r3,$r3,-32		# 分配 32 字节栈空间
        st.d    $r1,$r3,24		# $ra 进栈
        st.d    $r22,$r3,16		# $fp 进栈
        addi.d  $r22,$r3,32		# $fp 指向栈底
        addi.w  $r12,$r0,9                      # 0x9
        st.d    $r12,$r3,0		# 将 0x9 进栈
        addi.w  $r11,$r0,8                      # 0x8
        addi.w  $r10,$r0,7                      # 0x7
        addi.w  $r9,$r0,6                       # 0x6
        addi.w  $r8,$r0,5                       # 0x5
        addi.w  $r7,$r0,4                       # 0x4
        addi.w  $r6,$r0,3                       # 0x3
        addi.w  $r5,$r0,2                       # 0x2
        addi.w  $r4,$r0,1                       # 0x1
        bl      %plt(nested)
        or      $r12,$r4,$r0
        or      $r4,$r12,$r0
        ld.d    $r1,$r3,24		# $ra 出栈,不严格的栈		 
        ld.d    $r22,$r3,16		# $fp 出栈
        addi.d  $r3,$r3,32		# 栈回收
        jr      $r1

编译参数 -O2 汇编代码如下:

$ gcc test_fp.c -O2 -S -o test_fp_O2.s

...
normal:
        addi.d  $r3,$r3,-32
        addi.w  $r12,$r0,9                      # 0x9
        st.d    $r12,$r3,0
        addi.w  $r11,$r0,8                      # 0x8
        addi.w  $r10,$r0,7                      # 0x7
        addi.w  $r9,$r0,6                       # 0x6
        addi.w  $r8,$r0,5                       # 0x5
        addi.w  $r7,$r0,4                       # 0x4
        addi.w  $r6,$r0,3                       # 0x3
        addi.w  $r5,$r0,2                       # 0x2
        addi.w  $r4,$r0,1                       # 0x1
        st.d    $r1,$r3,24
        bl      %plt(nested)
        ld.d    $r1,$r3,24
        addi.d  $r3,$r3,32
        jr      $r1

       可以看出,O2 优化后的代码去掉了非必要的指令(函数头与函数尾巴中的代码),这会提高性能。但有时候可能无法在编译时确定一个函数的栈帧大小。在某些语言中,可以在运行时动态分配栈空间,如 C 程序的 alloca 调用,这会改变 $sp 的值。这时函数头会使用 $fp寄存器,将其设置为函数入口时的 $sp 值,函数的局部变量等栈帧上的值则用相对于 $fp 的常量偏移来表示。

代码示例:

# include <stdio.h>
# include <stdlib.h>

extern long 
nested(long a, long b, long c, long d, long e, long f, long g, long h, long i);

long dynamic(void) {
        long *p = alloca(64);
        p[0] = 0x123;

        return nested((long)p, p[0], 3, 4, 5, 6, 7, 8, 9);
}

$ gcc fp.c -O2 -S -o fp.s

PS:
	fp.c: In function ‘dynamic’:
	fp.c:8:12: warning: implicit declaration of function ‘alloca’ [-Wimplicit-function-declaration]
  	long *p = alloca(64);
            ^~~~~~
	fp.c:8:12: warning: incompatible implicit declaration of built-in function ‘alloca’
# include <stdlib.h> 解决

dynamic:
        addi.d  $r3,$r3,-32		# $sp = $sp -32
        st.d    $r22,$r3,16		# $(sp + 16) = $fp
        st.d    $r1,$r3,24		# $(sp + 24) = $ra
        addi.d  $r22,$r3,32		# $fp = $sp + 32
        addi.d  $r3,$r3,-64		# $sp = $sp - 64
        addi.d  $r4,$r3,16		# $a0 = $sp + 16
        addi.w  $r12,$r0,291    # $t0 = 0x123
        st.d    $r12,$r4,0		# $(sp + 16) = 0x123
        addi.w  $r12,$r0,9		# $t0 = 0x9
        st.d    $r12,$r3,0		# $(sp + 0) = 0x9
        addi.w  $r11,$r0,8      # $a7 = 0x8
        addi.w  $r10,$r0,7      # $a6 = 0x7
        addi.w  $r9,$r0,6		# $a5 = 0x6
        addi.w  $r8,$r0,5		# $a4 = 0x5
        addi.w  $r7,$r0,4		# $a3 = 0x4
        addi.w  $r6,$r0,3		# $a2 = 0x3
        addi.w  $r5,$r0,291		# $a1 = 0x123
        bl      %plt(nested)
        addi.d  $r3,$r22,-32	# $sp = $fp - 32  
        ld.d    $r1,$r3,24		# $ra = $(sp + 24)
        ld.d    $r22,$r3,16		# $fp = $(sp + 16)
        addi.d  $r3,$r3,32
        jr      $r1
 
 High_Address   0x10000---> +-----------------+ <--- $fp 
      |                     |       $ra       |
      |         0x0FFF8---> +-----------------+
      |                     |       $fp       |
      |         0x0FFF0---> +-----------------+ <--+ $sp + 80 
      |                     |                 |    |
      |         0x0FFE8---> +-----------------+    |
      |                     |                 |    |
      |         0x0FFE0---> +-----------------+    |          
      |                     |                 |    |
      |         0x0FFD8---> +-----------------+    |
      |                     |                 |    |   
      |         0x0FFD0---> +-----------------+    +---> alloca
      |                     |                 |    |
      |         0x0FFC8---> +-----------------+    |
      |                     |                 |    |
      |         0x0FFC0---> +-----------------+    |
      |                     |                 |    |
      |         0x0FFB8---> +-----------------+    |
      |                     |      0x123      |    |
      |         0x0FFB0---> +-----------------+ <--+ $sp + 16
      |                     |                 |
      |         0x0FFA8---> +-----------------+
      V                     |       0x9       |
 Low_Address    0x0FFA0---> +-----------------+ <--- $sp

5.5 系统调用约定

       从用户程序的角度看,内核是一个透明的系统层,因为用户程序都通过 libc 库运行,而不会直接调用内核接口。内核是一个操作系统的核心,负责管理系统的进程、内存、设备驱动程序、文件和网络系统等等,它是计算机硬件的第一层软件扩充,对上提供操作系统的应用程序接口(Application Program Interface, API),这些 API 也叫做系统调用。通常 libc 库对这些系统调用的接口做了封装,被看作用户程序和内核的中间层,例如函数 printf 的调用过程。


 +----------+       +----------------------------+       +-------------+
 |          |       |                            |       |             |
 | printf() |------>| printf() --------> write() |------>| sys_write() |
 |          |       |                            |       |             |
 +----------+       +----------------------------+       +-------------+

    App -----------------------> libc ---------------------> kernel

       了解系统调用约定,在必要的时候我们就可以编写汇编程序直接实现对内核接口的调用。ABI 规定:
     (1)寄存器 a7 传递系统调用号
     (2)寄存器 a0 ~ a6 传参数
     (3)同时 a0 也用来传递返回值
       不同于普通函数调用约定,系统调用回来后,寄存器 a0 ~ a6 的值可能会被破坏掉。

       内核提供的所有接口函数的名称及其系统调用号可以在内核源代码文件 include/uapi/asm-generic/unistd.h 或者系统文件 <asm/unistd.h> 中查到。
       这些函数对应的接口声明在 include/linux/syscalls.h。

     (4)使用 syscall 实现字符串 “Hello World” 的屏幕输出

       sys_write,其对应的系统调用号为 64 ,函数接口形式为

long sys_write(unsigned int fd, const char __user *buf, size_t count)

       即有 3 个参数,文件描述符、待输出的字符串地址、字符串长度。1 个返回值用于接收此函数的执行返回值。屏幕使用标准输出设备 /dev/stdout 的文件描述符为 1, 字符串长度为 12 ,地址由编译器来决定。

(4.1)代码如下:

        .text
        .align 2
        .globl syscall_test
        .type syscall_test,@function
syscall_test:
        li.d    $a7, 64				# 将 sys_write 系统调用号 64 写到寄存器 a7
        li.d    $a0, 1				# 将 /dev/stdout 文件描述符写到第一个参数寄存器 a0
        la.local        $a1, .LC0	# 将字符串地址写到第二个参数寄存器 a1
        li.d    $a2, 12				# 将字符串长度 12 写到第三个参数寄存器 a2
        syscall 0					# 系统调用
        jr $ra
        .size syscall_test, .-syscall_test

        .section        .rodata
.LC0:
        .ascii  "Hello World\n"
#include <stdio.h>

extern void syscall_test();

int main() {
        syscall_test();
        return 0;
}

       上述汇编示例中的后三条不是 LoongArch 汇编指令,而是 GCC 编译器的汇编器指令,用于通知汇编器工作时将字符串 “Hello World\n” 存放在当前进程的只读数据区,具体位置通过 .LC0 标注,使用时用伪指令 la.local 将其地址加载到指定的寄存器中。

(4.2)编译与运行:

Build:
$ gcc syscall_main.c syscall_test.S -o syscall_test

Run:
$ ./syscall_test
Hello World

(4.3)反汇编:

$ objdump syscall_test -D syscall_test

00000001200006d0 <main>:
   1200006d0:   02ffc063        addi.d  $r3,$r3,-16(0xff0)
   1200006d4:   29c02061        st.d    $r1,$r3,8(0x8)
   1200006d8:   29c00076        st.d    $r22,$r3,0
   1200006dc:   02c04076        addi.d  $r22,$r3,16(0x10)
   1200006e0:   54001c00        bl      28(0x1c) # 1200006fc <syscall_test>
   1200006e4:   0015000c        move    $r12,$r0
   1200006e8:   00150184        move    $r4,$r12
   1200006ec:   28c02061        ld.d    $r1,$r3,8(0x8)
   1200006f0:   28c00076        ld.d    $r22,$r3,0
   1200006f4:   02c04063        addi.d  $r3,$r3,16(0x10)
   1200006f8:   4c000020        jirl    $r0,$r1,0

...

00000001200006fc <syscall_test>:
   1200006fc:   0381000b        ori     $r11,$r0,0x40
   120000700:   03800404        ori     $r4,$r0,0x1
   120000704:   1c000005        pcaddu12i       $r5,0 # 获得当前 PC 值,PC = 0x120000704 
   120000708:   02c2b0a5        addi.d  $r5,$r5,172(0xac) # 字符串地址 = PC + 0xac = 0x1200007b0
   12000070c:   03803006        ori     $r6,$r0,0xc
   120000710:   002b0000        syscall 0x0
   120000714:   4c000020        jirl    $r0,$r1,0

...

Disassembly of section .rodata:

00000001200007ac <_IO_stdin_used>:
   1200007ac:   00020001        0x00020001
   1200007b0:   6c6c6548(lleH)        bgeu    $r10,$r8,27748(0x6c64) # 120007414 <__GNU_EH_FRAME_HDR+0x6c58>
   1200007b4:   6f57206f(oW o)        bgeu    $r3,$r15,-43232(0x35720) # 11fff5ed4 <_start-0xa64c>
   1200007b8:   0a646c72(\ndlr)        xvfmsub.d       $xr18,$xr3,$xr27,$xr8
...


第六章 LoongArch 目标文件和进程虚拟空间

       目标文件(Object File)指的是编译器对源代码进行编译后生成的文件。例如编译生成的未链接的中间文件 hello.o ,以及最终经过链接生成的不带文件扩展名的可执行文件 hello 都属于目标文件。目标文件包含编译后的机器指令、数据(全局变量、字符串等),以及链接和运行时需要的符号表、调试信息、字符串等。目前主流的目标文件格式是 Windows 系统采用的 PE ( Portable Executable,包括未链接的 .obj 文件和可执行的 .exe 文件 )和 Linux 系统中采用的 ELF ( Executable Linkable Format,包括未链接的 .o 文件和可执行文件 )。

6.1 ELF 文件格式解析

       ELF 文件是用在 Linux 系统下的一种目标文件存储格式。典型的目标文件有以下 3 类:
       可重定向文件(Relocatable File):还未经过链接的目标文件。其内容包含经过编译器编译的汇编代码和数据,用于和其他可重定向文件一起链接形成一个可执行文件或者动态库。通常文件扩展名为 .o 。

       可执行文件(Executable File):经过链接器链接,可被 Linux 系统直接执行的目标文件。其内容包含可以运行的机器指令和数据。通常此文件无扩展名。

       动态库文件(Shared Object):动态库文件是共享程序代码的一种方式,其内容和可重定向文件类似,包含可用于链接的代码和程序,可看作多个可重定向文件、动态库一起链接形成一个可执行文件。程序运行时,动态链接器负责在需要的时候动态加载动态库文件到内存。


 +----------------------+                       +----------------------+                 
 |       ELF Header     |                       |       ELF Header     |                 
 +----------------------+--+                    +----------------------+                 
 |        .text         |  |                    | Program Header Table |                 
 +----------------------+  |                    +----------------------+--+              
 |        .rodata       |  |                    |        .dynamic      |  |              
 +----------------------+  |                    +----------------------+  |              
 |        .data         |  |                    |        .hash         |  |              
 +----------------------+  |                    +----------------------+  |              
 |        .bss          |  |                    |        .int          |  |              
 +----------------------+  |                    +----------------------+  |              
 |        .symtab       |  +--> Sections        |        .text         |  |              
 +----------------------+  |                    +----------------------+  |              
 |        .rel.text     |  |                    |        .rodata       |  |              
 +----------------------+  |                    +----------------------+  |              
 |        .rel.data     |  |                    |        .data         |  +--> Segments            
 +----------------------+  |                    +----------------------+  |              
 |        .debug        |  |                    |        .bss          |  |              
 +----------------------+  |                    +----------------------+  |              
 |        .common       |  |                    |        .symtab       |  |              
 +----------------------+  |                    +----------------------+  |              
 |        .strtab       |  |                    |        .fini         |  |              
 +----------------------+--+                    +----------------------+  |              
 | Section Header Table |                       +        .common       |  |              
 +----------------------+                       +----------------------+  |              
                                                |        ...           |  |              
 Relocatable File Format                        +----------------------+  |              
                                                |        .strtab       |  |              
                                                +----------------------+--+
                                                 Executable File Format

       可重定向文件中的节和可执行文件中的段都存储了程序的代码部分、数据部分等,区别是可执行文件中的段结合了多个可重定向文件中的节,且代码部分是经过重定向的最终机器指令。

6.1.1 ELF 文件头

       ELF 文件头描述了一个目标文件的组织,是对目标文件基本信息的描述,包括字的大小和字节序列(尾端)、ELF 文件头的大小、目标文件类型、机器类型、节头表或段头表的大小和数量、程序入口点等

$ readelf -h hello.o 
ELF 头:
  Magic:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          LoongArch
  版本:                              0x1
  入口点地址:              0x0
  程序头起点:              0 (bytes into file)
  Start of section headers:          1048 (bytes into file)
  标志:             0x3, LP64
  本头的大小:       64 (字节)
  程序头大小:       0 (字节)
  Number of program headers:         0
  节头大小:         64 (字节)
  节头数量:         13
  字符串表索引节头: 12

6.1.2 可重定向文件中的节和节头表

       一个可重定向文件中的节头表描述了 ELF 的各个 Section 的信息,比如每个节的名称、长度、在文件中的偏移、读写权限、地址等。

$ readelf -S hello.o 
There are 13 section headers, starting at offset 0x418:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000034  0000000000000000  AX       0     0     4
  [ 2] .rela.text        RELA             0000000000000000  00000248
       0000000000000150  0000000000000018   I      10     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000074
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000074
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  00000078
       000000000000000d  0000000000000000   A       0     0     8
  [ 6] .comment          PROGBITS         0000000000000000  00000085
       000000000000002a  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000af
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000b0
       0000000000000040  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000398
       0000000000000018  0000000000000018   I      10     8     8
  [10] .symtab           SYMTAB           0000000000000000  000000f0
       0000000000000138  0000000000000018          11    11     8
  [11] .strtab           STRTAB           0000000000000000  00000228
       000000000000001c  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  000003b0
       0000000000000061  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

     (1)段名

       常见的段名及其功能描述

 段名 功能描述
 .text 代码段,用于存放程序被编译器编译后的机器指令
 .data 和 .datal 数据段,用于存放程序中已经初始化的全局静态变量和局部静态变量 ?
 .rodata 和 rodatal 只读数据段,用于存只读数据,如 const 类型变量和字符串变量
 .bss 用于存放未初始化的全局变量和局部静态变量 ?
 .common 用于存放编译器的版本信息
 .hash 符号哈希表
 .dynamic 动态链接信息
 .strtab 字符串表,用于保存变量名、函数名等字符串
 .symtab 符号表,用于保存变量、函数等符号值
 .shstrtab 段名表,用于保存段名信息,如 ".text" ".data" 等
 .plt 和 .got 动态链接的跳转表和全局入口表
 .init 和 .fini 程序初始化和终结代码段
 .debug 调试信息

(1.1)变量分散在节、段中验证:

#include <stdio.h>
#define STR     "Hello World!"

int global_val1;
int global_val2 = 0xa;

static int global_static_var3;
static int global_static_var4 = 0xb;

void func() {
        static int local_static_var1;
        static int local_static_var2 = 0xe;
}

int main() {

        static int local_static_var1;
        static int local_static_var2 = 0xd;

        printf("%s\n", STR);
        return 0;
}

 目标文件类型 变量及变量值 段名
可重定向文件        global_val1; 无
 global_val2 = 0xa; .data
 global_static_var3; .bss
 global_static_var4 = 0xb; .data
 local_static_var1.2024; .bss
 local_static_var2.2025 = 0xd; .data
 local_static_var1.2028; .bss
 local_static_var2.2029; .data
 "Hello World!" .rodata
可执行文件     global_val1 = 0; .bss
 global_val2 = 0xa; .data
 global_static_var3 = 0; .bss
 global_static_var4 = 0xb; .data
 local_static_var1.2024 = 0; .bss
 local_static_var2.2025 = 0xd; .data
 local_static_var1.2028; .bss
 local_static_var2.2029; .data
 "Hello World!" .rodata

       未初始化的全局普通变量在可重定向目标文件中是不在段中的,在链接过后,链接器会为其初始化 0 后并放在 .bss;
       未初始化的普通全局变量(可重定向目标文件除外)、静态全局变量、静态局部变量放在 .bss 段,链接后,其初始化 0;
       初始化的普通全局变量、静态全局变量、静态局部变量放在 .data 段;
       为了区分相同名字的变量名,汇编器会将其重命名,例如 local_static_var1.2024、 local_static_var1.2028。

(1)$ gcc -c hello.c

(2)$ readelf -S hello.o 
There are 13 section headers, starting at offset 0x610:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000050  0000000000000000  AX       0     0     4
  [ 2] .rela.text        RELA             0000000000000000  00000428
       0000000000000150  0000000000000018   I      10     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000090
       0000000000000010  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a0
       000000000000000c  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000a0
       000000000000000d  0000000000000000   A       0     0     8
  [ 6] .comment          PROGBITS         0000000000000000  000000ad
       000000000000002a  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000d7
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000d8
       0000000000000068  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000578
       0000000000000030  0000000000000018   I      10     8     8
  [10] .symtab           SYMTAB           0000000000000000  00000140
       0000000000000228  0000000000000018          11    18     8
  [11] .strtab           STRTAB           0000000000000000  00000368
       00000000000000bb  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  000005a8
       0000000000000061  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

(3)$ readelf -s hello.o 

Symbol table '.symtab' contains 23 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 global_static_var3
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 global_static_var4
     7: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 L0^A
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     9: 000000000000001c     0 NOTYPE  LOCAL  DEFAULT    1 L0^A
    10: 0000000000000008     4 OBJECT  LOCAL  DEFAULT    3 local_static_var2.2025
    11: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 local_static_var1.2024
    12: 000000000000000c     4 OBJECT  LOCAL  DEFAULT    3 local_static_var2.2029
    13: 0000000000000008     4 OBJECT  LOCAL  DEFAULT    4 local_static_var1.2028
    14: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
    15: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    5 .LC0
    16: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    17: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    18: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_val1
    19: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_val2
    20: 0000000000000000    28 FUNC    GLOBAL DEFAULT    1 func
    21: 000000000000001c    52 FUNC    GLOBAL DEFAULT    1 main
    22: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

(4)$ readelf hello.o -p 5
String dump of section '.rodata':
  [     0]  Hello World!

(5)$ gcc hello.c -o hello

(6)$ readelf -S hello

(7)$ readelf -s hello

(8)$ readelf hello -p 14
String dump of section '.rodata':
  [     8]  Hello World!

     (2)段类型

       常见的段类型及其含义

 段类型 含义
 NULL 无效段,忽略
 PROGBITS 程序段。.text、data 都属于此类型
 SYMTAB 字符串表段。.strtab 和 .shstrtab 都属于此类型
 RELA 带加数的重定位表段。存放那些代码段和数据
 HASH 符号表的哈希表
 DYNAMIC 动态链接信息
 NOTE 提示性信息
 NOBITS 表示该段在文件中无内容,比如 .bss 段
 REL 不带加数的重定位表段。功能同 RELA 。对应的段名有 .rel.text、.rel.data 等。

     (3)段标志
       段标志(Flag) 在上面显示为旗标,用于表示该段在进程虚拟空间中的访问属性。常见访问属性包括可写(Write)、可执行(Execute)、可分配(Alloc),而所有段都是可读的。标志为空得的代表该段为只读。

     (4)段地址、偏移量和大小
       段地址(在上面段信息中显示为地址):记录了当前段加载到内存后的虚拟起始地址值。尽管理论上进程可以使用 40 位的全部虚拟地址空间,但是一般情况下进程并不能使用全部的虚拟地址空间,系统通常会预留一部分虚拟地址空间用于自身配置。在龙芯平台上,一个进程大概可用的地址空间范围在0x120000000 ~ 0xffffc48000
       偏移量(Offset):用来表示该段在 ELF 文件中的偏移;
       大小(Size):用于表示该段的大小。

     (5)段地址对齐
       如果某段有地址对齐要求,那么段地址对齐(在上面的段信息中显示为对齐)就指定了地址对齐方式。例如 .text 段的对齐值为 4 ,表示 4 字节对齐。当对齐值为 0 和 1 时,表示没有对齐要求。

6.1.3 可执行文件中的段和程序头表

       可重定向文件中描述 Section 属性结构叫段头表(Section Header Table),而可执行文件和动态库文件中描述 Segment 属性结构的叫程序头表(Program Header Table),它指导系统如何把多个端(Segment)加载到内存空间。Segment 可以看作多个可重定向文件(.o 文件)中的相同节( Section )的合并,即一个 Segment 包含一个或多个属性相似的 Section。这里的属性相似更多是指权限(在段标志 Flag 中指定),链接器会把多个 .o 文件中的都具有可执行的 .text 和 .init 段都放在最终可执行文件中的一个 Segment 段内,这样做的好处是节省空间。因为 ELF 文件被加载时以系统页为单位,如果一个 ELF 文件中有 10 个段且每个段的大小都小于一个内存页,那么按一个段占据一个内存页,当前进程就得需要 10 个内存页。如果将相同权限的段合并到一起去映射,那么就小于 10 个页,就可以充分利用内存页,减少内存碎片。

$ readelf -l hello

Elf 文件类型为 EXEC (可执行文件)
Entry point 0x120000580
There are 9 program headers, starting at offset 64

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000120000040 0x0000000120000040
                 0x00000000000001f8 0x00000000000001f8  R      0x8
  INTERP         0x0000000000000238 0x0000000120000238 0x0000000120000238
                 0x000000000000000f 0x000000000000000f  R      0x1
      [Requesting program interpreter: /lib64/ld.so.1]
  LOAD           0x0000000000000000 0x0000000120000000 0x0000000120000000
                 0x0000000000000864 0x0000000000000864  R E    0x4000
  LOAD           0x0000000000003d08 0x0000000120007d08 0x0000000120007d08
                 0x0000000000000368 0x0000000000000380  RW     0x4000
  DYNAMIC        0x0000000000003e40 0x0000000120007e40 0x0000000120007e40
                 0x00000000000001c0 0x00000000000001c0  RW     0x8
  NOTE           0x0000000000000248 0x0000000120000248 0x0000000120000248
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_EH_FRAME   0x0000000000000830 0x0000000120000830 0x0000000120000830
                 0x0000000000000034 0x0000000000000034  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000003d08 0x0000000120007d08 0x0000000120007d08
                 0x00000000000002f8 0x00000000000002f8  R      0x1

 Section to Segment mapping:
  段节...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .plt .text .rodata .eh_frame_hdr 
   03     .eh_frame .init_array .fini_array .dynamic .data .got.plt .got .sdata .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .eh_frame .init_array .fini_array .dynamic 

6.1.4 符号和符号表

       在目标文件中,将函数和变量统称为符号( Symbol )。这里的变量是指不占用函数栈空间的全局变量、静态全局变量或静态局部变量不包括函数内的局部变量。函数名和变量名称为符号名( Symbol Name )。每一个可重定向的目标文件中都会有一个符号表( Symbol Table ),用于记录目标文件中所有到的所有符号、符号名、符号类型、符号大小等信息。有了符号和符号表的存在,编译器在链接阶段才能正确地解析多个目标文件中变量、函数之间的关系配合重定位表来正确地完成重定位,最终正确地将多个目标文件合并在一起形成可执行文件或动态库文件

     (1)符号分为局部符号、全局符号、外部符号和段符号。
     (1.1)局部符号:对应 C 语言函数内部定义的静态局部变量和静态函数,这类符号只在编译单元内部(当前目标文件内部)可见

static int a;
static int fun(){}

     (1.2)全局符号:定义在当前目标文件,但可以被其他文件引用的变量和函数。

int global_var;
int main(){}

     (1.3)外部符号( External Symbol ):在当前目标文件中引用的全局符号。例如 printf 函数(定义在模块 libc 内的符号)或者使用 extern 声明的变量。

     (1.4)段符号:由编译器产生的 .text .data 等段名也称为符号。

     (2)符号所在的段位 .symtab 。可执行目标文件还有一个 .dynsym 用于存放动态符号表。

(1)$ readelf --dyn-syms hello

Symbol table '.dynsym' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.27 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND abort@GLIBC_2.27 (2)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.27 (2)
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     6: 0000000120000780   144 FUNC    GLOBAL DEFAULT   13 __libc_csu_init
     7: 000000012000074c    52 FUNC    GLOBAL DEFAULT   13 main
     8: 0000000120000810     4 FUNC    GLOBAL DEFAULT   13 __libc_csu_fini
 
 (2)$ readelf -S hello
There are 29 section headers, starting at offset 0x4a60:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000120000238  00000238
       000000000000000f  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000120000248  00000248
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000120000268  00000268
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .hash             HASH             0000000120000290  00000290
       0000000000000038  0000000000000004   A       6     0     8
  [ 5] .gnu.hash         GNU_HASH         00000001200002c8  000002c8
       0000000000000030  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           00000001200002f8  000002f8
       00000000000000d8  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           00000001200003d0  000003d0
       0000000000000089  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           000000012000045a  0000045a
       0000000000000012  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          0000000120000470  00000470
       0000000000000020  0000000000000000   A       7     1     8
  [10] .rela.dyn         RELA             0000000120000490  00000490
       00000000000000a8  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             0000000120000538  00000538
       0000000000000018  0000000000000018  AI       6    21     8
  [12] .plt              PROGBITS         0000000120000550  00000550
       0000000000000030  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000120000580  00000580
       0000000000000294  0000000000000000  AX       0     0     16
  [14] .rodata           PROGBITS         0000000120000818  00000818
       0000000000000015  0000000000000000   A       0     0     8
  [15] .eh_frame_hdr     PROGBITS         0000000120000830  00000830
       0000000000000034  0000000000000000   A       0     0     4
  [16] .eh_frame         PROGBITS         0000000120007d08  00003d08
       0000000000000124  0000000000000000  WA       0     0     8
  [17] .init_array       INIT_ARRAY       0000000120007e30  00003e30
       0000000000000008  0000000000000008  WA       0     0     8
  [18] .fini_array       FINI_ARRAY       0000000120007e38  00003e38
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .dynamic          DYNAMIC          0000000120007e40  00003e40
       00000000000001c0  0000000000000010  WA       7     0     8
  [20] .data             PROGBITS         0000000120008000  00004000
       0000000000000010  0000000000000000  WA       0     0     4
  [21] .got.plt          PROGBITS         0000000120008010  00004010
       0000000000000018  0000000000000008  WA       0     0     8
  [22] .got              PROGBITS         0000000120008028  00004028
       0000000000000040  0000000000000008  WA       0     0     8
  [23] .sdata            PROGBITS         0000000120008068  00004068
       0000000000000008  0000000000000000  WA       0     0     8
  [24] .bss              NOBITS           0000000120008070  00004070
       0000000000000018  0000000000000000  WA       0     0     4
  [25] .comment          PROGBITS         0000000000000000  00004070
       0000000000000029  0000000000000001  MS       0     0     1
  [26] .symtab           SYMTAB           0000000000000000  000040a0
       0000000000000648  0000000000000018          27    49     8
  [27] .strtab           STRTAB           0000000000000000  000046e8
       000000000000027a  0000000000000000           0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  00004962
       00000000000000fe  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

(3)$ readelf  hello -p 7

String dump of section '.dynstr':
  [     1]  libc.so.6
  [     b]  puts
  [    10]  abort
  [    16]  __libc_start_main
  [    28]  GLIBC_2.27
  [    33]  __libc_csu_fini
  [    43]  _ITM_deregisterTMCloneTable
  [    5f]  __libc_csu_init
  [    6f]  _ITM_registerTMCloneTable

     (2.1)符号值( Value ):每个符号都有一个对应值,如果此符号是一个函数或变量,其符号值就是函数或变量的虚拟地址。但对于 OBJECT 类型的符号, Value 列表示的是其对齐方式或相对所在段的偏移。

     (2.2)符号大小( Size ):对于变量,符号大小就是数据类型的大小,单位是字节。对于函数,符号大小就是该函数被编译器编译后的所有机器指令占用的的字节数

     (2.3)符号类型( Type ) 分为如下种类:
       NOTYPE:未知符号类型。包括目标文件中用于条件跳转的标签、在外部定义的符号等。
       OBJECT:数据对象,比如 C 语言变量、字符串、数组等。
       FUNC:函数或其他可执行代码。
       SECTION:一个段。
       FILE:文件名。

     (2.4)绑定信息( Bind ) 分为如下种类:
       LOCAL:局部符号。
       GLOBAL:全局符号。
       WEAK:弱引用符号。对于 C/C++ 语言,编译器默认函数和已经初始化的全局变量为强符号,而为初始化的全局变量和使用 attribute((weak)) 定义的变量为弱符号。

     (2.5)VIS :可扩展符号功能,暂未定义其功能,可忽略。

     (2.6)符号所在段( Ndx ):如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标。Ndx 特殊值:
       UND:未定义。通常表示这是个外部符号,故不在本目标文件中定义。例如定义在 libc 库的 printf 函数或者使用 extern 声明的外部变量。
       ABS:表示该符号包含一个绝对值。
       COM:表示该符号是个未初始化的全局变量。

     (2.6)符号名( Name ):符号表的最后一列。有的符号是没有名字的,只能通过编号来识别。没有名字的符号是段(从 Type 列的 SECTION 可以看出)或未知符号。

6.1.5 重定位和重定位表

     (1)重定位包括链接时重定位加载时重定位
       链接时重定位:是在编译器链接阶段将多个可重定位目标文件合并成一个可执行文件时,对文件中所有的程序数据和函数调用指令进行地址确定的过程。链接时重定位不包括对动态库中数据加载和函数调用指令的定位,这个过程要在加载时重定位。
       加载时重定位:针对动态库而言的,在程序运行过程中需要加载动态库时,对所有动态库中函数调用的绝对地址引用进行地址确定的过程

     (2)对于同一个文件内的函数调用,由于函数之间的相对位置是固定的(在链接时同文件内的函数是连续存放的),所以不存在需要重定位的情况。故重定位指的是多个文件之间或多个模块之间(这里模块指动态库或可执行目标文件)存在函数调用和数据引用的处理

/* a.c */
extern void temp();
int main() {
        temp();
}

/* b.c */
int test=0;
void temp() {
        //do nothing
}
$ objdump -d a.o

a.o:     文件格式 elf64-loongarch
Disassembly of section .text:
0000000000000000 <main>:
   0:   02ffc063        addi.d  $r3,$r3,-16(0xff0)
   4:   29c02061        st.d    $r1,$r3,8(0x8)
   8:   29c00076        st.d    $r22,$r3,0
   c:   02c04076        addi.d  $r22,$r3,16(0x10)
  10:   54000000        bl      0 # 10 <main+0x10>
  14:   0015000c        move    $r12,$r0
  18:   00150184        move    $r4,$r12
  1c:   28c02061        ld.d    $r1,$r3,8(0x8)
  20:   28c00076        ld.d    $r22,$r3,0
  24:   02c04063        addi.d  $r3,$r3,16(0x10)
  28:   4c000020        jirl    $r0,$r1,0

$ objdump -d b.o

b.o:     文件格式 elf64-loongarch
Disassembly of section .text:
0000000000000000 <temp>:
   0:   02ffc063        addi.d  $r3,$r3,-16(0xff0)
   4:   29c02076        st.d    $r22,$r3,8(0x8)
   8:   02c04076        addi.d  $r22,$r3,16(0x10)
   c:   03400000        andi    $r0,$r0,0x0
  10:   28c02076        ld.d    $r22,$r3,8(0x8)
  14:   02c04063        addi.d  $r3,$r3,16(0x10)
  18:   4c000020        jirl    $r0,$r1,0

       链接前的可重定位目标文件中的所有段的起始地址都是 0 ,所以上面的 main 和 temp 的起始地址都是 0 ,而相对跳转指令“bl 0”代表要进行函数 temp 的调用,需要链接时函数 temp 地址确定后,重新修改这条指令。

$ objdump -d a.out

a.out:     文件格式 elf64-loongarch

Disassembly of section .text:
...
00000001200006d0 <main>:
   1200006d0:   02ffc063        addi.d  $r3,$r3,-16(0xff0)
   1200006d4:   29c02061        st.d    $r1,$r3,8(0x8)
   1200006d8:   29c00076        st.d    $r22,$r3,0
   1200006dc:   02c04076        addi.d  $r22,$r3,16(0x10)
   1200006e0:   54001c00        bl      28(0x1c) # 1200006fc <temp>
   1200006e4:   0015000c        move    $r12,$r0
   1200006e8:   00150184        move    $r4,$r12
   1200006ec:   28c02061        ld.d    $r1,$r3,8(0x8)
   1200006f0:   28c00076        ld.d    $r22,$r3,0
   1200006f4:   02c04063        addi.d  $r3,$r3,16(0x10)
   1200006f8:   4c000020        jirl    $r0,$r1,0

00000001200006fc <temp>:
   1200006fc:   02ffc063        addi.d  $r3,$r3,-16(0xff0)
   120000700:   29c02076        st.d    $r22,$r3,8(0x8)
   120000704:   02c04076        addi.d  $r22,$r3,16(0x10)
   120000708:   03400000        andi    $r0,$r0,0x0
   12000070c:   28c02076        ld.d    $r22,$r3,8(0x8)
   120000710:   02c04063        addi.d  $r3,$r3,16(0x10)
   120000714:   4c000020        jirl    $r0,$r1,0
...

       从上面可以看到,“ bl 0 ” 变成了 “ bl 28(0x1c)”,0x1c = temp - PC(0x1200006e0)。

       编译器的链接过程最主要的两件事是地址分配重定位
       地址分配:过程就是处理所有输入文件(这里指的是 a.o 和 b.o),获取所有的符号信息、段长度、属性等信息,并以此为依据将相同属性的段合并和确定符号的地址,例如确定 main 和 temp 函数的起始地址确定。
       重定位:在地址分配的基础上对数据加载指令或函数调用指令做地址确定并修改指令,例如“ bl 0 ” 修改为 “ bl 28(0x1c)”

       并不是所有的数据加载指令或函数调用指令都需要修改。那么哪些指令需要修改,如何修改呢?这就需要目标文件中的重定位表。在可重定位的目标文件中,每一个需要地址修正指令所在的段,都会对应一个重定位段。例如目标文件 a.o 中的代码段 .text 里面需要修正的指令 bl,那么 a.o 中就会有一个 .rel.text 的段,段内记录了需要进行地址修正的指令所在位置、修正方法、修正后的符号名称等信息。

# objdump -r a.o

a.o:     文件格式 elf64-loongarch

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000010 R_LARCH_SOP_PUSH_PLT_PCREL  temp
0000000000000010 R_LARCH_SOP_POP_32_S_0_10_10_16_S2  *ABS*

       龙芯架构 ELF psABI 规范 重定位类型


$ objdump -d a.o
       10:   54000000        bl      0 # 10 <main+0x10> => 
$ objdump -d a.out
1200006e0:   54001c00        bl      28(0x1c) # 1200006fc <temp> ?
...
00000001200006fc <temp>:
   1200006fc:   02ffc063        addi.d  $r3,$r3,-16(0xff0)
   120000700:   29c02076        st.d    $r22,$r3,8(0x8)
   120000704:   02c04076        addi.d  $r22,$r3,16(0x10)
   120000708:   03400000        andi    $r0,$r0,0x0
   12000070c:   28c02076        ld.d    $r22,$r3,8(0x8)
   120000710:   02c04063        addi.d  $r3,$r3,16(0x10)
   120000714:   4c000020        jirl    $r0,$r1,0

(1) "bl      0" => "bl      28(0x1c)", 0x1c ?
    0x1c <=> R_LARCH_SOP_PUSH_PLT_PCREL <=> push(PLT - PC) <=> 0x1200006fc - 0x1200006e0
    gcc -S -O2 a.c b.c

(2) "54000000" => "54001c00" ?

                   31          26 25                            10 9                       0
 +----------------------------------------------------------------------------------------+
 | BL    offs26    | 0 1 0 1_0 1 |             offs[15:0]         |       offs[25:16]     |
 +----------------------------------------------------------------------------------------+
                        0x54

 BL offs26 <=> $ra = PC + 4; PC = PC + offs26 << 2

 R_LARCH_SOP_POP_32_S_0_10_10_16_S2 <=> opr1 = pop (), 
 (*(uint32_t *) PC) [9 ... 0] = opr1 [27 ... 18], 
 (*(uint32_t *) PC) [25 ... 10] = opr1 [17 ... 2]

 opr1 [27 ... 0] <=> 0x000001c <=> 0000_0000_0000_0000_0000_0001_1100
 *PC [9 ... 0] = opr1 [27 ... 18] = 0000_0000_00 = offs[25:16]
 *PC [25 ... 10] = or1 [17 ... 2] = 00_0000_0000_0001_11 = offs[15:0]

                   31          26 25                              10 9                    0
 +----------------------------------------------------------------------------------------+
 | BL    offs26    | 0 1 0 1_0 1 | 0 0_0 0 0 0_0 0 0 0_ 0 0 0 1_1 1 | 0 0_0 0 0 0_0 0 0 0 |
 +----------------------------------------------------------------------------------------+
                   0x   5        4        0       0        1        c        0       0
 
 "bl      28(0x1c)" <=> "bl      0x7 << 2"

6.2 进程虚拟地址空间和页大小

       龙芯架构参考手册中描述,应用软件能够访问的内存物理地址空间范围是 0 ~ 2PALEN - 1。LA32 ,PALEN 理论上是一个不大于 32 的正整数,通常建议为 32;LA64 ,理论上是一个不大于 64 的正整数,由实现决定其具体的值,通常 PALEN 在[40, 48]。应用软件可以通过 CPUCFG 指令读取 0x1 号配置字的 PALEN 域来确定 PALEN 的具体值。龙芯 3A5000 处理器上是 48 ,即支持的内存物理地址空间范围是 0 ~ 248 - 1。当程序中访存指令的地址超出上述范围时,将触发异常

              +----------------------+
High_Address  |        Reserve       | <--- 系统保留
      |       +----------------------+
      |       |         Stack        |
      |       +----------------------+
      |       |          .so         | <--- 共享库
      |       +----------------------+
      |       |                      |
      |       |                      |
      |       |                      |
      |       |                      |
      |       |                      |
      |       |         Heap         |
      |       |                      |
      |       |                      |
      |       |                      |
      |       |                      |             
      |       |                      |             
      |       +----------------------+             
      |       |        .data         |
      |       +----------------------|             
      |       |         .bss         |             
      V       +----------------------+ <--- 0x120000000           
Low_Address   |        Reserve       | <--- 系统保留            
              +----------------------+

$ getconf PAGE_SIZE
16384

6.3 可执行文件与进程虚拟地址空间的映射

       系统启动一个进程时,首先要做的就是加载可执行文件中的数据到内存,然后才能运行。但并不是所有的数据都会被加载,一个典型的可执行文件中,只有程序头中记录的类型为 LOAD 的段才需要被加载到内存,其他段不需要加载到内存(仅用于辅助判断)。

       实际开发中,可以通过 “/proc/pid/maps” 节点来查看一个进程的虚拟地址空间布局,其中 pid 为待查看的进程号。


第七章 编写 LoongArch 汇编源程序

7.1 汇编源程序 .s 文件和 .S 文件

       扩展名为 .s 文件中仅包含和 CPU 架构相关的汇编指令、和汇编器相关的汇编器指令、注释等。
       扩展名为 .S 文件通常是程序员编写的汇编源文件,此文件除了包括 .s 文件所有内容,还可以有 C 语言的宏定义和预处理命令(以“#”开头的语句)等,这部分内容是汇编器无法处理的,需要 GCC 工具(具体是 cc1 )来完成预处理后再交由汇编器处理。

+-------------------------------------+
| +-----------+         +-----------+ |        +-----------+        +-----------+            +-----------+  
| |  hello.c  |   cc1   |           | |  cc1   |           |   as   |           |  collect2  |           |
| |  stdio.h  |-------->|  hello.i  |-+------->|  hello.s  |------->|  hello.o  |----------->|   hello   |      
| |   ...     |         |           | |   ^    |           |        |           |     ^      |           |
| +-----------+         +-----------+ |   |    +-----------+        +-----------+     |      +-----------+
+-------------------------------------+   |                                           |
                                          |                                           |
                                          |                                     +-----+-----+ 
                                          |                                     |   crtl.o  |
                                          |                                     |   crti.o  |
                                          |                                     | crtbegin.o|
                                          |                                     | crtend.o  |
                                          |                                     |   crtn.o  |
			+-----------+                 |                                     +-----------+
			|           |                 |
			|  hello.S  |-----------------+
			|           |                
			+-----------+

7.2 汇编源文件中的汇编器指令

       汇编指令是机器指令的易读版,所以汇编指令和机器指令一一对应,在 GCC 编译的汇编阶段,汇编器会将汇编指令翻译成机器指令并存放到目标文件中。汇编器指令和汇编指令完全不同,汇编器指令是为汇编器而生的,是用于指导汇编器如何定义变量和函数、汇编指令在目标文件中如何存放等。即汇编器指令是指导汇编器工作的指令

7.2.1 符号定义相关的汇编器指令

     (1)定义一个字符串变量
       在汇编源程序中定义一个字符串变量需要包括的完整信息有字符串名称、字符串内容、字符串大小、符号类型、对齐方式、变量作用域、变量所在段等

// C 语言
char str[10] = "hello";

//汇编器指令
	.globl	str				# 指定符号 str 的作用域为全局
	.data					# 指定符号 str 所在段为 .data
	.align	3				# 指定符号 str 为 8 字节对齐
	.type	str, @object	# 指定符号 str 的类型为对象
	.size	str, 10			# 指定符号 str 大小为 10 字节

str:					# 指定符号名称为 str
	.ascii	"hello\0"		# 指定符号 str 的内容

//等价的
.ascii	"hello\0"
.asciz	"hello" 
.string "hello"

汇编器指令.string 是区分字符宽度的,sting8、sting16、string32、string64。

     (2)定义一个整型变量
       和定义一个字符串变量类似,需要的信息包括变量名称、变量值、变量大小、变量类型、对齐方式、作用域和变量所在段等

// C 语言
static int int_v = 20;

//汇编指令
	.data
	.align 2
	.type int_v, @object
	.size int_v, 4	# 变量大小

int_v:
	.word 20

.byte、.half、.word、.dword value,value 为变量值。

     (3)定义一个函数

// C 语言
int add(int a, int b){
return a+b;
}

// 汇编指令
	.text
	.align 2
	.globl add
	.type add, @function

add:
	add.w $a0, $a0, $a1
	jr $r1
	.size add, .-add

     (4)符号定义相关的汇编器指令说明
     (4.1)设置符号类型
       定义符号类型的汇编器指令为 .type ,其后面常跟的类型有 @function 和 @object,分别表示当前符号为函数和变量。

.type 	add, @function 		# 符号 add 的类型为函数
.type 	v1, @object 		# 符号 v1 为变量(对象)

     (4.2)设置符号大小
       汇编器指令“.size name, expression”用于设置符号的大小,name 为符号名称。当设置变量是,expression 为一个正整数;当设置函数大小时,expression 通常为“.-name”表达式。

.size 	short_v, 2 		# 设置变量 short_v 的大小为 2 字节
.size 	main, .-main 	# 设置函数 main 的大小为当前位置减去 main 起始地址

     (4.3)指定符号对齐方式
       汇编器指令“.align expr”用于指定符号的对齐方式,expr 为正整数,用于指定接下来的数据在目标文件中存放地址的对齐方式。不同架构下 expr 代表的意思不同, .align 4 在 LoongArch 中为 2 的 4 次方即 16 字节。如果考虑可移植性问题用 .balign 和 .p2align。

     (4.4)指定符号的作用域
       和 C 语言一样,在汇编源文件中定义一个变量或函数符号时也要声明其作用域,用于标识当前符号的作用范围。默认情况下不指定当前符号作用域,符号作用域为当前汇编源文件内可见。
       “.globl symbol” 用于指定符号 symbol(通常为一个全局变量或者非静态成员函数)为全局可见,即对链接器(ld)中其他源文件可见。 .globl 等价于 .global
       “.common symbol” 声明一个通用符号(Common Symbol)。这里的通用符号可理解为 C 语言中未初始化的全局变量。在多个汇编源文件中出现的同名通用符号,在编译器的链接阶段可能会被合并,合并的结果是保留占用空间最大的一个。
       “.local symbol”用于声明一个类似 C 语言中的未初始化的局部静态变量定义。

7.2.2 逻辑控制相关的汇编器指令

       这里的逻辑控制包括指定符号数据存放段、常量设置和条件编译、本地标签和程序跳转、编译调试、文件引用、循环展开、宏定义等。

     (1)指定符号数据存放段
       使用“.data subsection” “.text subsection”等汇编器指令分别指定接下来的语句要存放在目标文件的数据段和代码段。当需要指定更精细的段类型时,可以使用“.section name”。

	.data # 指定接下来的数据存放到目标文件的数据段
str:
	.ascii "hello\0"

	.text # 指定接下来的数据存放到目标文件的代码段
add:

	.section .rodata # 指定接下来的数据存放到目标文件的 .rodata 段。
str:
	.ascii "hello\0"

     (2)常量设置和条件编译
       汇编器指令 “.set(.equ) symbol,expression” 可用于常量设置,类似 C 语言中的宏定义,可以配合汇编指令 .if、.else、.endif 使用,从而一起完成一些条件编译。

	.set FLAG,0
.LC0:
	.ascii "Hello 1!\000"
.LC1:
	.ascii "Hello 2!\000"
main:
.if FLAG == 1
	la.local $r4, .LC0
.else
	la.local #r4, .LC1
.endif
	bl %plt(puts)
命令 功能
 .ifdef        symbol 如果符号 symbol 已经被定义过,汇编接下来的代码
 .ifndef        symbol 如果符号 symbol 没定义过,汇编接下来的代码,等价于 .ifnotdef symbol
 .ifc        string1,sting2 如果两个字符串相同,汇编接下来的代码,等价于 .ifeqs string1,string2
 .ifnc        string1,string2 如果两个字符串不相同,汇编接下来的代码
 .ifeq        expression 如果 expression == 0 ,汇编接下来的代码
 .ifge        expression 如果 expression >= 0 ,汇编接下来的代码
 .ifgt        expression 如果 expression > 0 ,汇编接下来的代码
 .ifle        expression 如果 expression <= 0 ,汇编接下来的代码
 .iflt        expression 如果 expression < 0 ,汇编接下来的代码

     (3)本地标签和程序跳转
       为了方便程序的编写,汇编器指令中提供一种本地标签(Local Label),用于逻辑跳转。本地标签可采用编号(可以为数字、字母、特殊字符或其组合)加冒号“:”的格式

1: branch 1f # 向后跳转到第 3 条位置
2: branch 1b # 向后跳转到第 1 条位置
1: branch 2f # 向后跳转到第 4 条位置
2: branch 1b # 向后跳转到第 3 条位置

     (4)编译调试
       “.print string”会让汇编器在标准输出上输出一个字符串。
       “.fail expression”会生成一个错误(error)或警告(warning),当 expression >= 500 时,warning,当 expression < 500 时,error, expression 默认为0,可直接写成“.fail”。

.print	"this is a test for print" 	# 输出信息:this is a test for print
.fail 499 							# 输出信息:warning: .fail 500 encountered
.fail  								# 输出信息:error: .fail 0 encountered
.err  								# 输出信息:error: .err encountered
.error	"error happen"  			# 输出信息:error: error happen

     (5)文件引用
       “.include “file””,默认引用文件路径为当前目录,当被引用文件的路径不在同目录时,可以通过汇编器的命令行选项参数“-I”来控制搜索路径;
       “#include”,这时需要汇编器文件必须是 .S,且要通过 GCC 工具(具体为工具 cc1)进行预处理。

#ref.S
	.text

add:
	add.d $r4, $r5, $r4
	jr $r1

#main.S
	.include "ref.S"

     (6)循环展开
       汇编器指令“.rept count”和“.endr”可用于将其内部的语句循环展开 count 次。

.rept 3
nop
.endr

相当于目标文件中生成 3 条 nop 指令:
nop
nop
nop

       汇编指令“.irp symbol,values …”,实现用 values 替代 symbol 的语句序列,也以 .endr 为结尾。指令中使用 symbol 的格式为“\symbol”。

.irp n,4,5,6,7,8
st.d $r\n, $sp,\n*8
.endr

st.d $4 $sp,32
st.d $5 $sp,40
st.d $6 $sp,48
st.d $7 $sp,56
st.d $8 $sp,64

     (7)宏定义
汇编器指令“.macro name args”功能上类似 C 语言中宏定义功能,其中 name 为宏名称,args 为参数,以 .endm 结尾。.macro 的参数可以为 0,也可以为多个参数,当参数为多个时,参数之间可以用逗号或空格分隔。

.text
.macro INSERT_NOP a
.rept \a
nop
.endr
.endm

# 使用
INSERT_NOP 8

       更多汇编器指令可参考:
       GAS手册
       Hexagon Binutils GNU 手册

7.3 汇编源文件中的汇编指令

7.3.1 汇编指令

汇编源文件中要求在寄存器前面都有符号“$”,也支持使用寄存器别名的指令汇编方式,例如“add.w $a0, $a1, $a2”。

7.3.2 汇编宏指令

       任何一个完善的体系结构生态都会提供丰富的宏指令,从而尽量向开发者屏蔽目标文件中一些功能不直观的汇编指令用法,或屏蔽一些符号重定位等方面的细节问题,为开发者快速编写汇编程序提供方便。

     (1)空指令

nop <=> andi $r0, $r0, 0x0`

     (2)立即数加载宏指令

li.w	rd, imm32
li.d	rd, imm64

见 3.1.1 算术运算指令

     (3)地址加载宏指令

la.local	rd, label
la.global	rd, label

la.local	$r4, 0 <=> pcaddu12i	$r4, 0
					   addi.d		$r4,$r4,0

     (4)跳转宏指令

jr 		rd # jirl $r0, $r1, 0
bgt 	rj, rd, label
ble 	rj, rd, label
bgtz 	rj, label
blez 	rj,label

7.4 汇编源程序实例文件 hello.S

        .data
.LC0:           # 本地标签,指定了字符串“Hello World!\0”的地址
        .ascii "Hello World!"
        .text
        .align  2
        .globl  main
        .type   main, @function
main:           # 本地标签,指定了函数 main 的开始
        addi.d          $sp, $sp, -8
        st.d            $ra, $sp, 0
        la.local        $r4, .LC0
        bl                      %plt(puts)
        li.w            $a0, 0
        ld.d            $ra, $sp, 0
        addi.d          $sp, $sp, 8
        jr                      $ra
        .size           main, .-main
        .section .note.GNU-stack,"",@progbits

$ gcc hello.S -o hello

7.5 没有函数栈的汇编程序

/* myadd.c */
int myadd(int a, int b) {
        return a+b;
}

$ gcc -S myadd.c

        .file   "myadd.c"
        .text
        .align  2
        .globl  myadd
        .type   myadd, @function
myadd:
.LFB0 = .
        .cfi_startproc
        addi.d  $r3,$r3,-32
        .cfi_def_cfa_offset 32
        st.d    $r22,$r3,24
        .cfi_offset 22, -8
        addi.d  $r22,$r3,32
        .cfi_def_cfa 22, 0
        or      $r13,$r4,$r0
        or      $r12,$r5,$r0
        slli.w  $r13,$r13,0
        st.w    $r13,$r22,-20
        slli.w  $r12,$r12,0
        st.w    $r12,$r22,-24
        ld.w    $r13,$r22,-20
        ld.w    $r12,$r22,-24
        add.w   $r12,$r13,$r12
        or      $r4,$r12,$r0
        ld.d    $r22,$r3,24
        .cfi_restore 22
        addi.d  $r3,$r3,32
        .cfi_def_cfa_register 3
        jr      $r1
        .cfi_endproc
.LFE0:
        .size   myadd, .-myadd
        .ident  "GCC: (Loongnix 8.3.0-6.lnd.vec.34) 8.3.0"
        .section        .note.GNU-stack,"",@progbits

$ gcc -O2 -S myadd.c

        .file   "myadd.c"
        .text
        .align  2
        .align  4
        .globl  myadd
        .type   myadd, @function
myadd:
.LFB0 = .
        .cfi_startproc
        add.w   $r4,$r4,$r5
        jr      $r1
        .cfi_endproc
.LFE0:
        .size   myadd, .-myadd
        .ident  "GCC: (Loongnix 8.3.0-6.lnd.vec.34) 8.3.0"
        .section        .note.GNU-stack,"",@progbits

       编写汇编程序时,因为我们已经知道此函数不破坏任何寄存器,所以可以取消函数栈的分配、释放必要寄存器的保存整体实现上仅仅用了两条指令。可以看出,编写汇编程序确实可以给性能优化带来很大的想象空间(-O2,GCC做了此工作)。


第八章 内嵌汇编

8.1 内嵌汇编基本格式

	asm asm-qualifiers (
		"Assembler Template"	//汇编模板
		: OutputOperands		//输出操作数
		: InputOperands			//输入操作数
		: Clobbers				//破坏描述
	);

     (1)内嵌汇编以 asm() 格式表示,括号里面分为 4 个部分:
       Assembler Template:汇编模板,里面包含 0 条或者多条内嵌汇编指令;
       OutputOperands:输出操作数,可以有 0 个或者多个;
       InputOperands:输入操作数,可以有 0 个或者多个;
       Clobbers:破坏描述,可以没有或有多个;
       各个部分之间使用“:”分隔。
       asm-qualifiers 为 asm() 的限定符,可以为空,或者是 volatiles、inline、goto 中的任意一个。

       (2)汇编模板部分是必不可少的,但可以为空,即 " "。也可以有一条或多条内嵌汇编指令,每条指令都以双引号 " " 为单位,以 \n\t 结尾或者换行来分隔。

#include <stdio.h>
int main() {
        int a = 1, b = 2, ret;
        asm("");
        asm("add.w %0, %1\n\t"
            "add.w %0, %2\n\t"
            :"=r"(ret)
            :"r"(a),"r"(b)
        );
        printf("ret = %d\n", ret);
        return 0;
}
没有显示使用寄存器,破坏描述部分可以省略。

$ gcc main.c
/tmp/cchEYjPD.s: Assembler messages:
/tmp/cchEYjPD.s:30: 致命错误:no match insn: add.w      $r12,$r12

add.w 是三操作数指令,汇编器没有做2操作数缩写兼容。
#include <stdio.h>
int main() {
        int a = 1, b = 2, ret;
        asm("add.w %0, %1, %2\n\t"
            :"=r"(ret)
            :"r"(a),"r"(b)
        );
        printf("ret = %d\n", ret);
        return 0;
}

       (3)如果内嵌汇编中只有内嵌汇编指令,不需要输出操作数、输入操作数和破坏描述时,后面的 ":"都可以省略,asm <=> asm

asm("break 0"); <=> __asm__("break 0");

       (4)如果内嵌汇编中仅使用了后面部分,其前面部分为空,那么前面部分也需要使用 “:” 分隔。

asm("move $4, %0\n\t"
    :
    :"r"(a)
);

8.1.1 输入操作数和输出操作数

     (1)在内嵌汇编格式中的输入操作数和输出操作数里,每一个操作数都由一个带双引号 “” 的约束字符串和一个带括号的 C 语言表达式或变量组成。当有多个输入或输出操作数时,多个操作数之间用 “,” 分隔。内嵌汇编指令中对输入和输出操作数统一编号,使用 %num 的形式依次表示每一个操作数,num 为正数,从 0 开始。

ret = a + b;

asm("add.w %0, %1, %2\n\t"
    :"=r"(ret)
    :"r"(a),"r"(b)
);

     (2)每个操作数前面的约束字符或者字符串表示对后面 C 语言表达式或变量的限制条件。编译器会根据这个约束条件来决定相应的处理方式。比如 “=r”(ret) 中的 “=r” 表示有两个约束条件,“=”表明此操作数是输出操作数,所以在输入操作数列表中不可能出现此约束符;“r”表明对此操作数要分配寄存器,即和某个寄存器做关联。

     (3)输入操作数通常是 C 语言的变量,但是也可以是 C 语言表达式。

asm("move %0, %1\n\t"
    :"=r"(ret)
    :"r"(&src+4)
);

     (4)输出操作数也可以有多个,且为多个时,每个输出操作数都要用 “=”来标识自己。

unsigned long count = 0;
int count_id = 0;
asm("rdtime.d %0, %1\n\t"
    :"=r"(count), "=r"(id)
);

     (5)默认情况下输出操作数权限是只写的,但是编译器不会做检查。这个特性会给编程带来麻烦,例如误把输出操作符当右值来操作,编译阶段不报错,但是程序运行后无法得到想要的结果。

#include <stdio.h>
int main() {
        int a = 1, ret = 0;
        asm("add.w %0, %0, %1\n\t"
            :"=r"(ret)
            :"r"(a)
        );
        printf("ret = %d\n", ret);
        return 0;
}
$ ./a.out 
ret = 2

#include <stdio.h>
int main() {
        int a = 1, ret = 0;
        asm("add.w %0, %0, %1\n\t"
            :"+r"(ret)
            :"r"(a)
        );
        printf("ret = %d\n", ret);
        return 0;
}
# ./a.out 
ret = 1

#include <stdio.h>
int main() {
        int a = 1, ret = 0;
        asm("add.w %0, %1, %2\n\t"
            :"=r"(ret)
            :"0"(ret), "r"(a)
        );
        printf("ret = %d\n", ret);
        return 0;
}
# ./a.out 
ret = 1
这里数字限制符“0”的意思是输入操作数 ret 和第 0 个输出操作数使用同样的地址空间。数字限制符只能用在输入操作数部分,而且必须指向某个输出操作数。

8.1.2 破坏描述

       内嵌汇编中的破坏描述部分用于声明那些在汇编指令部分有写操作的寄存器或内存,用于通知编译器这些寄存器或内存在此内嵌汇编中会被破坏(被写),需要提前做好上栈保存,并在内嵌汇编中指令完成后做给旧值恢复。破坏描述部分有两种声明方式:声明寄存器声明 memory

     (1)破坏描述寄存器
       如果内嵌汇编中的操作数有多个指定寄存器被破坏,那么建议对所有被修改的寄存器在破坏描述部分做声明。声明多个寄存器时,寄存器之间使用“,”分开。

asm"add.d $a3, $a1, $a2\n\t"
     "move $v0, %0\n\t"
     :"=r"(ret)
     :"r"(a),"r"(b)
     :"$a3", "$v0"
);

     (2)破坏描述 memory
       通常 GCC 编译器会对程序的指令生成做一个优化,例如会在保证程序正确性的前提下,尽可能地利用寄存器作为缓存来减少访存指令的生成。

a += 1;
b += a;

ldptr.w 	$r12, $r4, 0 		# 从内存加载变量 a 的值
addi.w 		$r12, $r12, 1(0x1) 	# 加法运算
stptr.w 	$r12, $r4, 0 		# 将结果写回变量 a 所在内存
ldptr.w 	$r13, $r5, 0 		# 从内存加载变量 b 的值
add.w 		$r12, $r13, $r12 	# 加法运算(复用 r12 的结果)
stptr.w 	$r12, $r5, 0 		88# 将结果写回变量 b 所在内存

a += 1;
asm volatile ("":::"memory");
b += a;

ldptr.w		$r12, $r4, 0	# 从内存加载变量 a 的值
addi.w 		$r12, $r12, 1(0x1)
stptr.w 	$r12, $r4, 0
ldptr.w 	$r12, $r5, 0
ldptr.w 	$r13, $r4, 0 	# 再次从内存加载变量 a 的值
add.w 		$r12, $r12, $r13
stptr.w 	$r12, $r5, 0 

       破坏描述 memory 的功能可描述为:通知编译器,asm 中可能对操作数做了修改(写操作),所以在 asm 前后不要对访存相关的语句做任何的值假设(优化),而是要实时刷新内存,即要把寄存器数据写入内存或从内存重新读取最新数据,以便获取内存中的最新值。

/* test.c */
#include <stdio.h>
int main() {
        int dest = 1, add_value = 2, old_value;

        asm volatile ("amadd.w %0, %1, %2\n\t"
                      : "=&r" (old_value)
                      : "r" (add_value), "r" (&dest)
                      :
        );
        printf("old_value=%d, dest=%d\n", old_value, dest);
        return 0;
}

$ gcc test.c
$ ./a.out 
old_value=1, dest=3

$ gcc -O2 test.c
$ ./a.out 
old_value=1, dest=1

$ gcc -O2 -S test.c
.LC0:
        .ascii  "old_value=%d, dest=%d\012\000"
        addi.w  $r12,$r0,1                      # dest = 0x1
        st.w    $r12,$r3,12
        addi.d  $r13,$r3,12						# $r13 = &dest
        addi.w  $r12,$r0,2                      # 0x2
        amadd.w $r5, $r12, $r13					# $r5 = 1, (*$r13) = dest = 0x3
        addi.w  $r6,$r0,1                       # $r6 = 0x1
        la.local        $r4,.LC0
        bl      %plt(printf)

/* test1.c */
#include <stdio.h>
int main() {
        int dest = 1, add_value = 2, old_value;

        asm volatile ("amadd.w %0, %1, %2\n\t"
                      : "=&r" (old_value)
                      : "r" (add_value), "r" (&dest)
                      : "memory"
        );
        printf("old_value=%d, dest=%d\n", old_value, dest);
        return 0;
}

$ gcc -O2 -S test1.c
.LC0:
        .ascii  "old_value=%d, dest=%d\012\000"
        addi.w  $r12,$r0,1                      # dest = 0x1
        st.w    $r12,$r3,12
        addi.d  $r13,$r3,12						# $r13 = &dest
        addi.w  $r12,$r0,2                      # 0x2
        amadd.w $r5, $r12, $r13					# $r5 = 1, (*$r13) = dest = 0x3
        ld.w    $r6,$r3,12						# $r6 = (*$r13) = dest = 0x3
        la.local        $r4,.LC0
        bl      %plt(printf)

$ gcc -O2 test1.c
$ ./a.out 
old_value=1, dest=3

8.1.3 有名操作数

       从 GCC 的 3.1 版本开始,内嵌汇编支持有名操作数,即可以在内嵌汇编中为输入操作数、输出操作数取名字,名字形式是 [name],其中的 name 可以是大小写字母、数字、下划线等,且放在每个操作数的前面。在汇编指令中使用有名操作数的形式为 %[name]。

asm("add.d %[out], %[in1], %[in2]\n\t"
    :[out]"=r"(ret)
    :[in1]"r"(a), [in2]"r"(b)
);

8.2 约束字符

       约束字符就是放在输入和输出操作数前面的修饰符,用以说明操作数的类型和读写权限等
       “=”:用来修饰输出操作数,表示该操作数为可写,先前的值将被丢弃且由输出数据替换。
       “+”:用来修饰输出操作数,表示该操作数为可读可写。
       “r”:表示该操作数是整型变量(用于修饰 C 语言中 short、int、long 等),请求分配一个通用寄存器。
       “f”:请求分配一个浮点寄存器,用于修饰 C 语言中浮点变量( float 或 double 类型)。

float ret = (float)a + (float)b;

float ret = 0;
asm("fadd.s %0, %1, %2\n\t"
    : "f"(ret)
    : "f"(a), "f"(b)
);

       “I(i)”:表示该操作数是有符号的 12 位常量。当常量操作数小于 12 位时,可以使用此约束符。

asm("addi.w %0, %1, %2\n\t"
    : "=r"(ret)
    : "r"(a), "I"(10)
);

       “l”:表示该操作数时有符号的 16 位常量。当常量大于 12 位但小于 16 位时,可以考虑使用此约束符。
       “K”:表示该操作数时无符号的 12 位常量。当操作数为负数时,只能用 “I” 或 “l” ,使用此约束符会报错。
       “J”:表示该操作数时整数零。
       “G”:表示该操作数时浮点数零。
       “&”:表示使用该操作数的内存地址,且可被修改。
       “m”:内存操作数,用于访存指令的地址加载和存储,常用于修饰 C 语言中指针类型。

int *p = &a;
asm("ld $t0, %0 \n\t"
    :
    : "m"(p)
    : "$t0"
);

8.3 限制符 volatile

       使用 volatile 限制符的内嵌汇编在任何情况下都不会被 GCC 编译器优化。GCC 编译器中内置了很多优化功能,例如当检测到 asm() 中的输出操作数没有被当前程序的上下文使用时,就会认为这段内嵌汇编是多余的并删除它;或者检测到循环体内部的内嵌汇编总是返回相同的结果时,就把这段内嵌汇编移动到循环体外部。当这些优化情况并不是你所期望的时候,可以使用 volatile 限制符通知编译器关闭这些优化。

asm volatile("break 0 \n\t");

       如果此内嵌汇编没有 volatile 限制符的存在,内嵌汇编内部仅有一条 “break 0“ 指令,没有和当前方法中的任何有意义的变量做关联(即认为没有任何数据依赖关系),那么 GCC 极大可能优化掉这条指令。

8.4 脱离 libc 库的最“小”程序示例

       绝大多数情况下,我们编写的程序依赖很多系统库才能运行,其中 libc 库为必不可少的基础库。例如 hello.c,调用 printf 函数输出“Hello World!”,编译过程必不可少的就有 crt1.o、crti.o、crtn.o、crtbegin.o、crtend.o 和 libc 库的参与。如果程序使用了动态链接,那么还需要 ld 库帮助在程序运行时实现其他动态库的加载。

8.4.1 编写主程序

/* main.c */

#define STR     "Hello World! \n"

/* sys_write(unsigned int fd, const char __user *buf, size_t count) */
void printf(char *str, int len) {
        asm(
                "li.w   $r11, 64 \n\t"  // sys_write 的系统调用号是 64,放在 r11
                "li.w   $r4, 1 \n\t"    // 参数 1 :stdout 文件描述符是 1
                "move   $r5, %0 \n\t"   // 参数 2 :字符串地址
                "move   $r6, %1 \n\t"   // 参数 3 :字符串长度
                "syscall        0 \n\t" // 系统调用指令
                :
                : "r"(str), "r"(len)
                : "$r11", "$r4", "$r5", "$r6"
        );
}

/* sys_exit(int error_code); */
void exit() {
        asm(
                "li.w   $r11, 93 \n\t"  // sys_exit 的系统调用号是 93,放在 r11
                "li.w   $r4, 0 \n\t"    // 参数 1:进程退出状态码 0
                "syscall        0 \n\t"
                ::: "$r11", "$r4"
        );
}

int main() {
        printf(STR, 14);
        exit();
}

8.4.2 链接脚本

/* ld.lds */
OUTPUT_ARCH(loongarch) //指定体系架构为 LoongArch
ENTRY(main) //指定程序入口函数为 main
SECTIONS //链接脚本主体,里面包含 SECTIONS 的变换规则。
{
        /*
         *将当前程序加载到内存的起始虚拟地址设置为 0x120000000 + SIZE_HEADERS。
         *“.”代表起始虚拟地址,SIZEOF_HEADERS 为输出文件头(EFI 头 + 程序头 + 节头)大小。
         *链接脚本里面的语句分为赋值语句和命令语句,OUTPUT_ARCH 和 ENTRY 就属于命令语句,可以用换行代替“;”,赋值语句必须使用“;”结尾。
         */
        . = 0x120000000 + SIZEOF_HEADERS;
        /* 段转换规则 */
        .text : {
                *(.head.text)
                *(.text*)
        }

        .rodata : {
                *(.rodata*)
                *(.got*)
        }

        .data : {
                *(.data*)
                *(.bss*)
                *(.sbss*)
        }
        /* 以下段不保存到输出文件中 */
        /DISCARD/ : {
        *(.comment)
        *(.pdr) /* debug used */
        *(.options)
        *(.gnu.attributes)
        *(.debug)
        *(.eh_frame) /* add */
        }
}

8.4.3 程序的运行

$ gcc -c -fno-builtin main.c // -c:编译、汇编生成目标文件(.o),不进行链接
$ ld -T ld.lds main.o -o main //-fno-builtin:关闭 GCC 内置函数功能,-T 指定链接脚本,否则使用系统默认的链接脚本。
$ ./main
Hello World!

/* SIZEOF_HEADERS */
$ readelf -S main
There are 6 section headers, starting at offset 0x260:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         00000001200000b0  000000b0
       00000000000000ac  0000000000000000  AX       0     0     4
  [ 2] .rodata           PROGBITS         0000000120000160  00000160
       000000000000000f  0000000000000000   A       0     0     8
  [ 3] .symtab           SYMTAB           0000000000000000  00000170
       00000000000000a8  0000000000000018           4     4     8
  [ 4] .strtab           STRTAB           0000000000000000  00000218
       0000000000000019  0000000000000000           0     0     1
  [ 5] .shstrtab         STRTAB           0000000000000000  00000231
       0000000000000029  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

$ readelf -h main
ELF 头:
  Magic:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          LoongArch
  版本:                              0x1
  入口点地址:              0x120000120
  程序头起点:              64 (bytes into file)
  Start of section headers:          608 (bytes into file)
  标志:             0x3, LP64
  本头的大小:       64 (字节)
  程序头大小:       56 (字节)
  Number of program headers:         2
  节头大小:         64 (字节)
  节头数量:         6
  字符串表索引节头: 5

$ gcc -c main.c
main.c:6:6: warning: conflicting types for built-in function ‘printf’ [-Wbuiltin-declaration-mismatch]
 void printf(char *str, int len) {
      ^~~~~~
main.c: In function ‘exit’:
main.c:20:6: warning: number of arguments doesn’t match built-in prototype
 void exit() {
      ^~~~

       TODO:使用内嵌汇编语法,结合系统调用实现文件 a.txt 的创建、读写、文件关闭功能。


第九章 调试汇编程序

9.1 GDB 调试器的常用命令

       gdb 命令本身有很多选项(参数),可以帮助我们快速定位到程序异常点,或监控程序执行的每一个细节,例如异常点或断点处的寄存器值、函数调用栈信息、线程调度等。

9.1.1 GDB 的启动和退出

gdb program		// 启动 gdb 并执行程序 program
gdb program core	// 启动 gdb 并停止到 core 文件中的异常位置
gdb -p 1234		// 启动并绑定 gdb 到进程为 1234 的程序上
gdb attach -p 1234	// 同 gdb -p 1234
gdb --args program	// program 后面可以带参数
gdb -x gdbinit program	// 指定 gdb 配置文件

       为了更好地使用 gdb 调试程序,我们希望被调试的程序的二进制文件及其依赖的一些动态库文件中包含符号表信息。而通常情况下,为了节省存储空间,已经发布的产品级的二进制程序文件都是经过瘦身的,即已经剥离了文件中的符号信息和调试信息(stripped),这种情况下 GDB 调试过程将看不到函数名、变量名和行号等直观信息。当使用 gcc/g++ 编译源码时,带上参数 -g 选项可以生成带有符号信息和调试信息的二进制文件。

/* gdbtest.c */
#include <stdio.h>

int main (int argc, char *argv[]) {
        printf("Hello World! argc=%d\n", argc);
        for (int i = 0; i< argc; i++)
                printf("%s\n", argv[i]);
        return 0;
}


$ gcc -g gdbtest.c -o gdbtest
$ gdb -q gdbtest
Reading symbols from gdbtest...done.
(gdb) r
Starting program: /root/asm_loongarch/ch9/gdbtest 
Hello World!
[Inferior 1 (process 15238) exited normally]
(gdb) q

$ gcc gdbtest.c -o gdbtest1
$ readelf -S gdbtest1
There are 28 section headers, starting at offset 0x48c0:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000120000238  00000238
       000000000000000f  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000120000248  00000248
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000120000268  00000268
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .hash             HASH             0000000120000290  00000290
       0000000000000038  0000000000000004   A       6     0     8
  [ 5] .gnu.hash         GNU_HASH         00000001200002c8  000002c8
       0000000000000030  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           00000001200002f8  000002f8
       00000000000000d8  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           00000001200003d0  000003d0
       0000000000000089  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           000000012000045a  0000045a
       0000000000000012  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          0000000120000470  00000470
       0000000000000020  0000000000000000   A       7     1     8
  [10] .rela.dyn         RELA             0000000120000490  00000490
       00000000000000a8  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             0000000120000538  00000538
       0000000000000018  0000000000000018  AI       6    20     8
  [12] .plt              PROGBITS         0000000120000550  00000550
       0000000000000030  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000120000580  00000580
       0000000000000278  0000000000000000  AX       0     0     16
  [14] .rodata           PROGBITS         00000001200007f8  000007f8
       0000000000000015  0000000000000000   A       0     0     8
  [15] .eh_frame_hdr     PROGBITS         0000000120000810  00000810
       000000000000002c  0000000000000000   A       0     0     4
  [16] .eh_frame         PROGBITS         0000000120007d30  00003d30
       00000000000000fc  0000000000000000  WA       0     0     8
  [17] .init_array       INIT_ARRAY       0000000120007e30  00003e30
       0000000000000008  0000000000000008  WA       0     0     8
  [18] .fini_array       FINI_ARRAY       0000000120007e38  00003e38
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .dynamic          DYNAMIC          0000000120007e40  00003e40
       00000000000001c0  0000000000000010  WA       7     0     8
  [20] .got.plt          PROGBITS         0000000120008000  00004000
       0000000000000018  0000000000000008  WA       0     0     8
  [21] .got              PROGBITS         0000000120008018  00004018
       0000000000000040  0000000000000008  WA       0     0     8
  [22] .sdata            PROGBITS         0000000120008058  00004058
       0000000000000008  0000000000000000  WA       0     0     8
  [23] .bss              NOBITS           0000000120008060  00004060
       0000000000000008  0000000000000000  WA       0     0     1
  [24] .comment          PROGBITS         0000000000000000  00004060
       0000000000000029  0000000000000001  MS       0     0     1
  [25] .symtab           SYMTAB           0000000000000000  00004090
       0000000000000558  0000000000000018          26    42     8
  [26] .strtab           STRTAB           0000000000000000  000045e8
       00000000000001dd  0000000000000000           0     0     1
  [27] .shstrtab         STRTAB           0000000000000000  000047c5
       00000000000000f8  0000000000000000           0     0     1
  ...

$ readelf -S gdbtest
There are 33 section headers, starting at offset 0x50f8:

节头:
  ...
  [25] .debug_aranges    PROGBITS         0000000000000000  00004089
       0000000000000030  0000000000000000           0     0     1
  [26] .debug_info       PROGBITS         0000000000000000  000040b9
       0000000000000301  0000000000000000           0     0     1
  [27] .debug_abbrev     PROGBITS         0000000000000000  000043ba
       00000000000000cc  0000000000000000           0     0     1
  [28] .debug_line       PROGBITS         0000000000000000  00004486
       0000000000000124  0000000000000000           0     0     1
  [29] .debug_str        PROGBITS         0000000000000000  000045aa
       0000000000000264  0000000000000001  MS       0     0     1
  ...

       “-g”:通知编译器保留调试信息。
       “-q”:屏蔽 GDB 启动时输出的 GDB 版本信息、版权说明、帮助提示等。
       “r” <=> “run”
       “q”<=> “quit”

9.1.2 断点设置

       通常我们会在 GDB 启动后进行断点设置。程序断点的设置可以让 GDB 通过程序执行到指定位置(如某行、某个函数、某个地址)处停下来,等待我们进一步处理。
       GDB 支持 3 种断点:
       break 断点(程序断点):让程序执行到指定行或者指定函数位置时暂停下来,是最常用的断点类型;
       watch 断点(数据断点):用于监视某个数据变量的变化,当指定数据变量或内存地址单元被修改时,程序暂停。
       catch 断点(事件断点):用于捕获程序执行期间产生的指定事件,例如 assert、exception、syscall、signal、fork 等。

     (1)break 断点设置

 操作 命令
 设置断点

 break [LOCATION] [thread THREADNUM] [if CONDITION]

 tbreak [LOCATION] [thread THREADNUM] [if CONDITION]

 rbreak [LOCATION] [thread THREADNUM] [if CONDITION]

 hbreak [LOCATION] [thread THREADNUM] [if CONDITION]

 thbreak [LOCATION] [thread THREADNUM] [if CONDITION]

 查看断点 info break
 删除断点 clear location 或者 delete num
 禁用断点 disable num1 num2 ...
 使能断点 enable num1 num2...

       表中 break、tbreak 和 rbreak 被称为软件断点,用于一般程序的断点设置。hbreak 和 thbreak 被称为硬件断点,主要是针对位于 EEPROM/ROM 上的代码调试。
       设置命令 break 的缩写命令为“b”。
       参数 LOCATION 可以为行号、函数名或者一个具体的内存地址。如果没有指定 LOCATION,默认为当前栈帧的 PC 值。
       使用选项 thread THREADNUM 可以设置断点到某一个线程,其中线程号 THREADNUM 可以通过命令“info threads”查看并获得。
       选项 if CONDITION 用于带条件的断点设置,即当条件表达式 CONDITION 的值为真时,断点才会生效。这对调试某个变量为特定值或者调试循环到指定次数的情况很有用

b a.c:4					//在源 C 语言文件 a.c 的第 4 行设置断点
b main					//在函数 main 入口处设置断点
a.c:add					//在源 C 语言文件 a.c 的函数 add 入口处设置断点
b *0x120000774 			//在地址 0x120000774 处设置断点
b a.c:21 if out == 20 	//条件断点,即当变量等于 20 时,程序在 a.c 中 21 行处暂停
b a.c:21 thread 1 		//在 a.c 中 21 行设置断点,仅对 NUM 为 1 的线程起效

       命令 tbreak( tb 和 rbreak ( rb )的用法和 break 类似。区别是 tbreak 表示临时断点,即此断点只生效一次。rbreak 用于对满足匹配规则的所有函数设置断点

tbreak a.c:21	//在 a.c 中的 21 行设置断点,此断点只生效一次
ignore 1 10		//跳过(忽略)1 号断点的前 10 次执行。1 为断点号
rbreak .		//对程序中所有函数设置断点
rbreak a.c::.	//仅对 a.c 文件中的所有函数设置断点
rbreak add*		//对程序中所有以 add 为前缀的函数设置断点

       硬件断点 hbreak( hb )和 thbreak( thb )的用法也同 break。
       断点设置后,使用命令“info break”或“info b”来查看当前程序已经设置的所有断点信息。

$ gdb -q gdbtest
Reading symbols from gdbtest...done.
(gdb) b gdbtest.c:4 ---> 设置断点为源码的第 4 行
Breakpoint 1 at 0x1200007a0: file gdbtest.c, line 4.
(gdb) info b ---> 查看断点设置信息
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000001200007a0 in main at gdbtest.c:4
(gdb) r ---> 运行程序
Starting program: /root/asm_loongarch/ch9/gdbtest 

Breakpoint 1, main (argc=1, argv=0xffffff74f8) at gdbtest.c:4
4               printf("Hello World! argc=%d\n", argc); ---> 程序运行到第 4 行时暂停
(gdb) c ---> 继续运行程序
Continuing.
Hello World! argc=1
/root/asm_loongarch/ch9/gdbtest
[Inferior 1 (process 23995) exited normally] ---> 程序执行完毕
(gdb) q ---> 退出 gdb

       clear 命令可以删除指定位置的所有断点,参数 location 通常为某一个行代码的行号或者某个具体的函数名。当参数 location 为某个函数的函数名时,表示删除位于该函数入口处的所有断点。
       delete 命令( d )可以删除指定编号的断点或全部断点,其参数 num 为指定断点的编号。当 num 没有指定时,delete 命令会删除当前程序中存在的所有断点。
       disable 命令,禁用 1 个或多个或所有断点,disable num1 禁用编号为 num1 的一个断点,disable num1 num2 禁用编号为 num1,num2 两个断点,当没有指定编号时,禁用所有断点。
       enable 命令与 disable 命令相反。

$ gdb -q gdbtest
Reading symbols from gdbtest...done.
(gdb) b gdbtest.c:4
Breakpoint 1 at 0x1200007a0: file gdbtest.c, line 4.
(gdb) b main
Note: breakpoint 1 also set at pc 0x1200007a0.
Breakpoint 2 at 0x1200007a0: file gdbtest.c, line 4.
(gdb) b gdbtest.c:5
Breakpoint 3 at 0x1200007b4: file gdbtest.c, line 5.
(gdb) info b ---> 显示当前共有 3 个断点
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000001200007a0 in main at gdbtest.c:4
2       breakpoint     keep y   0x00000001200007a0 in main at gdbtest.c:4
3       breakpoint     keep y   0x00000001200007b4 in main at gdbtest.c:5
(gdb) disable 2 ---> 禁用编号为 2 的断点,对应的 Enb 显示 n
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000001200007a0 in main at gdbtest.c:4
2       breakpoint     keep n   0x00000001200007a0 in main at gdbtest.c:4
3       breakpoint     keep y   0x00000001200007b4 in main at gdbtest.c:5
(gdb) delete 1 ---> 删除编号为 1 的断点
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep n   0x00000001200007a0 in main at gdbtest.c:4
3       breakpoint     keep y   0x00000001200007b4 in main at gdbtest.c:5
(gdb) enable 2 ---> 重新启用编号为 2 的断点
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x00000001200007a0 in main at gdbtest.c:4
3       breakpoint     keep y   0x00000001200007b4 in main at gdbtest.c:5
(gdb) 

     (2)watch 断点设置

       借助 watch 断点可以监控程序中某个变量或者表达式的值,只要此值发生改变,程序就会停止执行。这对于定位某个变量或内存单元遇到非法篡改的程序时很有帮助。

watch a 					//对变量 a 设置断点。仅当 a 发生写变化(被修改)时,程序暂停
watch *(int*)0x120008064 	//对地址 0x120008064 设置断点,当此地址内的 4 字节发生写变化时,程序暂停
watch a thread 2 			//对变量 a 设置断点,仅当 a 在线程 2 中发生写变化时,程序暂停
rwatch a 					//对变量 a 设置断点,仅当 a 发生读变化时,程序暂停
awatch a 					//对变量 a 设置断点,当 a 发生读或者写变化时,程序暂停
info watch					//查看当前程序设置的所有 watch 断点
info b 						//查看当前程序设置的所有 break 断点和 watch 断点
info thread 				//查看当前程序的所有线程信息

/*
 * gdbtest.c
 * gcc -g gdbtest.c -o gdbtest
 */
#include <stdio.h>
int tt;
int main (int argc, char *argv[]) {
        for(int i = 0; i < 3; i++)
                tt = i;
        return 0;
}

$ gdb -q gdbtest
Reading symbols from gdbtest...done.
(gdb) watch tt
Hardware watchpoint 1: tt
(gdb) info watch
Num     Type           Disp Enb Address    What
1       hw watchpoint  keep y              tt
(gdb) r
Starting program: /root/asm_loongarch/ch9/9.1.2.2/gdbtest 

Hardware watchpoint 1: tt

Old value = 0
New value = 1
main (argc=1, argv=0xffffff74d8) at gdbtest.c:8
8               for(int i = 0; i < 3; i++)
(gdb) c
Continuing.

Hardware watchpoint 1: tt

Old value = 1
New value = 2
main (argc=1, argv=0xffffff74d8) at gdbtest.c:8
8               for(int i = 0; i < 3; i++)
(gdb) c
Continuing.
[Inferior 1 (process 29359) exited normally]

       watch 的实现一般需要处理器硬件支持。从上面的而信息可以看出,龙芯处理器硬件支持 watch 断点。

     (3)catch 断点设置
       catch 断点的作用是监控程序中某一个事件的发生,例如程序发生某种异常、某一动态库被加载等,一旦目标事件发生,则程序暂停。

catch event
 事件(event) 含义
 catch/throw catch/throw 都用于捕获程序异常,使用命令为 “catch catch”  “catch throw”  “catch throw int”
 exec 为 exec 系列系统调用设置捕获点,使用命令为 “catch exec”
 fork 在 fork 调用发生后,暂停程序的运行。设置命令 “catch fork”
 vfork 在 vfork 调用发生后,暂停程序的运行。设置命令 “catch vfork”
 load 当一个库被加载时,暂停程序的运行。设置命令 “catch load libc.so.6”
 unload 当一个库被卸载时,暂停程序的运行。设置命令 “catch unload libc.so.6”
 signal 通过信号值或信号别名来捕获一个信号异常。使用命令为 “catch signal 11” “catch signal SIGSEGV”
 syscall 通过方法名或系统调用号来捕获一个系统调用
catch signal SIGBUS		//捕获 SIGBUS 事件,当此事件发生时程序暂停
tcatch signal SIGBUS	//仅捕获 SIGBUS 事件一次
catch signal all		//捕获所有信号事件,当任意一个事件发生时程序暂停
catch syscall chroot	//捕获系统调用 chroot,当此接口被调用时程序暂停
catch syscall			//捕获所有系统调用,当任意一个系统调用发生时程序暂停
info b					//查看所有 break、watch、catch 断点信息

9.1.3 查看变量、内存数据和寄存器信息

     (1)print/display命令
       当程序执行被 GDB 暂停到某个断点处时,可以通过 print( p )或 display 命令来查看某个变量或表达式的值。

p variable
p file::variable
print function:variable

display variable
display file::variable
display function:variable

display 与 print 区别在于,使用 display 查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 都自动输出。

     (2)info register 命令
       此命令可以在程序暂停在某个断点时,查看一个、多个或所有寄存器的信息。

info register r4
info register r4 r5
info all-register//查看所有通用寄存器、浮点寄存器、向量寄存器的值
i r r4
i r a0
i r//查看所有通用寄存器、pc、badvaddr的值
i all-r

$ cat gdbtest.c 
/* gdbtest.c */
#include <stdio.h>
int add(int a, int b) {
        return a+b;
}

int main() {
        add(1, 2);
        return 0;
}

$ gdb -q gdbtest
Reading symbols from gdbtest...done.
(gdb) b add ---> 设置断点到函数 add 入口处
Breakpoint 1 at 0x1200006f4: file gdbtest.c, line 4.
(gdb) r
Starting program: /root/asm_loongarch/ch9/9.1.3.2/gdbtest 

Breakpoint 1, add (a=1, b=2) at gdbtest.c:4
4               return a+b;
(gdb) i r a0 ---> 查看寄存器 a0 的值,分别显示十六进制和十进制
a0             0x1                 1
(gdb) i r a1 ---> 查看寄存器 a1 的值
a1             0x2                 2
(gdb) i r ---> 查看所有通用寄存器、pc、badvaddr的值
                  zero               ra               tp               sp
R0   0000000000000000 000000012000072c 000000fff7ffefe0 000000ffffff7380 
                    a0               a1               a2               a3
R4   0000000000000001 0000000000000002 000000ffffff74e8 000000fff7fa84b0 
                    a4               a5               a6               a7
R8   0000000000000000 000000fff7fdfc30 000000ffffff74d0 0000000000800000 
                    t0               t1               t2               t3
R12  0000000000000002 0000000000000001 0000000000000000 000000fff7faaec0 
                    t4               t5               t6               t7
R16  000000fff7fa9d48 000000fff7fa9d48 7f7f7f7f7f7f7f7f 0000000000000000 
                    t8                x               fp               s0
R20  ffffff0000000000 0000000000000000 000000ffffff73a0 0000000000000000 
                    s1               s2               s3               s4
R24  0000000120000744 000000012014acc0 000000012014d5e0 000000012013aa90 
                    s5               s6               s7               s8
R28  000000012014d590 000000012014acc0 0000000000000000 000000012013fb60 
pc             0x1200006f4         0x1200006f4 <add+36>
badvaddr       0xfff7e49dec        0xfff7e49dec <__GI___ctype_init>
(gdb) 

     (3)disassemble 命令
       使用 disassemble ( disass )可以查看(反汇编)指定方法或指定一段地址的汇编指令。

disass//查看当前断点所在函数对应的汇编指令
disass func_name//查看指定函数名对应的汇编指令
disass addr//查看指定地址 addr 所在函数的汇编指令
disass addr1,addr2//查看指定地址 addr1 和 addr2 范围内的汇编指令

/* gdbtest.c */
#include <stdio.h>
int add(int a, int b) {
        return a+b;
}

int main() {
        add(1, 2);
        return 0;
}

$  gdb -q gdbtest
Reading symbols from gdbtest...done.
(gdb) b add
Breakpoint 1 at 0x1200006f4: file gdbtest.c, line 4.
(gdb) r
Starting program: /root/asm_loongarch/ch9/9.1.3.2/gdbtest 

Breakpoint 1, add (a=1, b=2) at gdbtest.c:4
4               return a+b;
(gdb) disass
Dump of assembler code for function add:
   0x00000001200006d0 <+0>:     addi.d  $r3,$r3,-32(0xfe0)
   0x00000001200006d4 <+4>:     st.d    $r22,$r3,24(0x18)
   0x00000001200006d8 <+8>:     addi.d  $r22,$r3,32(0x20)
   0x00000001200006dc <+12>:    move    $r13,$r4
   0x00000001200006e0 <+16>:    move    $r12,$r5
   0x00000001200006e4 <+20>:    slli.w  $r13,$r13,0x0
   0x00000001200006e8 <+24>:    st.w    $r13,$r22,-20(0xfec)
   0x00000001200006ec <+28>:    slli.w  $r12,$r12,0x0
   0x00000001200006f0 <+32>:    st.w    $r12,$r22,-24(0xfe8)
=> 0x00000001200006f4 <+36>:    ld.w    $r13,$r22,-20(0xfec) //可以看出,break 命令在进行函数断点设置时,断点位置在程序栈侯建之后位置,而非函数入口的第一条指令。
   0x00000001200006f8 <+40>:    ld.w    $r12,$r22,-24(0xfe8)
   0x00000001200006fc <+44>:    add.w   $r12,$r13,$r12
   0x0000000120000700 <+48>:    move    $r4,$r12
   0x0000000120000704 <+52>:    ld.d    $r22,$r3,24(0x18)
   0x0000000120000708 <+56>:    addi.d  $r3,$r3,32(0x20)
   0x000000012000070c <+60>:    jirl    $r0,$r1,0
End of assembler dump.
(gdb) disass main
Dump of assembler code for function main:
   0x0000000120000710 <+0>:     addi.d  $r3,$r3,-16(0xff0)
   0x0000000120000714 <+4>:     st.d    $r1,$r3,8(0x8)
   0x0000000120000718 <+8>:     st.d    $r22,$r3,0
   0x000000012000071c <+12>:    addi.d  $r22,$r3,16(0x10)
   0x0000000120000720 <+16>:    addi.w  $r5,$r0,2(0x2)
   0x0000000120000724 <+20>:    addi.w  $r4,$r0,1(0x1)
   0x0000000120000728 <+24>:    bl      -88(0xfffffa8) # 0x1200006d0 <add>
   0x000000012000072c <+28>:    move    $r12,$r0
   0x0000000120000730 <+32>:    move    $r4,$r12
   0x0000000120000734 <+36>:    ld.d    $r1,$r3,8(0x8)
   0x0000000120000738 <+40>:    ld.d    $r22,$r3,0
   0x000000012000073c <+44>:    addi.d  $r3,$r3,16(0x10)
   0x0000000120000740 <+48>:    jirl    $r0,$r1,0
End of assembler dump.
(gdb) disass $pc-16, $pc+16
Dump of assembler code from 0x1200006e4 to 0x120000704:
   0x00000001200006e4 <add+20>: slli.w  $r13,$r13,0x0
   0x00000001200006e8 <add+24>: st.w    $r13,$r22,-20(0xfec)
   0x00000001200006ec <add+28>: slli.w  $r12,$r12,0x0
   0x00000001200006f0 <add+32>: st.w    $r12,$r22,-24(0xfe8)
=> 0x00000001200006f4 <add+36>: ld.w    $r13,$r22,-20(0xfec)
   0x00000001200006f8 <add+40>: ld.w    $r12,$r22,-24(0xfe8)
   0x00000001200006fc <add+44>: add.w   $r12,$r13,$r12
   0x0000000120000700 <add+48>: move    $r4,$r12
End of assembler dump.
(gdb) 

     (4)x 命令
       print 或 display 命令不能查看指定内存地址中的数据。GDB 提供了查看内存的命令 x,其可查看指定内存地址上的数据,且数据格式还可以指定。

x/FMT ADDRESS

参数 FMT 由内存单元数量、格式、内存单元长度组成。

内存单元数量:为整数,不指定时默认为 1 ;

格式:有多种,具体如下:
	x(hex):按十六进制格式显示变量
	d(decimal):十进制格式
	u(unsigned decimal):十进制无符号格式
	o(octal):八进制格式
	t(binary):二进制格式
	a(address):十六进制显示地址
	i(instruction):指令地址格式
	c(char):字符格式
	f(float):浮点数格式
	s(string):字符串格式

内存单元长度:b 表示单字节、h 表示双字节、w 表示 4 字节、g 表示 8 字节, 不指定时默认值为 w。

ADDRESS:内存地址,可以是一个绝对地址(如 0x12000006c,也可以是基于当前 pc 的相对地址(如 $pc-4,表示当前程序暂停时,地址减 4 字节的内存位置)

/* gdbtest.c */
#include <stdio.h>
int out = 0;

int main() {
        out += 3;
        return 0;
}

$ gdb -q gdbtest
Reading symbols from gdbtest...done.
(gdb) b main ---> 在 main 函数设置断点
Breakpoint 1 at 0x1200006dc: file gdbtest.c, line 6.
(gdb) r ---> 程序运行
Starting program: /root/asm_loongarch/ch9/9.1.3.4/gdbtest 

Breakpoint 1, main () at gdbtest.c:6
6               out += 3;
(gdb) x/10i $pc ---> 查看 pc 位置开始的 10 条汇编指令
=> 0x1200006dc <main+12>:       pcaddu12i       $r12,8(0x8)
   0x1200006e0 <main+16>:       addi.d  $r12,$r12,-1680(0x970)
   0x1200006e4 <main+20>:       ld.w    $r12,$r12,0
   0x1200006e8 <main+24>:       addi.w  $r12,$r12,3(0x3)
   0x1200006ec <main+28>:       move    $r13,$r12
   0x1200006f0 <main+32>:       pcaddu12i       $r12,8(0x8)
   0x1200006f4 <main+36>:       addi.d  $r12,$r12,-1700(0x95c)
   0x1200006f8 <main+40>:       st.w    $r13,$r12,0
   0x1200006fc <main+44>:       move    $r12,$r0
   0x120000700 <main+48>:       move    $r4,$r12
(gdb) b *0x1200006fc ---> 在地址 0x1200006fc 处设置断点
Breakpoint 3 at 0x1200006fc: file gdbtest.c, line 7.
(gdb) c ---> 继续程序执行
Continuing.

Breakpoint 3, main () at gdbtest.c:7
7               return 0;
(gdb) i r r12 r13 ---> 查看寄存器 r12 和 r13 的值
r12            0x12000804c         4831871052
r13            0x3                 3
(gdb) x/1d 0x12000804c ---> 查看地址 0x12000804c 一个十进制值
0x12000804c <out>:      3 ---> 即变量 out 值
(gdb) 

9.1.4 查看堆栈信息

     (1)backtrace 命令
       backtrace( bt)命令用于查看当前被调试程序的方法栈信息,以直观显示函数间的调用关系。

backtrace [QUALIFIERA] [COUNT]
参数 QUALIFIERA 为可选项,其值可为“full”或者“no-filters”,分别表示输出局部变量的值和限定符禁止执行帧筛选器。
参数 COUNT 也是可选项,其值为一个整数值,当值为正整数 n 时,表示输出最里层的 n 个栈帧信息;当值为负整数时,那么表示输出最外层 n 个栈帧信息;当没有 COUNT 参数时,backtrace 会显示完整的栈帧信息。

/* gdbtest.c */
#include <stdio.h>

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

int add2(int a, int b) {
        return add3(a, b);
}

int add1(int a, int b) {
        return add2(a, b);
}

int add0(int a, int b) {
        return add1(a, b);
}

int main() {
        add0(1, 2);
        return 0;
}

$gdb -q gdbtest 
Reading symbols from gdbtest...done.
(gdb) b add3
Breakpoint 1 at 0x1200006f4: file gdbtest.c, line 5.
(gdb) r
Starting program: /root/asm_loongarch/ch9/9.1.4.1/gdbtest 

Breakpoint 1, add3 (a=1, b=2) at gdbtest.c:5
5               return a+b;
(gdb) bt
#0  add3 (a=1, b=2) at gdbtest.c:5
#1  0x000000012000074c in add2 (a=1, b=2) at gdbtest.c:9
#2  0x00000001200007a0 in add1 (a=1, b=2) at gdbtest.c:13
#3  0x00000001200007f4 in add0 (a=1, b=2) at gdbtest.c:17
#4  0x0000000120000828 in main () at gdbtest.c:21
(gdb) bt 2
#0  add3 (a=1, b=2) at gdbtest.c:5
#1  0x000000012000074c in add2 (a=1, b=2) at gdbtest.c:9
(More stack frames follow...)
(gdb) bt -2
#3  0x00000001200007f4 in add0 (a=1, b=2) at gdbtest.c:17
#4  0x0000000120000828 in main () at gdbtest.c:21
(gdb) 

     (2)frame 命令
       如果要查看 backtrace 结果中某一层的方法栈信息,可以使用 frame(f)命令。

frame [frame_num | frame_addr]
当不指定任何参数时,frame 命令将显示 backtrace 结果中最顶层方法的栈帧。

$ gdb -q gdbtest 
Reading symbols from gdbtest...done.
(gdb) b add3 
Breakpoint 1 at 0x1200006f4: file gdbtest.c, line 5.
(gdb) r
Starting program: /root/asm_loongarch/ch9/9.1.4.1/gdbtest 

Breakpoint 1, add3 (a=1, b=2) at gdbtest.c:5
5               return a+b;
(gdb) info f
Stack level 0, frame at 0xffffff7340:
 pc = 0x1200006f4 in add3 (gdbtest.c:5); saved pc = 0x12000074c
 called by frame at 0xffffff7360
 source language c.
 Arglist at 0xffffff7340, args: a=1, b=2
 Locals at 0xffffff7340, Previous frame's sp is 0xffffff7340
 Saved registers:
  fp at 0xffffff7338
(gdb) f ---> 显示最顶层(即断点处对应方法)的栈信息
#0  add3 (a=1, b=2) at gdbtest.c:5
5               return a+b;
(gdb) f 1 ---> 显示编号为 1 的栈信息
#1  0x000000012000074c in add2 (a=1, b=2) at gdbtest.c:9
9               return add3(a, b);
(gdb) f 0
#0  add3 (a=1, b=2) at gdbtest.c:5
5               return a+b;

9.2 程序单步调试

       当程序执行到断点位置暂停时,可以用 continue(c)命令恢复并继续执行,也可以使用单步调试命令一步一步地跟踪程序执行。

9.2.1 语句单步调试

       语句单步调试是指以源程序(如 C 语言)的一条语句为单位,一步一步地执行。
       GDB 提供了 3 种命令:
       next(n):最常用的单步调试命令。其最大的特点是当遇到调用函数的语句时,next 命令会将其视为一行语句并一步执行完,不会跳入调用函数内部。
       step(s):当遇到调用函数的语句时,会进入该函数内部继续执行。
       util(u):在程序执行至循环体尾部时,使 GDB 快速执行完成当前的循环体并运行至循环体外停止。
       next 或者 step 命令都可以选择性地添加 count 参数,表示一次执行完后面的 count 条语句。

gdb -q gdbtest 
Reading symbols from gdbtest...done.
(gdb) b main
Breakpoint 1 at 0x12000081c: file gdbtest.c, line 21.
(gdb) r
Starting program: /root/asm_loongarch/ch9/9.1.4.1/gdbtest 

Breakpoint 1, main () at gdbtest.c:21
21              add0(1, 2);
(gdb) s
add0 (a=1, b=2) at gdbtest.c:17
17              return add1(a, b);
(gdb) s ---> 进入 add1 函数内部
add1 (a=1, b=2) at gdbtest.c:13
13              return add2(a, b);
(gdb) s ---> 进入 add2 函数内部
add2 (a=1, b=2) at gdbtest.c:9
9               return add3(a, b);
(gdb) n ---> 不进入函数 add3 内部
10      }
(gdb) 

9.2.2 汇编指令的单步调试

       stepi(si)和 nexti(ni)都可以用于单步执行汇编指令,和 step 与 next 是类似的。

9.2.3 退出当前函数

       在某个函数中调试一段时间后,可能希望直接执行完当前函数,可以用 finish 命令。与 finish 命令类似的还有 return 命令,它们都可以结束当前执行的函数。区别在与 finish 命令会执行完函数退出;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余代码未执行完,也不会执行了,但是使用 return 命令可以指定函数的返回值。


第十章 汇编程序性能优化

       如何充分地利用处理器特性来编写高效的汇编指令?
       一方面要从汇编指令逻辑入手做优化,例如使用移位指令代替简单的乘法指令、把被多次使用的内存数据或常量提前载入寄存器以便重复使用、展开循环次数为常数的小循环、利用额外的寄存器和向量指令等。另一方面,我们有必要了解一些计算机体系架构的知识和龙芯处理器内部的关键细节,比如高速缓存、流水线、多发射技术等,在编写汇编程序时可尽量充分利用这些技术特点来提高程序性能。

10.1 计算机体系架构的三类并行技术

     (1)指令级并行:是指在一个时钟周期内执行尽可能多条机器指令。
       典型的指令级并行技术有:
       指令级流水线技术:指同一个周期里可以有多条指令在执行,即通过时间重叠实现指令级并行,实际上是提高频率。龙芯处理器可以达到 12 级流水线。
       多发射技术:指增加处理器中的功能部件,例如增加 4 个加法器就可以同时处理 4 条加法指令,即通过空间重复实现指令级并行,实现一拍执行多条指令。目前,龙芯是 4 发射,即一拍可以执行 4 条加法指令。
       乱序执行技术:指当遇到执行时间较长或条件不具备的指令时,把条件具备的后续指令提前执行,目的就是提高指令流水线的效率,充分利用指令间潜在的可重叠性和不相关性。
     (2)数据级并行:指一条指令可以处理多个数据,通常称为单指令多数据流(Single Instruction Multiple Data,SIMD),例如一条指令完成 4 组或 8 组的加法运算。典型实现技术就是向量指令。LoongArch 基础指令集采用 LSX 和 LASX ,操作的向量位宽是 128 位和 256 位。
     (3)任务级并行:包括线程级并行和进程级并行。任务级并行依赖处理器的多核结构,在单处理器的算力固定的情况下,多核的并行计算就是提升系统计算吞吐量的最好方式。在多核处理器环境下,软件开发人员要了解的就是同步机制,即多核处理器之间如何正确且尽可能高效地协调共享数据的问题。
上述所有并行技术的目的都是一致的,即尽可能地提高处理器运行效率。

10.2 使用向量指令

       对于加速计算密集型应用程序,使用向量指令再好不过了。比如在图像处理领域,图像常用的数据类型是 YUV 格式(Y 表示明亮度,也就是灰阶值;U、V 表示色度,描述的是色调和饱和度),通常 YUV 占用的数据长度为 8 位。在对这类数据进行大量的加法运算时,如果使用基础指令集,需要 4 条指令,虽然寄存器是 32 位或 64 位,但是只能用低 8 位,只能完成 1 组(8位)的加法计算。

ld.b	t1, addr1
ld.b	t2, addr2
add.w	t3, t1, t2
st.b	t3, addr3

       但是如果用向量指令,一次可以完成 16 组或 32 组数据计算,寄存器宽度为 128 位或 256 位。

xvld	x1, addr1
xvld	x2, addr2
xvadd.b	x3, x1, x2
xvst	x3, addr3

       使用向量指令的缺点是,向量寄存器复用浮点数寄存器,可能会有浮点数模式切换场景,从而产生性能开销。所以在使用向量指令时,应避免向量指令和浮点指令同时出现在同一个方法中,同时要确保向量指令用在热点方法或循环计算体总。所谓热点方法或循环计算体就是在整个程序运行过程中,多次被执行到或执行时间占比较大的方法或循环体。

10.3 指令融合和地址对齐

       指令融合是指将多条指令由使用效率更高的一条或者几条指令进行替换,从而提高性能。

shl r3, 3
add.d r2, r2, r3
=>
alsl_d r2, r3, r2, 2

       因为龙芯指令集有移位加的指令 alsl ,故数据相关的两条移位指令 shl 和 add 可用一条 alsl 指令完成。

       如果多线程程序对共享数据有保序要求,可能需要在写指令 st 前后添加屏障指令 dbar ,此时使用具有屏障功能的原子指令会更高效。

dbar 0
st.w r4, r12, 0
dbar 0
=>
amswap_db.w r0, r4, r12

       众所周知,访存应当尽可能地满足地址自然对齐。对非对齐的内存地址进行访问(load 或 store)可能导致处理器花费额外的内存周期和执行更多的指令。即使龙芯处理器已经支持硬件自动处理非对齐,使得非对齐数据访问没有软件处理开销那么大,但也有一定程度的性能下降。所有对一些常用的保证数据对其的方式还是有必要了解的,比如:
       将多字节整数和浮点数对齐到自然边界
       尽量使用存储对齐,而非加载对齐
       必要时填充数据结构,以保证正确对齐

       在龙芯指令集中,边界检查访存指令、原子访存指令和普通访存指令中的 LDX、STX 指令强制要求访存地址自然对齐,否则将触发非对齐例外。而其他常用的普通访存指令,例如 LD.{B/H/W/D}、ST.{B/H/W/D}等,如果硬件实现非对齐访存且当前环境配置为允许非对齐访存,那么其支持非对齐访存,即当访存地址不是自然对齐时,硬件处理自然对齐并返回正确结果。LoongArch 支持硬件处理非对齐的内存数据访存。对于如何判断当前环境配置为允许非对齐访存,可以编写一个简单的非对齐访存程序来验证,也可以通过读取控制状态寄存器 MISC 来判断。

10.4 指令调度

10.4.1 指令流水线和流水线冲突

       指令流水线就是把每一条指令的执行划分成几个阶段,多条指令的不同阶段可以在同一个周期内同时进行,充分利用 CPU 核中的功能部件,从而提高指令吞吐量。
       以经典的 5 级流水线为例,一条指令的执行分为取指、读寄存器、执行、访问内存、写回这 5 个阶段。

取指读寄存器执行访问内存写回    
 取指读寄存器执行访问内存写回   
  取指读寄存器执行访问内存写回  
   取指读寄存器执行访问内存写回 
    取指读寄存器执行访问内存写回

       指令的每一个阶段都占用固定的时间(通常为一个处理器时钟周期)。
       取指阶段:根据程序计数器(PC)访问指令缓存和指令 TLB 来取一条或多条指令到指令存储器。
       读寄存器:用于读取该指令的源寄存器中的内容。
       执行阶段:用于完成算术或者逻辑运算。
       访存阶段:用于读写数据缓存中的内存变量。
       写回阶段:将操作结果值写回寄存器堆。

       龙芯 3 号处理器的基本流水线包括 PC、取指、预译码、译码 1、译码 2、寄存器重命名、调度、发射、读寄存器、执行、写回、提交,共 12 级流水。

       大部分指令之间是存在相关性的,具体分为 3 种情况:
       数据相关:如果当前指令需要用到上一条指令的结果,当前指令的执行需要等上一条指令执行完成,则这两条指令定义为数据相关。
       控制相关:如果当前指令为条件转移指令,下一条指令的执行取决于当前条件转移指令的执行结果,则这两条指令定义为控制相关。
       结构相关:如果两条指令使用同一功能部件,例如都使用 ALU 部件的整数运算指令或 FLU 部件的浮点指令,则这两条指令定义为结构相关。

       指令间的相关性会导致流水线阻塞。以数据相关为例,例如第 N 条指令的功能是把结果写回 r1 寄存器,第 N+1 条指令要用到 r1 的值进行计算。在上述 5 级流水线中,第 N 条指令在第五阶段才能把结果写回寄存器 r1,而第 N+1 条指令在第二阶段就要读 r1 值,这将导致第 N 条指令还没有把结果写回 r1 寄存器时,第 N+1 条指令就把旧的值读出来使用,如果不加以控制就会造成运算结果的错误。简单的等待可以解决这类指令的数据相关,即第 N+1 条指令在第二阶段等待 3 拍再读取寄存器的值,不过这样就会引起指令流水线的阻塞而导致性能下降。

       流水线前递技术可以解决指令间的数据相关问题,即后面指令不需要等到前面指令把执行结果写回寄存器即可获得。

add.d r5, r4, r3
sub.d r6, r5, r3

       这里第二条减法指令 sub.d 的计算用到了第一条加法指令 add.d 的结果,即可认为两条指令是存在数据相关的。使用流水线前递技术可以让 sub.d 指令计算时不用等到 add.d 指令的写回阶段完成,而是在 r4 与 r3 加法运算执行完成后就可获得结果,继续 sub.d 指令的执行阶段。
       但在多拍操作的情况下,前递技术的作用还十分有限。因为前递技术只能少等 1、2 拍,而对于下面这样的指令序列:

load r1, addr
addi r1, r1, #2

       因为 load 指令要执行多拍且不能确定拍数,流水线前递技术对此无能为力。故后文会介绍通过静态指令调度隔开相关的指令来避免流水线冲突。

       再来了解一下控制相关和结构相关造成的流水线阻塞。对于控制相关,由于在条件转移指令执行完成直线,处理器无法确定下一条要执行的指令地址,所以不能把下一条指令放入流水线中,只能等待该条件转移指令执行完毕才能开始下一条待执行指令的取指,故也会导致流水线阻塞。目前大部分的处理器基本都采用分支预测技术,从而尽量减少控制相关带来的流水线阻塞次数。结构相关引起流水线阻塞的原因就是资源的有限性,例如如果处理器只有一个乘法功能部件,那么一条乘法指令在运算时,后面的乘法指令只能等,故也导致流水线阻塞。

10.4.2 指令调度

       指令调度指的是在不影响程序执行结果正确性的前提下,通过改变指令的执行顺序来避免由于指令相关引起的流水线阻塞。指令调度分静态调度动态调度动态调度由硬件自动完成,而静态调度由汇编语言编写人员或编译器在程序执行前进行指令重新排序来实现
       在对指令静态调度优化之前,我们需要了解现代处理器内部通常有多个功能部件,不同类型的指令由不同的功能部件执行,且可能需要不同的执行拍数。例如算术运算、逻辑运算、转移指令在定点 ALU 里执行,且 1 拍就够了;浮点运算在浮点 ALU 中执行,且浮点 ALU 需要 2、3 拍,浮点乘、除运算需要最少 5、6拍;访存指令在访存部件中执行,且执行拍数是不确定的(和 Cache 命中/不命中有很大关系),但是也需要多拍。
       假设数据相关的浮点加载指令和浮点运算指令之间需要空 1 拍(记为指令延迟为 1),两条数据相关的浮点运算指令之间需要空 2 拍(记为指令延迟为 2),其他数据相关的整型运算指令之间没有延迟。例如要实现对一个数组内的每个元素和一个定值的加法运算,其指令序列和指令延迟信息如下:

i1 loop: fld.f	fa0, 0(r1)
		 |1
i2		 fadd.f fa2, fa0, fa1
         |2
i3 		 fsd.f fa2, 0(r1)
i4 		 addi.w r1, r1, -4
i5 		 bnez r1, loop

执行完这5条指令需要 8 拍。对于这样的指令序列,我们可以通过调整指令间的执行顺序来减少指令延迟。

i1 loop: fld.f	fa0, 0(r1)
		 |1
i2		 fadd.f fa2, fa0, fa1
i3 		 addi.w r1, r1, -4
i4 		 fsd.f fa2, 4(r1)
i5 		 bnez r1, loop
优化完仅需要 6 拍。

       一般而言,对于控制相关,我们能做的优化是有限的。但是对于数据相关和结构相关,我们可以更细致地分析,充分利用指令间的延迟来提高程序的执行效率。

10.5 循环展开

       循环展开是对有循环体的程序进行优化的技术,通过多次复制循环体内部命令,是循环次数减少或消除,以此降低由于循环索引递增和条件检查指令的多次执行而引起的性能开销。例如在 10.4.2 小节中的循环体中,指令 fld.f、fadd.f、fsd.f 是直接和运算相关的,而指令 addi、bnez 则是循环开销,可以通过循环展开来减少或者消除被执行次数。当循环次数较小时(比如循环次数为 3),我们可以将其全部展开来消除指令 addi、bnez 的使用。

i1 fld.d  fa0, 0(r1)
   | 1
i2 fadd.f fa2, fa0, fa1
   | 2
i3 fsd.f  fa2, 0(r1)

i4 fld.d  fa0, 4(r1)
   | 1
i5 fadd.f fa2, fa0, fa1
   | 2
i6 fsd.f  fa2, 4(r1)

i7 fld.d  fa0, 8(r1)
   | 1
i8 fadd.f fa2, fa0, fa1
   | 2
i9 fsd.f  fa2, 8(r1)

       循环展开之前的指令为 5 条,循环执行 3 次,共需执行的指令数为 15。而循环展开后仅需要执行 9 条指令即可完成同样的功能。

       对循环展开后的指令,我们还可以再进行一次指令调度优化。具体可以通过使用不同的寄存器和指令重排来减少数据相关带来的的指令延迟。

i1 fld.d  fa0, 0(r1)
i4 fld.d  fa3, 4(r1)
i7 fld.d  fa4, 8(r1)

i2 fadd.f fa2, fa0, fa1
i5 fadd.f fa5, fa3, fa1
i8 fadd.f fa6, fa4, fa1

i3 fsd.f  fa2, 0(r1)
i6 fsd.f  fa5, 4(r1)
i9 fsd.f  fa6, 8(r1)

       通常在编译器领域,对于迭代次数较大的循环体都有最大展开次数,通常为 4 次、8 次或者 16 次。

       循环展开可以降低循环执行开销,但是会增加代码空间,可能对指令 Cache 的命中率(CPU 在指令 Cache 中找到有用的指令被称为命中,否则为不命中。命中率为全部执行指令后,命中指令占全部执行指令的比率)产生影响故一般 16 基本是展开次数上限。展开次数不宜过大的另一个原因是寄存器数量的限制。上面介绍的循环展开后,如果寄存器空闲,就可以被利用起来减少指令间的额数据依赖,从而对展开后的指令做二次优化。但是如果展开次数过大,没有多的空闲寄存器可用,此时要么选择部分寄存器数据进栈保存、待循环结束后再出栈恢复这些寄存器的值,要么就只能忍受循环展开后的数据依赖带来的部分性能损耗。

10.6 性能分析工具 perf

       perf 是 Linux 平台的一款性能分析工具,能够对一个程序进行全程或者部分运行时段进行监控,实现函数级甚至指令级的性能统计和热点查找,从而帮助我们评估和定位程序的性能瓶颈。监控也是多方面的额,比如程序运行的总时间、程序执行的总指令数、CPU 周期数、程序中分支指令总数和分支预测率、Cache 命中率、程序触发缺页异常数量等,这些都被称为事件。使用命令 perf list 可以统计出 perf 支持的全部事件,实际工作中可以根据需要选择相应的事件。

       perf 工具支持的子命令也很多,可以通过执行 perf 命令查看全部子命令。
       perf stat:在程序开始时,对特定的事件计数器进行计算,在程序运行结束时把默认或者指定的事件统计结果简单的汇总并显示在标准输出上。
       perf top:实时显示系统/进程的性能统计信息。
       perf record/perf report:perf record 用于记录一段时间内或程序全过程的性能事件,并将结果保存在 perf.data 文件中;而 perf report 用于读取 perf record 生成的 perf.data 文件,并显示分析数据。

10.6.1 perf stat 的使用

perf stat [-e <event> | --event=EVENT] [ -p <pid> | start_command ]

       其中参数“-e”或“–event”用来指定要监测的具体事件。参数“-p”用于监测一个已经在运行的程序,后面跟的 pid 为此程序进程号。例如,要统计程序进程号为 17223 的程序在监测时间内的分支指令数和分支预测率。

perf stat -e branches -e branch-misses -p 17223

       如果不指定具体监控事件,perf stat 的默认监测的事件有 task-clock、context-switches、cpu-migrations、page-faults、cycles、instructions、branches、branch-misses。例如使用 perf stat 全程监控一个名为 hot 的程序的性能。

$ perf stat ./hot

 Performance counter stats for './hot':

              0.22 msec task-clock                #    0.559 CPUs utilized          
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
                30      page-faults               #    0.139 M/sec                  
           527,555      cycles                    #    2.444 GHz                    
           489,414      instructions              #    0.93  insn per cycle         
            87,235      branches                  #  404.128 M/sec                  
             3,515      branch-misses             #    4.03% of all branches        

       0.000385959 seconds time elapsed

       0.000432000 seconds user
       0.000000000 seconds sys

       这里显示了执行“perf stat”后默认事件的信息统计,第一列显示了每个事件占用的时间或执行次数的统计值,第二列显示了每个事件的名称,第三列为每个事件的备注信息。评价程序性能好坏最直观的就是总执行时间,即数据“0.000385959 seconds time elapsed”,这代表了程序执行消耗的实际时间(从程序开始执行到完成所经历的时间)。最后两行数据分别统计的是此程序消耗的用户态 CPU 时间内核态 CPU 时间。

       事件 task-clock 统计的是此程序真正占用的处理器时间,单位为毫秒。该值与程序的总执行时间的比值就是 CPU 占用率,即 “CPUs utilized”,比值越高说明程序的更多时间花费在 CPU 计算上而非 I/O 上。对于密集计算型多线程程序,如果是单线程执行,此值可以接近于 1;如果是多线程执行,此值可接近当前处理器所用核数。

       事件 context-switches 统计的是程序执行过程中上下文切换总次数。如果程序中执行了系统调用、进程切换等,都会触发上下文切换。该值与事件 task-clock 统计结果比值位单位时间内上下文切换次数

       perf 支持的事件中有些信息需要 root 权限,例如事件 context-switches ,当权限不足时获取到的事件值为 0 。建议你使用 perf 前将 /proc/sys/kernel/perf_event_paranoid 的值设置为 -1,或者以 root 身份运行 perf

       事件 cpu-migrations 统计的是程序执行过程中处理器核的迁移次数。这里统计的结果是 0 次,说明程序在执行过程中一直在一个核上,没有发生过迁移。通常系统为了维护多个处理器之间的负载平衡,在达到一定条件后可能会将一个任务从一个处理器核迁移到另外一个处理器核上。

       事件 page-faults 统计的是程序执行过程中缺页异常放生的总次数。该值与事件 task-clock 的比值为单位时间内发生缺页异常的次数。

       事件 cycles 统计的是程序执行占用的处理器周期数。此值与事件 task-clock 的比值为处理器有效主频

       事件 instructions 统计的是程序执行的总指令数量。此值与事件 cycles 的比值称为 IPC (insn per cycle),代表平均一个 CPU 周期内执行的指令数。通常 IPC 值越高越好,值越高说明程序更充分的利用处理器。当前龙芯处理器为四发射结构,那么理论上 IPC 值最高可以接近 4 。前面在介绍指令重排优化时,完全可以通过 IPC 值变化来判断重排效果的好坏。

       事件 branchesbranch-misses 分别统计程序执行过程中的分支指令数量和分支预测失败的指令数量。branch-misses 与 branches 的比值为分支预测率,分支预测率越高,越影响性能。前面提到的循环展开技术可以减少分支指令的执行。

       如果默认的事件不能满足要求,可以使用“perf stat -e event_name”来指定具体事件的统计。例如要查看一个程序执行过程中的一级数据缓存情况,可以使用命令“perf stat -e L1-dcache-load-misses, L1-dcache-loads”。

/* hot.c */
#include <stdio.h>

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

int add2(int a, int b) {
        return add3(a, b);
}

int add1(int a, int b) {
        return add2(a, b);
}

int add0(int a, int b) {
        return add1(a, b);
}

int main() {
        add0(1, 2);
        return 0;
}

$ perf stat -e L1-dcache-load-misses,L1-dcache-loads ./hot

 Performance counter stats for './hot':

            44,938      L1-dcache-load-misses     #   21.82% of all L1-dcache hits  
           205,953      L1-dcache-loads                                             

       0.000370479 seconds time elapsed

       0.000410000 seconds user
       0.000000000 seconds sys

       LoongArch 支持硬件预取功能,故一般正常的程序的 Cache 未命中率都不会很高。如果用户程序出现 Cache 未命中率很高的情况,可以进一步使用 perf record 来定位问题函数,并尝试调整函数实现逻辑或使用 LoongArch 的数据预取指令尝试对其进行优化。
       另外perf stat 还可以按线程来监测某个程序的性能。

perf stat --per-thread -e branch-misses -p 1318

10.6.2 perf top 的使用

       可以看到 perf stat 能对程序运行进行概括性的总结分析,但是不能精确到函数或汇编指令级别。perf top 不仅能精确到函数或汇编指令级别的事件性能统计,还可以实时显示出性能统计结果。

per top [ -e <event> | --event=EVENT ] [ -p <pid> ]

10.6.3 perf record/report 的使用

       perf top 实时展示了系统的性能信息,但它并不保存数据,所以无法用于后续总体的性能分析,perf record 解决了这一问题。perf record 用于一段时间内或程序全过程的性能事件做统计记录,并将结果保存在名为 perf.data 的文件中,这个文件不能直接查看,需要使用 perf report 来帮助读取 perf.data 文件内容,并显示分析数据到输出终端。

perf record [ -e <event> | --event=EVENT ] [ -p <pid> | start_command ]
perf report [ file_name ]

       perf record 的默认监控事件类型也是 cycles,如需指定其他事件类型,可以使用参数“-e”或者“–event”。监控可以在程序启动之前开始,也可以在程序运行中(-p)。per report 默认加载当前目录下的名为 perf.data 的性能文件,如果文件不在当前目录或者名称不是 perf.data,可以通过指定文件路径和文件名加载。
       采样时间尽可能长一些,这样能更精准定位到热点指令。

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值