书名:ARM64体系结构编程与实践
ISBN:978-7-115-58210-2
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
编 著 奔跑吧Linux社区
责任编辑 谢晓芳
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书旨在详细介绍ARM64体系结构的相关技术。本书首先介绍了ARM64体系结构的基础知识、搭建树莓派实验环境的方法,然后讲述了ARM64指令集中的加载与存储指令、算术与移位指令、比较与跳转等指令以及ARM64指令集中的陷阱,接着讨论了GNU汇编器、链接器、链接脚本、GCC内嵌汇编代码、异常处理、中断处理、GIC-V2,最后剖析了内存管理、高速缓存、缓存一致性、TLB管理、内存屏障指令、原子操作、操作系统等内容。
本书适合嵌入式开发人员阅读。
在阅读本书之前,请读者尝试完成以下自测题,从而了解自己对ARM64体系结构的掌握程度。一共有20道题,每道题5分,总分100分。
1.A64指令集支持64位宽的数据和地址寻址,为什么指令的编码宽度只有32位?
2.下面几条MOV指令中,哪些能执行成功?哪些会执行失败?
mov x0, 0x1234
mov x0, 0x1abcd
mov x0, 0x12bc0000
mov x0, 0xffff0000ffff
3.在下面的示例代码中,X0和X1寄存器的值分别是多少?
string1:
.string "Booting at EL"
ldr x0, string1
ldr x1, =string1
4.在下面的示例代码中,X0寄存器的值是多少?
mov x1, #3
mov x2, #1
sbc x0, x1, x2
5.检查数组array[0, index−1]是否越界需要判断两个条件,一是输入值是否大于或等于index,二是输入值是否小于0。如下两条指令可实现数组越界检查的功能,其中X0寄存器存储了数组的边界index,X1为输入值input。请解释这两条指令为什么能实现数组越界检查。
subs xzr,x1,x0
b.hs OutOfIndex
6.下面是kernel_ventry宏的定义。
.macro kernel_ventry, el, label
b el\()\el\()_\label
.endm
下面的语句调用kernel_ventry宏,请解释该宏是如何展开的。
kernel_ventry 1, irq
7.关于链接器,请解释链接地址、虚拟地址以及加载地址。当一个程序的代码段的链接地址与加载地址不一致时,我们应该怎么做才能让程序正确运行?
8.在ARM64处理器中,异常发生后CPU自动做了哪些事情?软件需要做哪些事情?在发生异常后,CPU是返回发生异常的指令还是下一条指令?什么是中断现场?对于ARM64处理器来说,中断现场应该保存哪些内容?中断现场保存到什么地方?
9.为什么页表要设计成多级页表?直接使用一级页表是否可行?多级页表又引入了什么问题?请简述ARM64处理器的4级页表的映射过程,假设页面粒度为4 KB,地址宽度为48位。
10.ARMv8体系结构处理器主要提供两种类型的内存属性,分别是普通类型内存(normal memory)和设备类型内存(device memory),它们之间有什么区别?
11.在使能MMU时,为什么需要建立恒等映射?
12.请简述直接映射、全相连映射以及组相连映射的高速缓存的区别。什么是高速缓存的重名问题?什么是高速缓存的同名问题?VIPT类型的高速缓存会产生重名问题吗?
13.在ARM64处理器中,什么是内部共享和外部共享的高速缓存?什么是PoU和PoC?
14.假设系统中有4个CPU,每个CPU都有各自的一级高速缓存,处理器内部实现的是MESI协议,它们都想访问相同地址的数据a,大小为64字节,这4个CPU的高速缓存在初始状态下都没有缓存数据a。在T0时刻,CPU0访问数据a。在T1时刻,CPU1访问数据a。在T2时刻,CPU2访问数据a。在T3时刻,CPU3想更新数据a的内容。请依次说明,T0~T3时刻,4个CPU中高速缓存行的变化情况。
15.DMA缓冲区和高速缓存容易产生缓存一致性问题。从DMA缓冲区向设备的FIFO缓冲区搬运数据时,应该如何保证缓存一致性?从设备的FIFO缓冲区向DMA缓冲区搬运数据时,应该如何保证缓存一致性?
16.为什么操作系统在切换(或修改)页表项时需要先刷新对应的TLB表项后切换页表项?
17.下面是关于无效指令高速缓存的代码片段,请解释为什么在使指令高速缓存失效之后要发送一个IPI,而且这个IPI的回调函数还是空的。
void flush_icache_range(unsigned long start, unsigned long end)
{
flush_icache_range(start, end);
smp_call_function(do_nothing, NULL, 1);
}
18.假设在下面的执行序列中,CPU0先执行了a=1和b=1,接着CPU1一直循环判断b是否等于1,如果等于1则跳出while循环,最后执行“assert (a== 1)”语句来判断a是否等于1,那么assert语句有可能会出错吗?
CPU0 CPU1
-------------------------------------------------------------
void func0() void func1()
{ {
a = 1; while (b == 0) continue;
b = 1; assert (a == 1)
} }
19.假设CPU0使用LDRXB/STXRB指令对0x341B0地址进行独占访问操作,CPU1也使用LDRXB/STXRB指令对0x341B4地址进行独占读操作,CPU1能成功独占访问吗?
20.假设函数调用关系为main()→func1()→func2(),请画出ARM64体系结构的函数栈的布局。
以上题目的答案都分布在本书的各章中。
站在2021年来看处理器的发展,x86_64体系结构与ARM64体系结构是目前市场上的主流处理器体系结构,而RISC-V有可能成为第三大体系结构。在手机芯片和嵌入式芯片领域,ARM64体系结构的处理器占了90%以上的市场份额,而在个人计算机和服务器领域,x86_64体系结构的处理器占了90%以上的市场份额。在这样的背景下,越来越多的芯片公司(例如海思、展讯、瑞芯微、全志等)基于ARM64体系结构来打造国产芯片。此外,苹果公司也切换到ARM64体系结构上,在2020年年底发布的基于ARM64体系结构的M1处理器芯片惊艳了全球。
基于ARM64体系结构处理器打造的产品越来越多,ARM64生态也越来越繁荣。面对几千页的英文原版ARM公司官方技术手册,不少开发者感到力不从心。有不少开发者希望有一本快速入门的ARM64体系结构编程图书,来帮助他们快速入门与提高。出于这个目的,奔跑吧Linux社区组织国内优秀的工程师,以社区合作的方式编写了本书,结合大学课程特色以及实际工程项目经验,精心制作了几十个有趣的实验,读者可以通过实验来深入学习和理解ARM64体系结构与编程。
本书有如下一些特色。
本书主要介绍ARM64体系结构的相关内容。本书重点介绍ARM64指令集、GNU汇编器、链接器、ARM64内存管理、高速缓存管理等。在每章开始之前会先列出一些思考题,读者可以围绕这些题目进行深入学习。
本书一共有23章,包含如下内容。
第1章主要介绍ARMv8/ARMv9体系结构基础知识以及Cortex-A72处理器等内容。
第2章介绍树莓派4B开发板的情况,以及如何利用树莓派4B来搭建一个实验环境。
第3章讨论A64指令集中加载与存储指令的使用以及常见陷阱。
第4章介绍A64指令集中的算术与移位指令。
第5章介绍A64指令集中的比较与跳转指令。
第6章介绍A64指令集中其他重要指令,例如PC相对地址加载指令、内存独占访问指令、异常处理指令、系统寄存器访问指令、内存屏障指令等。
第7章总结A64指令集常见的陷阱。
第8章介绍GNU汇编器的语法、常见伪指令、AArch64依赖特性等内容。
第9章介绍链接器的使用、链接脚本以及重定位等内容。
第10章介绍GCC内嵌汇编代码的语法、内嵌汇编宏的使用以及常见错误等内容。
第11章介绍ARM64体系结构异常处理的触发与返回、异常向量表、异常现场、同步异常的解析等相关内容。
第12章介绍ARM64体系结构中断处理的基本概念和流程,包括树莓派4B上的传统中断控制器、保存和恢复中断现场的方法等。
第13章介绍GIC-V2的相关内容,包括中断源分配、中断路由、树莓派4B上的GIC-400等。
第14章介绍ARM64体系结构下的内存管理,包括ARM64的页表、页表项属性、页表遍历过程、内存属性以及恒等映射等相关内容。
第15章介绍高速缓存的基础知识,包括高速缓存的工作原理、映射方式,虚拟高速缓存与物理高速缓存,重名与同名问题,高速缓存的共享属性、维护指令等相关内容。
第16章介绍缓存一致性相关问题,包括缓存一致性的分类、MESI协议、CCI与CCN缓存一致性控制器、高速缓存伪共享等内容。
第17章介绍TLB基础知识、TLB重名与同名问题、ASID、TLB管理指令等相关内容。
第18章介绍内存屏障指令基础知识,包括内存屏障指令产生的原因、ARM64中的内存屏障指令等相关内容。
第19章介绍如何使用内存屏障指令。
第20章介绍原子操作,包括原子操作基本概念、原子内存访问指令、独占内存访问工作原理、原子内存访问操作指令等相关内容。
第21章介绍与操作系统相关的内容,包括64位编程下常见的C语言陷阱、ARM64函数调用标准、ARM64栈布局、简易进程调度器等内容。
第22章介绍浮点运算以及NEON指令方面的相关内容。
第23章介绍SVE以及SVE2指令,还结合3个实际案例分析如何使用SVE/SVE2指令来进行优化。
本书由奔跑吧Linux社区中众多工程师共同完成。奔跑吧Linux社区由一群热爱开源的热心工程师组成,参与编写本书的人有魏汉武、寇朝阳、王乐、王晓华、蔡琛、余云波、牛立群、代祥军、何花、徐国栋、徐彦飞、郑律、张馨雨、Xiao Guangrong、Gavin Guo、Horry Zheng、Cherry Chen、Peter Chen、贾献华等。在编写过程中,作者还得到了大连理工大学软件学院吴国伟老师、上海交通大学软件学院古金宇老师以及南昌大学信息工程学院陈悦老师的支持和帮助。感谢这些老师的帮助。感谢Linaro组织的徐国栋认真审阅了大部分书稿,提出了很多修改意见。另外,本书还得到安谋科技教育计划的支持和帮助,特别感谢宋斌老师的无私帮助。
由于作者知识水平有限,书中难免存在纰漏,敬请各位读者批评指正。要下载本书配套的实验环境、实验参考代码以及配套视频课程,请扫描下方的二维码,在“奔跑吧Linux社区”微信公众号中输入“arm64”。
为了帮助读者更好地阅读本书以及完成本书的实验,我们对本书一些术语、实验环境做了一些约定。
本书介绍ARMv8/v9系列的体系结构方面的内容,书中提到的ARMv8体系结构指的是运行在AArch64状态的ARMv8-A处理器体系结构,ARMv9体系结构指的是ARMv9-A处理器体系结构。
本书提到的ARM64体系结构指的是运行在AArch64状态的处理器体系结构,本书混用了ARM64和AArch64这两个术语。本书不介绍AArch32状态的处理器体系结构。
本书基于Linux内核以及小型OS(BenOS)进行讲解。Linux内核采用Linux 5.0版本。本书大部分实验以BenOS为基础,使读者从最简单的裸机程序不断进行扩展,最终完成一个具有内存管理、进程调度、系统调用等现代操作基本功能的小操作系统,从而学习和掌握ARM64体系结构的相关知识。在实验的设计过程中参考了Linux内核等开源代码的实现,在此对开源社区表示感谢。
本书推荐的实验环境如下。
读者在安装完Ubuntu Linux 20.04系统后可以通过如下命令来安装本书需要的软件包。
$ sudo apt update -y
$ sudo apt install net-tools libncurses5-dev libssl-dev build-essential openssl qemu-system-arm libncurses5-dev gcc-aarch64-linux-gnu git bison flex bc vim universal-ctags cscope cmake python3-dev gdb-multiarch
我们基于VMware映像搭建了全套开发环境,读者可以通过“奔跑吧Linux社区”微信公众号来获取下载地址。使用本书配套的VMware映像可以减少配置开发环境带来的麻烦。
本书的所有实验都可以在如下两个实验平台上完成。
1)树莓派4B实验平台
实验中使用的设备如下。
树莓派4B开发板。
MicroSD卡。
USB MicroSD读卡器。
USB转串口线。
J-Link EDU仿真器。
我们可以使用真实的树莓派开发板或者使用QEMU模拟器来模拟树莓派,读者可以根据实际情况来选择。
2)QEMU + ARM64实验平台
我们基于QEMU + ARM64实现了一个简易的Linux/ARM64系统,本书部分实验(例如第22章和第23章的实验)可以基于此系统来完成。它有如下新特性。
支持ARM64体系结构。
支持Linux 5.0内核。
支持Debian根文件系统。
要下载本书配套的QEMU+ARM64实验平台的仓库,请访问GitHub网站,搜索“running linuxkernel/runninglinuxkernel-5.0”。
本书为了节省篇幅,大部分实验只列出了实验目的和实验要求,希望读者能独立完成实验。
本书提供部分实验的参考代码,在GitHub网站,搜索“runninglinuxkernel/arm64programming practice”即可找到。
本书有如下的配套资料。
部分实验参考代码。
实验平台VMware/VirtualBox映像。
配套视频课程。
读者可以通过微信公众号“奔跑吧Linux社区”获取下载地址。
本书在编写过程中参考了 ARM 公司的大量芯片手册和技术资料以及与 GNU 工具链相关的文档。下面是本书涉及的技术手册,这些技术手册都是公开发布的,读者可以在ARM官网以及GNU官网上下载。
ARM64指令集允许使用大写形式或者小写形式来书写汇编代码,在ARM官方的芯片手册中默认采用大写形式,而GNU AS汇编器默认使用小写形式,如Linux内核的汇编代码。本书的示例代码采用小写形式,正文说明采用大写形式。
[1] Ubuntu Linux 20.04内置的QEMU 4.2还不支持树莓派4B。若要在QEMU中模拟树莓派4B,还需要打上一系列补丁,然后重新编译QEMU。本书配套的实验平台VMware映像会提供支持树莓派4B的QEMU程序。
本书由异步社区出品,社区(https://www.epubit.com/)为您提供后续服务。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可,如下图所示。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区投稿(直接访问www.epubit.com/contribute即可)。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
异步社区
微信服务号
本章思考题
1.ARMv8体系结构处理器包含多少个通用寄存器?
2.AArch64执行状态包含多少个异常等级?它们分别有什么作用?
3.请简述PSTATE寄存器中NZCV标志位的含义。
4.请简述PSTATE寄存器中DAIF异常掩码标志位的含义。
本章主要介绍ARM64体系结构基础知识。
ARM公司主要向客户提供处理器IP。通过这种独特的盈利模式,ARM软硬件生态变得越来越强大。表1.1展示了ARM公司重大的历史事件。
表1.1 ARM公司重大的历史事件
时 间 |
重 大 事 件 |
---|---|
1978年 |
在英国剑桥创办了CPU(Cambridge Processing Unit)公司 |
1985年 |
第一款ARM处理器问世,它采用RISC架构,简称ARM(Acorn RISC Machine) |
1995年 |
发布ARM7处理器核心,它支持3级流水线和ARMv4指令集 |
1997年 |
发布了ARM9处理器核心,它支持5级流水线,支持ARMv4T指令集,支持MMU内存管理以及指令/数据高速缓存。兼容ARMv4T指令集的处理器核心有ARM920T,典型SoC芯片是三星S3C2410 |
2003年 |
发布ARM11处理器,它支持8级流水线和ARMv6指令集,典型的IP核心有ARM1176JZF |
2005年 |
发布Cortex-A8处理器核心,第一个引入超标量技术的ARM处理器 |
2007年 |
发布Cortex-A9处理器核心,它引入了乱序执行和猜测执行机制,并扩大了L2高速缓存的容量 |
2010年 |
发布Cortex-A15处理器核心,它的最高主频可以到2.5 GHz,最多可支持8个处理器内核,单个簇最多支持4个处理器内核 |
2012年 |
发布64位Cortex-A53和Cortex-A57处理器内核 |
2015年 |
发布Cortex-A72处理器内核。树莓派4B开发板采用Cortex-A72处理器内核 |
2019年 |
发布Neoverse系列处理器,它细分为E系列、N系列和V系列。V系列适用于性能优先的场景,例如高性能计算(HPC)。N系列适用于需要均衡的CPU设计优化的场景,例如网络应用、智能网卡、5G应用等,以提供出色的能耗比。E系列适用于高性能与低功耗的场景,例如网络数据平面处理器、5G低功耗网关等 |
2021年 |
发布ARMv9体系结构。Cortex-X2处理器支持ARMv9.0体系结构 |
ARM体系结构是一种硬件规范,主要是用来约定指令集、芯片内部体系结构(如内存管理、高速缓存管理)等。以指令集为例,ARM体系结构并没有约定每一条指令在硬件描述语言(Verilog或VHDL)中应该如何实现,它只约定每一条指令的格式、行为规范、参数等。为了降低客户基于ARM体系结构开发处理器的难度,ARM公司通常在发布新版本的体系结构之后,根据不同的应用需求开发出兼容体系结构的处理器IP,然后授权给客户。客户获得处理器IP之后,再用它来设计不同的SoC芯片。以ARMv8体系结构为例,ARM公司先后开发出Cortex-A53、Cortex-A55、Cortex-A72、Cortex-A73等多款处理器IP。
ARM公司一般有两种授权方式。
从最早的ARM处理器开始,ARM体系结构已经从v1版本发展到目前的v8版本。在每一个版本的体系结构里,指令集都有相应的变化,其主要变化如表1.2所示。
表1.2 ARM体系结构的变化
ARM体系结构版本 |
典型处理器核心 |
主 要 特 性 |
---|---|---|
v1 |
— |
仅支持26位地址空间 |
v2 |
— |
新增乘法指令和乘加法指令、支持协处理器指令等 |
v3 |
— |
地址空间扩展到32位,新增SPSR和CPSR等 |
v4 |
ARM7TDMI/ARM920T |
新增Thumb指令集等 |
v5 |
ARM926EJ-S |
新增Jazelle和VFPv2扩展 |
v6 |
ARM11 MPCore |
新增SIMD、TrustZone以及Thumb-2扩展 |
v7 |
Cortex-A8/Cortex-A9 |
增强NEON和VFPv3/v4扩展 |
v8 |
Cortex-A72 |
同时支持32位以及64位指令集的处理器体系结构 |
v9 |
Cortex-X2 |
支持可伸缩矢量扩展计算、机密计算体系结构 |
ARM体系结构又根据不同的应用场景分成如下3种系列。
ARMv8是ARM公司发布的第一代支持64位处理器的指令集和体系结构。它在扩充64位寄存器的同时提供了对上一代体系结构指令集的兼容,因此它提供了运行32位和64位应用程序的环境。
ARMv8体系结构除了提高了处理能力,还引入了很多吸引人的新特性。
ARMv8体系结构一共有8个小版本,分别是ARMv8.0、ARMv8.1、ARMv8.2、ARMv8.3、ARMv8.4、ARMv8.5、ARMv8.6、ARMv8.7,每个小版本都对体系结构进行小幅度升级和优化,增加了一些新的特性。
下面介绍市面上常见的采用ARMv8体系结构的处理器(简称ARMv8处理器)内核。
ARM处理器实现的是精简指令集体系结构。在ARMv8体系结构中有如下一些基本概念和定义。
<register_name>_Elx //最后一个字母 x 可以表示0、1、2、3
如SP_EL0表示在EL0下的SP寄存器,SP_EL1表示在EL1下的SP寄存器。
本书重点介绍ARMv8体系结构下的AArch64执行状态以及A64指令集,对AArch32执行状态、A32以及T32指令集不做过多介绍,感兴趣的读者可以阅读ARMv8相关技术手册。
指令集是处理器体系结构设计的重点之一。ARM公司定义与实现的指令集一直在变化和发展中。ARMv8体系结构最大的改变是增加了一个新的64位的指令集,这是早前ARM指令集的有益补充和增强。它可以处理64位宽的寄存器和数据并且使用64位的指针来访问内存。这个新的指令集称为A64指令集,运行在AArch64状态下。ARMv8兼容旧的32位指令集——A32指令集,它运行在AArch32状态下。
A64指令集和A32指令集是不兼容的,它们是两套完全不一样的指令集,它们的指令编码是不一样的。需要注意的是,A64指令集的指令宽度是32位,而不是64位。
ARMv8处理器支持两种执行状态——AArch64状态和AArch32状态。AArch64状态是ARMv8新增的64位执行状态,而AArch32是为了兼容ARMv7体系结构的32位执行状态。当处理器运行在AArch64状态下时,运行A64指令集;而当运行在AArch32状态下时,可以运行A32指令集或者T32指令集。
如图1.1所示,AArch64状态的异常等级(exception level)确定了处理器当前运行的特权级别,类似于ARMv7体系结构中的特权等级。
▲图1.1 AArch64状态的异常等级
ARMv8体系结构允许切换应用程序的运行模式。如在一个运行64位操作系统的ARMv8处理器中,我们可以同时运行A64指令集的应用程序和A32指令集的应用程序,但是在一个运行32位操作系统的ARMv8处理器中就不能运行A64指令集的应用程序了。当需要运行A32指令集的应用程序时,需要通过一条管理员调用(Supervisor Call,SVC)指令切换到EL1,操作系统会做任务的切换并且返回AArch32的EL0,从而为这个应用程序准备好AArch32状态的运行环境。
ARMv8支持如下几种数据宽度。
AArch64执行状态支持31个64位的通用寄存器,分别是X0~X30寄存器,而AArch32状态支持16个32位的通用寄存器。
除用于数据运算和存储之外,通用寄存器还可以在函数调用过程中起到特殊作用,ARM64体系结构的函数调用标准和规范对此有所约定,如图1.2所示。
在AArch64状态下,使用X(如X0、X30等)表示64位通用寄存器。另外,还可以使用W来表示低32位的数据,如W0表示X0寄存器的低32位数据,W1表示X1寄存器的低32位数据,如图1.3所示。
▲图1.2 AArch64状态的31个通用寄存器
▲图1.3 64位通用寄存器和低32位数据
AArch64体系结构使用PSTATE寄存器来表示当前处理器状态(processor state),如表1.3所示。
表1.3 PSTATE寄存器
分 类 | 字 段 | 描 述 |
---|---|---|
条件标志位 | N | 负数标志位。 在结果是有符号的二进制补码的情况下,如果结果为负数,则N=1;如果结果为非负数,则N=0 |
Z | 0标志位。 如果结果为0,则Z=1;如果结果不为0,则Z=0 |
|
C | 进位标志位。 当发生无符号数溢出时,C=1。 其他情况下,C=0 |
|
V | 有符号数溢出标志位。 ● 对于加/减法指令,在操作数和结果是有符号的整数时,如果发生溢出,则V=1;如果未发生溢出,则V=0。 ● 对于其他指令,V通常不发生变化 |
|
执行状态控制 | SS | 软件单步。该位为1,说明在异常处理中使能了软件单步功能 |
IL | 不合法的异常状态 | |
nRW | 当前执行状态。 ● 0:处于AArch64状态。 ● 1:处于AArch32状态 |
|
执行状态控制 | EL | 当前异常等级。 ● 0:表示EL0。 ● 1:表示EL1。 ● 2:表示EL2。 ● 3:表示EL3 |
SP | 选择SP寄存器。当运行在EL0时,处理器选择EL0的SP寄存器,即SP_EL0;当处理器运行在其他异常等级时,处理器可以选择使用SP_EL0或者对应的SP_ELn寄存器 | |
异常掩码标志位 | D | 调试位。使能该位可以在异常处理过程中打开调试断点和软件单步等功能 |
A | 用来屏蔽系统错误(SError) | |
I | 用来屏蔽IRQ | |
F | 用来屏蔽FIQ | |
访问权限 | PAN | 特权模式禁止访问(Privileged Access Never)位是ARMv8.1的扩展特性。 ● 1:在EL1或者EL2访问属于EL0的虚拟地址时会触发一个访问权限错误。 ● 0:不支持该功能,需要软件来模拟 |
UAO | 用户访问覆盖标志位,是ARMv8.2的扩展特性。 ● 1:当运行在EL1或者EL2时,没有特权的加载存储指令可以和有特权的加载存储指令一样访问内存,如LDTR指令。 ● 0:不支持该功能 |
ARMv8体系结构除支持31个通用寄存器之外,还提供多个特殊的寄存器,如图1.4所示。
▲图1.4 特殊寄存器
ARMv8体系结构提供两个零寄存器(zero register),这些寄存器的内容全是0,可以用作源寄存器,也可以用作目标寄存器。WZR是32位的零寄存器,XZR是64位的零寄存器。
PC指针寄存器通常用来指向当前运行指令的下一条指令的地址,用于控制程序中指令的运行顺序,但是编程人员不能通过指令来直接访问它。
ARMv8体系结构支持4个异常等级,每一个异常等级都有一个专门的SP寄存器SP_ELn,如处理器运行在EL1时选择SP_EL1寄存器作为SP寄存器。
当处理器运行在比EL0高的异常等级时,处理器可以访问如下寄存器。
当处理器运行在EL0时,它只能访问SP_EL0,而不能访问其他高级的SP寄存器。
当我们运行一个异常处理程序时,处理器的备份程序会保存到备份程序状态寄存器(Saved Program Status Register,SPSR)里。当异常将要发生时,处理器会把PSTATE寄存器的值暂时保存到SPSR里;当异常处理完成并返回时,再把SPSR的值恢复到PSTATE寄存器。SPSR的格式如图1.5所示。SPSR的重要字段如表1.4所示。
▲图1.5 SPSR的格式
表1.4 SPSR的重要字段
字 段 |
描 述 |
---|---|
N |
负数标志位 |
Z |
零标志位 |
C |
进位标志位 |
V |
有符号数溢出标志位 |
DIT |
与数据无关的指令时序(Data Independent Timing),ARMv8.4的扩展特性 |
UAO |
用户访问覆盖标志位,ARMv8.2的扩展特性 |
PAN |
特权模式禁止访问位,ARMv8.1的扩展特性 |
SS |
表示是否使能软件单步功能。若该位为1,说明在异常处理中使能了软件单步功能 |
IL |
不合法的异常状态 |
D |
调试位。使能该位可以在异常处理过程中打开调试断点和软件单步等功能 |
A |
用来屏蔽系统错误 |
I |
用来屏蔽IRQ |
F |
用来屏蔽FIQ |
M[4] |
用来表示异常处理过程中处于哪个执行状态,若为0,表示AArch64状态 |
M[3:0] |
异常模式 |
ELR存放了异常返回地址。
该寄存器表示PSTATE寄存器中的EL字段,其中保存了当前异常等级。使用MRS指令可以读取当前异常等级。
该寄存器表示PSTATE寄存器中的{D,A,I,F}字段。
该寄存器表示PSTATE寄存器中的SP字段,用于在SP_EL0和SP_ELn中选择SP寄存器。
PAN寄存器表示PSTATE寄存器中的PAN(Privileged Access Never,特权禁止访问)字段。可以通过MSR和MRS指令来设置PAN寄存器。当内核态拥有访问用户态内存或者执行用户态程序的能力时,攻击者就可以利用漏洞轻松地执行用户的恶意程序。为了修复这个漏洞,在ARMv8.1中新增了PAN特性,防止内核态恶意访问用户态内存。如果内核态需要访问用户态内存,那么需要主动调用内核提供的接口,例如copy_from_user()或者copy_from_user()函数。
PAN寄存器的值如下。
该寄存器表示PSTATE寄存器中的UAO(User Access Override,用户访问覆盖)字段。我们可以通过MSR和MRS指令设置UAO寄存器。UAO为1表示在EL1和EL2执行这非特权指令(例如LDTR、STTR)的效果与特权指令(例如LDR、STR)是一样的。
该寄存器表示PSTATE寄存器中的{N,Z,C,V}字段。
除上面介绍的通用寄存器和特殊寄存器之外,ARMv8体系结构还定义了很多的系统寄存器,通过访问和设置这些系统寄存器来完成对处理器不同的功能配置。在ARMv7体系结构中,我们需要通过访问CP15协处理器来间接访问这些系统寄存器,而在ARMv8体系结构中没有协处理器,可直接访问系统寄存器。ARMv8体系结构支持如下7类系统寄存器:
系统寄存器支持不同的异常等级的访问,通常系统寄存器会使用“Reg_ELn”的方式来表示。
程序可以通过MSR和MRS指令访问系统寄存器。
mrs X0, TTBR0_EL1 //把TTBR0_EL1的值复制到X0寄存器
msr TTBR0_EL1, X0 //把X0寄存器的值复制到TTBR0_EL1
基于ARMv8体系结构设计的处理器内核有很多,例如常见的Cortex-A53、Cortex-A55、Cortex-A72、Cortex-A77以及Cortex-A78等。本书的实验环境采用树莓派4B开发板,内置了4个Cortex-A72处理器内核,因此我们重点介绍Cortex-A72处理器内核。
Cortex-A72是2015年发布的一个高性能处理器内核。它最多可以支持4个内核,内置L1和L2高速缓存,如图1.6所示。
Cortex-A72处理器支持如下特性。
▲图1.6 Cortex-A72处理器内部体系结构
指令预取单元用来从L1指令高速缓存中获取指令,并在每个周期向指令译码单元最多发送3条指令。它支持动态和静态分支预测。指令预取单元包括如下功能。
指令译码单元对以下指令集进行译码:
指令译码单元会执行寄存器重命名,通过消除写后写(WAW)和读后写(WAR)的冲突来实现乱序执行。
指令分派单元控制译码后的指令何时被分派到执行管道以及返回的结果何时终止。它包括以下部分:
加载/存储单元(LSU)执行加载和存储指令,包含L1数据存储系统。另外,它还处理来自L2内存子系统的一致性等服务请求。加载/存储单元的特性如下。
L1内存子系统包括指令内存系统和数据内存系统。
L1指令内存系统包括如下特性。
L1数据内存系统包括如下特性。
MMU用来实现虚拟地址到物理地址的转换。在AArch64状态下支持长描述符的页表格式,支持不同的页面粒度,例如4 KB、16 KB以及64 KB页面。
MMU包括以下部分:
TLB不仅支持8位或者16位的ASID,还支持VMID(用于虚拟化)。
L2内存子系统不仅负责处理每个处理器内核的L1指令和数据高速缓存未命中的情况,还通过ACE或者CHI连接到内存系统。其特性如下。
2021年ARM公司发布ARMv9体系结构。ARMv9体系结构在兼容ARMv8体系结构的基础上加入了一些新的特性,其中:
ARMv9体系结构新加入的特性包括:
另外,ARMv9体系结构对AArch32执行环境的支持发生了变化。在EL0中,ARM64体系结构对AArch32状态的支持是可选的,取决于芯片设计;而在EL1/EL2/EL3中,ARM64体系结构将不再提供对AArch32状态的支持。
本书中大部分实验是基于BenOS的。BenOS是一个简单的小型操作系统实验平台,可以在树莓派4B上运行。我们可以通过在最简单裸机程序上慢慢添加功能,实现一个具有任务调度功能的小OS。
BenOS实验可以在树莓派4B开发板上运行,也可以在QEMU的模拟环境中运行。读者可以根据实际情况选择。
树莓派(Raspberry Pi)是树莓派基金会为普及计算机教育而设计的开发板。它以低廉的价格、强大的计算能力以及丰富的教学资源得到全球技术爱好者的喜爱。
树莓派截至2020年一共发布了4代产品。
建议读者选择树莓派4B作为实验硬件平台。
树莓派4B采用性能强大的Cortex-A72处理器内核,性能比树莓派3B快3倍。树莓派4B的结构如图2.1所示。
▲图2.1 树莓派4B的结构
表2.1对树莓派3B和树莓派4B做了比较。
表2.1 树莓派3B和树莓派4B的比较
对比项 |
树莓派3B |
树莓派4B |
---|---|---|
SoC |
博通BCM2837B |
博通BCM2711 |
CPU |
Cortex-A53处理器内核,4核 |
Cortex-A72处理器内核,4核 |
GPU |
VideoCore IV |
400 MHz VideoCore VI |
内存 |
1 GB DDR2内存 |
1 GB ~ 8 GB DDR4内存 |
视频输出 |
单个HDMI |
双micro HDMI |
分辨率 |
1920×1200像素 |
4K像素 |
USB端口 |
4个USB 2.0 |
两个USB 3.0,两个USB 2.0 |
有线网络 |
330 Mbit/s以太网 |
千兆以太网 |
无线网络 |
802.11ac |
802.11ac |
蓝牙 |
4.2 |
5.0 |
充电端口 |
micro USB |
Type-C USB |
树莓派4B采用的是博通BCM2711芯片。BCM2711芯片在BCM2837芯片的基础上做了如下改进。
BCM2711芯片支持两种地址模式。
树莓派4B默认情况下使用低地址模式,本书配套的实验也默认使用低地址模式,读者也可以通过修改配置文件来使能35位全地址模式。
熟悉和掌握一个处理器体系结构最有效的方法是多做练习、多做实验。本书采用树莓派4B作为硬件实验平台。
本章需要准备的实验设备如图2.2所示。
▲图2.2 硬件实验平台
▲图2.3 J-Link EDU仿真器
要在树莓派4B上运行实验代码,我们需要一根USB转串口线,这样在系统启动时便可通过串口输出信息来协助调试。读者可从网上商店购买USB转串口线,图2.4所示是某个厂商售卖的一款USB转串口线。串口一般有3根线。另外,串口还有一根额外的电源线(可选)。
▲图2.4 USB转串口线
树莓派4B支持包含40个GPIO引脚的扩展接口,这些扩展接口的定义如图2.5所示。根据扩展接口的定义,我们需要把串口的三根线连接到扩展接口,如图 2.6所示。
在Windows 10操作系统中需要在设备管理器中查看串口号,如图2.7所示。你还需要在Windows 10操作系统中安装用于USB转串口的驱动。
▲图2.5 树莓派扩展接口的定义
▲图2.6 将串口线连接到树莓派扩展接口
▲图2.7 在设备管理器中查看串口号
接上USB电源,在串口终端软件(如PuTTY或MobaXterm等)中查看是否有输出,如图2.8所示。即使没有插入MicroSD卡,串口也能输出信息,如果能看到串口输出信息,那么说明串口设备已经配置。这些信息是树莓派固件输出的。图 2.8 中的日志信息显示系统没有找到MicroSD卡。
▲图2.8 在串口终端软件中查看是否有输出
树莓派的映像文件需要安装(烧录)到MicroSD卡里。第一次使用树莓派时,我们先给树莓派安装一个官方的OS——Raspberry Pi OS(简称树莓派OS),用来验证开发板是否正常工作。另外,要在树莓派上运行BenOS,也需要准备一张格式化好的MicroSD卡。格式化的要求如下。
下面是安装树莓派OS的步骤。
(1)到树莓派官方网站上下载ARM64版本的树莓派OS映像文件,例如2021-03-04-raspios-buster-arm64.img。
(2)为了将映像文件烧录到 MicroSD 卡中,将MicroSD卡插入 USB 读卡器。在 Windows 主机上,安装Win32DiskImager软件来进行烧录,如图 2.9 所示。而在Linux主机上通过简单地执行dd命令将映像文件烧录至MicroSD卡。
▲图2.9 烧录映像文件
#dd if= 2021-03-04-raspios-buster-arm64.img of=/dev/sdX status=progress
其中,/dev/sdX中的X需要修改为存储卡实际的映射值,可以通过“fdisk -l”命令来查看。
(3)把MicroSD卡重新插入主机,此时会看到有一个名为“boot”的分区。修改boot分区里面的config.txt配置文件,在这个文件中新增两行,目的是使能串口输出功能。
uart_2ndstage=1
enable_uart=1
(4)启动树莓派。把MicroSD卡插入树莓派中,通过USB线给树莓派供电。树莓派OS的用户名为pi,密码为raspberry。
(5)配置树莓派 4B 上的 Wi-Fi。使用树莓派上的配置工具来配置,在串口中输入如下命令。
$ sudo raspi-config
(6)选择System Options→S1 Wireless LAN,配置SSID和密码,如图2.10所示。
(7)更新系统,这样会自动更新树莓派4B上的固件。
sudo apt update
sudo apt full-upgrade
sudo reboot
▲图2.10 配置SSID和密码
经过上面的步骤,我们得到格式化好的boot分区和最新版本的树莓派固件。boot分区主要包括如下几个文件。
了解和熟悉如何在树莓派4B上运行最简单的BenOS程序。
首先,在Linux主机中安装相关工具[3]。
$ sudo apt-get install apt-get install qemu-system-arm libncurses5-dev gcc-aarch64-linux-gnu build-essential git bison flex libssl-dev
然后,在Linux主机上使用make命令编译BenOS。
$ cd benos
$ make
编译完成之后会生成benos.bin可执行文件以及benos.elf文件。在把benos.bin可执行文件放到树莓派4B上之前,我们可以使用QEMU虚拟机来模拟树莓派运行,可直接输入“make run”命令。
$ make run
qemu-system-aarch64 -machine raspi4 -nographic -kernel benos.bin
Welcome BenOS!
把benos.bin文件复制到MicroSD卡的boot分区(可以通过USB的MicroSD读卡器进行复制),并且修改boot分区里面的config.txt文件。
<config.txt文件>
[pi4]
kernel=benos.bin
max_framebuffers=2
[pi3]
kernel=benos.bin
[all]
arm_64bit=1
enable_uart=1
kernel_old=1
disable_commandline_tags=1
插入MicroSD卡到树莓派,连接USB电源线,使用Windows端的串口软件可以看到输出,如图2.11所示。
▲图2.11 输出欢迎语句
我们可以使用GDB和QEMU虚拟机单步调试裸机程序。
本节以实验2-1为例,在终端启动QEMU虚拟机的gdbserver。
$ qemu-system-aarch64 -machine raspi4 -serial null -serial mon:stdio -nographic -kernel benos.bin -S -s
在另一个终端输入如下命令来启动GDB,可以使用aarch64-linux-gnu-gdb命令或者gdb-multiarch命令。
$ aarch64-linux-gnu-gdb --tui build/benos.elf
在GDB的命令行中输入如下命令。
(gdb) target remote localhost:1234
(gdb) b _start
Breakpoint 1 at 0x0: file src/boot.S, line 7.
(gdb) c
此时,可以使用GDB命令来进行单步调试,如图2.12所示。
▲图2.12 使用GDB调试裸机程序
调试BenOS是通过QEMU虚拟机中内置的gdbserver来实现的,但gdbserver只能调试在QEMU虚拟机上运行的程序。如果需要调试在硬件开发板上运行的程序,例如把BenOS放到树莓派上运行,那么GDB与QEMU虚拟机就无能为力了。如果我们编写的程序在QEMU虚拟机上能运行,而在实际的硬件开发板上无法运行,那就只能借助硬件仿真器来调试和定位问题。
硬件仿真器指的是使用仿真头完全取代目标板(例如树莓派4B开发板)上的CPU,通过完全仿真目标开发板上的芯片行为,提供更加深入的调试功能。目前流行的硬件仿真器是JTAG仿真器。JTAG(Joint Test Action Group)是一种国际标准测试协议,主要用于芯片内部测试。JTAG仿真器通过现有的JTAG边界扫描口与CPU进行通信,实现对CPU和外设的调试功能。
▲图2.13 J-Link仿真器的JTAG接口
目前市面上支持ARM芯片调试的仿真器主要有ARM公司的DSTREAM仿真器、德国Lauterbach公司的Trace32仿真器以及SEGGER公司的J-Link仿真器。J-Link EDU是SEGGER公司推出的面向高校和教育的版本,本章提到的J-Link仿真器指的是J-Link EDU版本。本节介绍如何使用J-Link仿真器[4]调试树莓派4B。
为了在树莓派4B上使用J-Link仿真器,首先需要把J-Link仿真器的JTAG接口连接到树莓派4B的扩展板。树莓派4B的扩展接口已经内置了JTAG接口。我们可以使用杜邦线来连接。
J-Link仿真器提供20引脚的JTAG接口,如图2.13所示。
JTAG接口引脚的说明如表2.2所示。
表2.2 JTAG接口引脚的说明
引 脚 号 |
名 称 |
类 型 |
说 明 |
---|---|---|---|
1 |
VTref |
输入 |
目标机的参考电压 |
2 |
NC |
悬空 |
悬空引脚 |
3 |
nTRST |
输出 |
复位信号 |
5 |
TDI |
输出 |
JTAG数据信号,从JTAG输出数据到目标CPU |
7 |
TMS |
输出 |
JTAG模式设置 |
9 |
TCK |
输出 |
JTAG时钟信号 |
11 |
RTCK |
输入 |
从目标CPU反馈回来的时钟信号 |
13 |
TDO |
输入 |
从目标CPU反馈回来的数据信号 |
15 |
RESET |
输入输出 |
目标CPU的复位信号 |
17 |
DBGRQ |
悬空 |
保留 |
19 |
5V-Supply |
输出 |
输出5V电压 |
树莓派与J-Link仿真器的连接需要8根线,如表2.3所示。读者可以参考图2.5和图2.13来仔细连接线路。
表2.3 树莓派与J-Link仿真器的连接
JTAG接口 |
树莓派引脚号 |
树莓派引脚名称 |
---|---|---|
TRST |
15 |
GPIO22 |
RTCK |
16 |
GPIO23 |
TDO |
18 |
GPIO24 |
TCK |
22 |
GPIO25 |
TDI |
37 |
GPIO26 |
TMS |
13 |
GPIO27 |
VTref |
01 |
3.3V |
GND |
39 |
GND |
在实验2-1的基础上,复制loop.bin程序到MicroSD卡。另外,还需要修改config.txt配置文件,打开树莓派对JTAG接口的支持。
完整的config.txt文件如下。
# BenOS for JLINK debug
[pi4]
kernel=loop.bin
[pi3]
kernel=loop.bin
[all]
arm_64bit=1
enable_uart=1
uart_2ndstage=1
enable_jtag_gpio=1
gpio=22-27=a4
init_uart_clock=48000000
init_uart_baud=115200
复制完之后,把MicroSD卡插入树莓派中,接上电源。
OpenOCD(Open On-Chip Debugger,开源片上调试器)是一款开源的调试软件。OpenOCD提供针对嵌入式设备的调试、系统编程和边界扫描功能。OpenOCD需要使用硬件仿真器来配合完成调试,例如J-Link仿真器等。OpenOCD内置了GDB server模块,可以通过GDB命令来调试硬件。
首先,通过git clone命令下载OpenOCD软件[5]。
然后,安装如下依赖包。
$ sudo apt install make libtool pkg-config autoconf automake texinfo
接下来,编译和安装。
$ cd openocd
$ ./ bootstrap
$ ./configure
$ make
$ sudo make install
另外,也可以从xPack OpenOCD项目中下载编译好的二进制文件。
为了使用openocd命令连接J-Link仿真器,需要指定配置文件。OpenOCD的安装包里内置了jlink.cfg文件,该文件保存在/usr/local/share/openocd/scripts/interface/目录下。jlink.cfg配置文件比较简单,可通过“adapter”命令连接J-Link仿真器。
<jlink.conf配置文件>
# SEGGER J-Link
adapter driver jlink
下面通过openocd命令来连接J-Link仿真器,可使用“-f”选项来指定配置文件。
$ openocd -f jlink.cfg
Open On-Chip Debugger 0.10.0+dev-01266-gd8ac0086-dirty (2020-05-30-17:23)
Licensed under GNU GPL v2
For bug reports, read
****://openocd.***/doc/doxygen/bugs.html
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : J-Link V11 compiled Jan 7 2020 16:52:13
Info : Hardware version: 11.00
Info : VTarget = 3.341 V
从上述日志可以看到,OpenOCD已经检测到J-Link仿真器,版本为11。
接下来,使用J-Link仿真器连接树莓派,这里需要描述树莓派的配置文件raspi4.cfg。树莓派的这个配置文件的主要内容如下。
<raspi4.cfg配置文件>
set _CHIPNAME bcm2711
set _DAP_TAPID 0x4ba00477
adapter speed 1000
transport select jtag
reset_config trst_and_srst
telnet_port 4444
# 创建 tap
jtag newtap auto0 tap -irlen 4 -expected-id $_DAP_TAPID
# 创建 dap
dap create auto0.dap -chain-position auto0.tap
set CTIBASE {0x80420000 0x80520000 0x80620000 0x80720000}
set DBGBASE {0x80410000 0x80510000 0x80610000 0x80710000}
set _cores 4
set _TARGETNAME $_CHIPNAME.a72
set _CTINAME $_CHIPNAME.cti
set _smp_command ""
for {set _core 0} {$_core < $_cores} { incr _core} {
cti create $_CTINAME.$_core -dap auto0.dap -ap-num 0 -ctibase [lindex $CTIBASE $_core]
set _command "target create ${_TARGETNAME}.$_core aarch64 \
-dap auto0.dap -dbgbase [lindex $DBGBASE $_core] \
-coreid $_core -cti $_CTINAME.$_core"
if {$_core != 0} {
set _smp_command "$_smp_command $_TARGETNAME.$_core"
} else {
set _smp_command "target smp $_TARGETNAME.$_core"
}
eval $_command
}
eval $_smp_command
targets $_TARGETNAME.0
使用如下命令连接树莓派,结果如图2.14所示。
$ openocd -f jlink.cfg -f raspi4.cfg
如图2.14所示,OpenOCD已经成功连接J-Link仿真器,并且找到了树莓派的主芯片BCM2711。OpenOCD开启了几个服务,其中Telnet服务的端口号为4444,GDB服务的端口号为3333。
▲图2.14 使用J-Link仿真器连接树莓派
在Linux主机中新建终端,输入如下命令以登录OpenOCD的Telnet服务。
$ telnet localhost 4444
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
>
在Telnet服务的提示符下输入“halt”命令以暂停树莓派的CPU,等待调试请求。
> halt
bcm2711.a72.0 cluster 0 core 0 multi core
bcm2711.a72.1 cluster 0 core 1 multi core
target halted in AArch64 state due to debug-request, current mode: EL2H
cpsr: 0x000003c9 pc: 0x78
MMU: disabled, D-Cache: disabled, I-Cache: disabled
bcm2711.a72.2 cluster 0 core 2 multi core
target halted in AArch64 state due to debug-request, current mode: EL2H
cpsr: 0x000003c9 pc: 0x78
MMU: disabled, D-Cache: disabled, I-Cache: disabled
bcm2711.a72.3 cluster 0 core 3 multi core
target halted in AArch64 state due to debug-request, current mode: EL2H
cpsr: 0x000003c9 pc: 0x78
MMU: disabled, D-Cache: disabled, I-Cache: disabled
target halted in AArch64 state due to debug-request, current mode: EL2H
cpsr: 0x000003c9 pc: 0x80000
MMU: disabled, D-Cache: disabled, I-Cache: disabled
>
接下来,使用load_image命令加载BenOS可执行程序,这里把benos.bin加载到内存的0x80000地址处,因为在链接脚本中设置的链接地址为0x80000。
> load_image /home/rlk/rlk/lab01/benos.bin 0x80000
936 bytes written at address 0x80000
downloaded 936 bytes in 0.101610s (8.996 KiB/s)
下面使用step命令让树莓派的CPU停在链接地址(此时的链接地址为0x80000)处,等待用户输入命令。
> step 0x80000
target halted in AArch64 state due to single-step, current mode: EL2H
cpsr: 0x000003c9 pc: 0x4
MMU: disabled, D-Cache: disabled, I-Cache: disabled
现在可以使用GDB调试代码了。首先使用aarch64-linux-gnu-gdb命令(或者gdb-multiarch命令)启动GDB,并且使用端口号3333连接OpenOCD的GDB服务。
$ aarch64-linux-gnu-gdb --tui build/benos.elf
(gdb) target remote localhost:3333 <=连接OpenOCD的GDB服务
当连接成功之后,我们可以看到GDB停在BenOS程序的入口点(_start),如图2.15所示。
▲图2.15 连接OpenOCD的GDB服务
此时,我们可以使用GDB的“step”命令单步调试程序,也可以使用“info reg”命令查看树莓派上的CPU寄存器的值。
使用“layout reg”命令打开GDB的寄存器窗口,这样就可以很方便地查看寄存器的值。如图2.16所示,当单步执行完第16行的“adr x0, bss_begin”汇编语句后,寄存器窗口中马上显示了X0寄存器的值。
▲图2.16 单步调试和查看寄存器的值
本书中大部分的实验代码是基于BenOS来实现的。BenOS是一个基于ARM64体系结构的小型操作系统。本书的实验会从最简单的裸机程序开始,逐步扩展和丰富,让其具有进程调度、系统调用等现代操作系统的基本功能。
本节介绍最简单的BenOS的代码体系结构,目前它仅仅只有串口显示功能,类似于逻辑程序。
由于我们写的是裸机程序,因此需要手动编写Makefile和链接脚本。对于任何一种可执行程序,不论是.elf还是.exe文件,都是由代码(.text)段、数据(.data)段、未初始化数据(.bss)段等段(section)组成的。链接脚本最终会把一大堆编译好的二进制文件(.o文件)整合为二进制可执行文件,也就是把所有二进制文件整合到一个大文件中。这个大文件由总体的.text/.data/.bss段描述。下面是本实验中的一个链接文件,名为link.ld。
1 SECTIONS
2 {
3 . = 0x80000;
4 .text.boot : { *(.text.boot) }
5 .text : { *(.text) }
6 .rodata : { *(.rodata) }
7 .data : { *(.data) }
8 . = ALIGN(0x8);
9 bss_begin = .;
10 .bss : { *(.bss*) }
11 bss_end = .;
12 }
在第1行中,SECTIONS是LS(Linker Script)语法中的关键命令,用来描述输出文件的内存布局。SECTIONS命令告诉链接文件如何把输入文件的段映射到输出文件的各个段,如何将输入段整合为输出段,以及如何把输出段放入程序地址空间和进程地址空间。
在第3行中,“.”非常关键,它代表位置计数(Location Counter,LC),这里把.text段的链接地址设置为0x80000,这里的链接地址指的是加载地址(load address)。
在第4行中,输出文件的.text.boot段内容由所有输入文件(其中的“*”可理解为所有的.o文件,也就是二进制文件)的.text.boot段组成。
在第5行中,输出文件的.text段内容由所有输入文件(其中的“*”可理解为所有的.o文件,也就是二进制文件)的.text段组成。
在第6行中,输出文件的.rodata段由所有输入文件的.rodata段组成。
在第7行中,输出文件的.data段由所有输入文件的.data段组成。
在第8行中,设置为按8字节对齐。
在第9~11行中,定义了一个.bss段。
因此,上述链接文件定义了如下几个段。
下面开始编写启动用的汇编代码,将代码保存为boot.S文件。
1 #include "mm.h"
2
3 .section ".text.boot"
4
5 .globl _start
6 _start:
7 mrs x0, mpidr_el1
8 and x0, x0,#0xFF
9 cbz x0, master
10 b proc_hang
11
12 proc_hang:
13 b proc_hang
14
15 master:
16 adr x0, bss_begin
17 adr x1, bss_end
18 sub x1, x1, x0
19 bl memzero
20
21 mov sp, #LOW_MEMORY
22 bl start_kernel
23 b proc_hang
启动用的汇编代码不长,下面做简要分析。
在第3行中,把boot.S文件编译链接到.text.boot段中。我们可以在链接文件link.ld中把.text.boot段链接到这个可执行文件的开头,这样当程序执行时将从这个段开始执行。
在第6行中,_start为程序的入口点。
在第7行中,由于树莓派4B有4个CPU内核,但是本实验的裸机程序不希望4个CPU内核都运行,我们只想让第一个CPU内核运行起来。mpidr_el1寄存器是表示处理器内核的编号。
在第8行中,and指令用于完成与操作。
第9行,cbz为比较并跳转指令。如果X0寄存器的值为0,则跳转到master标签处。若X0寄存器的值为0,则表示第1个CPU内核。其他CPU内核则跳转到proc_hang标签处。
在第12和13行,proc_hang标签这里是死循环。
在第15行,对于master标签,只有第一个CPU内核才能运行到这里。
在第16~19行,初始化.bss段。
在第21行中,使SP指向内存的4 MB地址处。树莓派至少有1 GB内存,我们这个裸机程序用不到那么大的内存。
在第22行中,跳转到C语言的start_kernel函数,这里最重要的一步是设置C语言运行环境,即栈。
总之,上述汇编代码还是比较简单的,我们只做了3件事情。
接下来,编写C语言的start_kernel函数。本实验的目的是输出一条欢迎语句,因而这个函数的实现比较简单。将代码保存为kernel.c文件。
#include "mini_uart.h"
void start_kernel(void)
{
uart_init();
uart_send_string("Welcome BenOS!\r\n");
while (1) {
uart_send(uart_recv());
}
}
上述代码很简单,主要操作是初始化串口和向串口中输出欢迎语句。
接下来,实现一些简单的串口驱动代码。树莓派有两个串口设备。
本实验使用PL011串口设备。Mini串口设备比较简单,不支持流量控制(flow control),在高速传输过程中还有可能丢包。
BCM2711芯片里有不少片内外设复用相同的GPIO接口,这称为GPIO可选功能配置(GPIO Alternative Function)。GPIO14和GPIO15可以复用UART0与UART1串口的TXD引脚和RXD引脚,如表2.4所示。关于GPIO可选功能配置的详细介绍,读者可以查阅BCM2711芯片手册。在使用PL011串口之前,我们需要通过编程来使能TXD0和RXD0引脚。
表2.4 GPIO可选功能配置
GPIO |
电平 |
可选项0 |
可选项1 |
可选项2 |
可选项3 |
可选项4 |
可选项5 |
---|---|---|---|---|---|---|---|
GPIO0 |
高 |
SDA0 |
SA5 |
|
|
|
|
GPIO1 |
高 |
SCL0 |
SA4 |
|
|
|
|
GPIO14 |
低 |
TXD0 |
SD6 |
|
|
|
TXD1 |
GPIO15 |
低 |
RXD0 |
|
|
|
|
RXD1 |
BCM2711芯片提供了 GFPSELn寄存器来设置GPIO可选功能配置,其中 GPFSEL0用来配置GPIO0~GPIO9,而GPFSEL1用来配置GPIO10~GPIO19,以此类推。其中,每个GPIO使用3位来表示不同的含义。
首先,在include/asm/base.h头文件中加入树莓派寄存器的基地址。
#ifndef _P_BASE_H
#define _P_BASE_H
#ifdef CONFIG_BOARD_PI3B
#define PBASE 0x3F000000
#else
#define PBASE 0xFE000000
#endif
#endif /*_P_BASE_H */
下面是PL011串口的初始化代码。
void uart_init ( void )
{
unsigned int selector;
selector = readl(GPFSEL1); selector &= ~(7<<12);
/* 为GPIO14设置可选项0*/
selector |= 4<<12;
selector &= ~(7<<15);
/* 为GPIO15设置可选项0 */
selector |= 4<<15;
writel(selector, GPFSEL1);
上述代码把 GPIO14 和 GPIO15 设置为可选项0,也就是用作PL011 串口的RXD0 和TXD0引脚。
/*设置gpio14/15为下拉状态*/
selector = readl(GPIO_PUP_PDN_CNTRL_REG0);
selector |= (0x2 << 30) | (0x2 << 28);
writel(selector, GPIO_PUP_PDN_CNTRL_REG0);
通常GPIO引脚有3个状态——上拉(pull-up)、下拉(pull-down)以及连接(connect)。连接状态指的是既不上拉也不下拉,仅仅连接。上述代码已把GPIO14和GPIO15设置为连接状态。
下列代码用来初始化PL011串口。
/* 暂时关闭串口 */
writel(0, U_CR_REG);
/* 设置波特率 */
writel(26, U_IBRD_REG);
writel(3, U_FBRD_REG);
/* 使能FIFO设备 */
writel((1<<4) | (3<<5), U_LCRH_REG);
/* 屏蔽中断 */
writel(0, U_IMSC_REG);
/* 使能串口,打开收发功能 */
writel(1 | (1<<8) | (1<<9), U_CR_REG);
接下来,实现如下几个函数以收发字符串。
void uart_send(char c)
{
while (readl(U_FR_REG) & (1<<5))
;
writel(c, U_DATA_REG);
}
char uart_recv(void)
{
while (readl(U_FR_REG) & (1<<4))
;
return(readl(U_DATA_REG) & 0xFF);
}
uart_send()和uart_recv()函数分别用于在while循环中判断是否有数据需要发送和接收,这里只需要判断U_FR_REG寄存器的相应位即可。
接下来,编写Makefile文件。
board ?= rpi3
ARMGNU ?= aarch64-linux-gnu
COPS += -DCONFIG_BOARD_PI4B
QEMU_FLAGS += -machine raspi4
COPS += -g -Wall -nostdlib -nostdinc -Iinclude
ASMOPS = -g -Iinclude
BUILD_DIR = build
SRC_DIR = src
all : benos.bin
clean :
rm -rf $(BUILD_DIR) *.bin
$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
mkdir -p $(@D)
$(ARMGNU)-gcc $(COPS) -MMD -c $< -o $@
$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
$(ARMGNU)-gcc $(ASMOPS) -MMD -c $< -o $@
C_FILES = $(wildcard $(SRC_DIR)/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)
DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)
benos.bin: $(SRC_DIR)/linker.ld $(OBJ_FILES)
$(ARMGNU)-ld -T $(SRC_DIR)/linker.ld -o $(BUILD_DIR)/benos.elf $(OBJ_FILES)
$(ARMGNU)-objcopy $(BUILD_DIR)/benos.elf -O binary benos.bin
QEMU_FLAGS += -nographic
run:
qemu-system-aarch64 $(QEMU_FLAGS) -kernel benos.bin
debug:
qemu-system-aarch64 $(QEMU_FLAGS) -kernel benos.bin -S -s
ARMGNU用来指定编译器,这里使用aarch64-linux-gnu-gcc。
COPS和ASMOPS用来在编译C语言与汇编语言时指定编译选项。
上述文件最终会被编译、链接成名为benos.elf的.elf文件,这个.elf文件包含了调试信息,最后使用objcopy命令把elf文件转换为可执行的二进制文件。
本书中少部分实验可以在ARM64的Linux主机上完成,例如指令集实验和部分高速缓存伪共享实验。ARM64的Linux主机可以通过如下两个方式获取:一种是在树莓派4B上安装树莓派OS,另一种是使用QEMU虚拟机与ARM64实验平台。
第一种方式请参考2.2.2节的介绍,下面介绍QEMU虚拟机与ARM64实验平台。Linux主机使用Ubuntu 20.04系统。
1)安装工具
首先,在Linux主机中安装相关工具。
$ sudo apt-get install apt-get install qemu-system-arm libncurses5-dev gcc-aarch64-linux-gnu build-essential git bison flex libssl-dev
然后,在Linux主机系统中默认安装ARM64 GCC的9.3版本。
$ aarch64-linux-gnu-gcc -v
gcc version 9.3.0 (Ubuntu 9.3.0-8ubuntu1)
2)下载仓库
下载runninglinuxkernel_5.0的git仓库并切换到runninglinuxkernel_5.0分支。
$
$ git clone *****://github.***/runninglinuxkernel/runninglinuxkernel_5.0.git
3)编译内核以及创建文件系统
runninglinuxkernel_5.0目录中有一个rootfs_arm64.tar.xz文件,这个文件基于Ubuntu Linux 20.04系统的根文件系统创建。
注意,该脚本会使用dd命令生成一个4 GB大小的映像文件,因此主机系统需要保证至少10 GB的空余磁盘空间。如果读者需要生成更大的根文件系统映像,那么可以修改run_rlk_arm64.sh脚本文件。
首先,编译内核。
$ cd runninglinuxkernel_5.0
$ ./run_rlk_arm64.sh build_kernel
执行上述脚本需要几十分钟时间,具体依赖于主机的计算能力。
然后,编译根文件系统。
$ cd runninglinuxkernel_5.0
$ sudo ./run_rlk_arm64.sh build_rootfs
注意,编译根文件系统需要管理员权限,而编译内核则不需要。执行完上述命令后,将会生成名为rootfs_arm64.ext4的根文件系统。
4)运行刚才编译好的ARM64版本的Linux系统
要运行run_rlk_arm64.sh脚本,输入run参数即可。
$./run_rlk_arm64.sh run
或者,输入以下代码。
$ qemu-system-aarch64 -m 1024 -cpu max,sve=on,sve256=on -M virt -nographic -smp 4 -kernel arch/arm64/boot/Image -append "noinintrd sched_debug root=/dev/vda rootfstype=ext4 rw crashkernel=256M loglevel=8" -drive if=none,file=rootfs_debian_arm64.ext4,id=hd0 -device virtio-blk-device,drive=hd0 --fsdev local,id=kmod_dev,path=./kmodules,security_model=none -device virtio-9p-pci,fsdev=kmod_dev,mount_tag=kmod_mount
运行结果如下。
rlk@ runninglinuxkernel_5.0 $ ./run_rlk_arm64.sh run
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
[ 0.000000] Linux version 5.0.0+ (rlk@ubuntu) (gcc version 9.3.0 (Ubuntu 9.3.0-8ubuntu1)) #5 SMP Sat Mar 28 22:05:46 PDT 2020
[ 0.000000] Machine model: linux,dummy-virt
[ 0.000000] efi: Getting EFI parameters from FDT:
[ 0.000000] efi: UEFI not found.
[ 0.000000] crashkernel reserved: 0x0000000070000000 - 0x0000000080000000 (256 MB)
[ 0.000000] cma: Reserved 64 MiB at 0x000000006c000000
[ 0.000000] NUMA: No NUMA configuration found
[ 0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x000000007fffffff]
[ 0.000000] NUMA: NODE_DATA [mem 0x6bdf0f00-0x6bdf1fff]
[ 0.000000] Zone ranges:
[ 0.000000] Normal [mem 0x0000000040000000-0x000000007fffffff]
[ 0.000000] Movable zone start for each node
[ 0.000000] Early memory node ranges
…
[ 2.269567] systemd[1]: systemd 245.2-1ubuntu2 running in system mode.
Ubuntu Focal Fossa (development branch) ubuntu ttyAMA0
rlk login:
登录系统时使用的用户名和密码如下。
5)在线安装软件包
QEMU虚拟机可以通过VirtIO-Net技术来生成虚拟的网卡,并通过NAT(Network Address Translation,网络地址转换)技术和主机进行网络共享。下面使用ifconfig命令检查网络配置。
root@ubuntu:~# ifconfig
enp0s1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.0.2.15 netmask 255.255.255.0 broadcast 10.0.2.255
inet6 fec0::ce16:adb:3e70:3e71 prefixlen 64 scopeid 0x40<site>
inet6 fe80::c86e:28c4:625b:2767 prefixlen 64 scopeid 0x20<link>
ether 52:54:00:12:34:56 txqueuelen 1000 (Ethernet)
RX packets 23217 bytes 33246898 (31.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4740 bytes 267860 (261.5 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
可以看到,这里生成了名为enp0s1的网卡设备,分配的IP地址为10.0.2.15。
可通过apt update命令更新Debian系统的软件仓库。
root@ubuntu:~# apt update
如果更新失败,有可能因为系统时间比较旧了,使用date命令来设置日期。
root@ubuntu:~# date -s 2020-03-29 #假设最新日期是2020年3月29日
Sun Mar 29 00:00:00 UTC 2020
使用apt install命令安装软件包,比如,在线安装gcc等软件包。
root@ubuntu:~# apt install gcc build-essential
6)在主机和QEMU虚拟机之间共享文件
主机和QEMU虚拟机可以通过NET_9P技术进行文件共享,这需要QEMU虚拟机和主机的Linux内核都使能NET_9P的内核模块。本实验平台已经支持主机和QEMU虚拟机的共享文件,可以通过如下简单方法来测试。
首先,复制一个文件到runninglinuxkernel_5.0/kmodules目录中。
$ cp test.c runninglinuxkernel_5.0/kmodules
启动QEMU虚拟机之后,检查一下/mnt目录中是否有test.c文件。
root@ubuntu:/# cd /mnt
root@ubuntu:/mnt # ls
README test.c
后续的实验(例如第22章和第23章的实验)会经常利用这个特性,比如把编写好的代码文件放入QEMU虚拟机。
[1] J-Link EDU仿真器需要额外购买,请登录SEGGER公司官网以了解详情。
[2] 对于上述颜色,可能每个厂商不太一样,读者需要认真阅读厂商的说明文档。
[3] Ubuntu Linux 20.04内置的QEMU 4.2还不支持树莓派4B。若要在QEMU中模拟树莓派4B,那么还需要打上一系列补丁,然后重新编译QEMU。本书配套的实验平台VMware/VirtualBox映像会提供支持树莓派4B的QEMU程序,请读者使用本书配套的VMware/Virtualbox映像。
[4]J- Link仿真器需要额外购买,读者可以登录SEGGER公司官网以了解详情。
[5] 读者可以到OpenOCD官网上下载源代码。另外,本书配套的实验平台VMware映像安装了OpenOCD软件。