书名:计算机系统开发与优化实战
ISBN:978-7-115-59288-0
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 周文嘉 刘 盼 王钰达
责任编辑 谢晓芳
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e59288”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。
本书首先介绍通用处理器的架构,以及汇编和编译的技术;然后讲解Linux内存管理、Linux进程管理,以及GDB、trace、eBPF、SystemTap等Linux系统开发工具;接着通过视频编解码主流技术和NVIDIA计算平台CUDA等讨论人工智能技术在音视频领域与自然语言处理领域的应用;最后讲解标准计算平台OpenCL的原理、开源硬件soDLA、Intel神经网络异构加速芯片、SystemC框架。
本书适合从事企业系统开发及优化的技术人员阅读,也可供计算机相关专业的师生参考。
计算机系统是一个耳熟能详的词。但这个词的诞生过程并不简单,它标志着计算机技术发展史上一个重要的里程碑。很长时间,计算机在人们心中就是一台一台机器。从19世纪初巴贝奇设计的差分机,到20世纪40年代的第一台电子计算机ENIAC,在人们的脑海中它们充满了金属、机械、电力这些时代元素。按维基百科的词条定义,计算机就是数字电子机器(digital electronic machine)。
从何时起,“计算机”这3个字后面加上“系统”两个字,出现了“计算机系统”这个词?这个问题其实并没有确切的答案,我便自己尝试着做了一个不严谨的考据。仍从维基百科的词条出发,按其定义,计算机系统是包含硬件、软件和外设的完整计算机(complete computer)。我们知道,早期计算机很珍贵,软件只是硬件的附属品。因此,什么时候开始软件能与硬件平起平坐,不再附属于硬件,便是一个重要的线索。
我们可以把这个时间点指向IBM System/360诞生的20世纪60年代。1964年,IBM推出了System/360系列大型机,彻底改变了计算机的发展趋势。在此之前,软件是与硬件绑定的,只能在一种计算机上运行。但System/360使用了兼容的概念,使得一个软件在不同型号的计算机上都能运行。从此,软件不再附属于计算机硬件,软件得到了解放,成为一种产品,最终形成一个新的产业。
20世纪60年代是人们真正认识到计算机系统重要性的年代。1960年,IBM成立了系统研究所(System Research Institute),并专门设置了“系统工程师”(System Engineer)岗位;1962年,IBM System Journal创刊,并在1964年发表了一期System/360专题文章……计算机系统这个词,从此开始深入人心。计算机系统的核心在于硬件与软件的协同。
计算机硬件核心部件在过去半个多世纪持续高速发展,尤其是处理器芯片,得益于摩尔定律,一直处于指数增长模式。20世纪60年代还在为每秒完成3万余次运算的System/360喝彩的人们也许根本无法想象今天一颗邮票大小的硅片上能集成近千亿个晶体管,能达到每秒几十万亿次双精度浮点运算的性能。
然而,这些晶体管的性能得到充分利用了吗?2020年麻省理工学院的一个科研团队在Science上发表了一篇名为“There’s Plenty of Room at the Top”的文章,给出了他们的答案:显然没有。他们开展了一个小实验:假设用Python实现一个矩阵乘法的性能是1,那么用C语言重写后性能可以提高50倍,如果能再充分挖掘硬件体系结构特性(如循环并行化、访存优化、单指令多数据流等),那么性能可以提高63000倍!这种跨层的软硬件协同优化存在巨大的潜力可被挖掘。
遗憾的是,真正能如此深入理解硬件体系结构、发挥硬件优势的软件开发人员依旧凤毛麟角。同样,真正能把握应用软件需求与特征并掌握操作系统运行机制的芯片研发人员也极其稀缺。但是,当前国内高校的教学体系在软硬件协同方面仍然存在一条鸿沟。虽然国内几乎所有高校都有计算机系,但大多数教学侧重软件与应用层面,即使开设与硬件体系结构相关的课程,也缺少系统的实践训练。虽然近年来国内许多高校兴办集成电路学院,但大多数课程侧重微电子,很少开设体系结构课程,更不用说操作系统这样的系统软件课程。很难想象这些学生毕业后能直接参与到处理器芯片架构、核心系统软件的设计与开发,但这种既懂硬件又懂软件的人才正是国内业界非常紧缺的。
过去几年,我们在中国科学院大学启动“一生一芯”计划,希望能为解决这种人才困境做一些贡献。“一生一芯”计划是一个实践课程,目标是让学生设计实现一款可运行操作系统的处理器芯片并完成流片,旨在让学生通过实践打通“程序→库→操作系统→指令→微结构→电路→晶体管”的知识与技能链条。目前“一生一芯”计划已经拓展到面向全国高校的学生,在第四期中,已有来自200多所高校的1100多位学生报名。
很惊喜地发现本书与“一生一芯”计划秉持相同的理念,并覆盖更宽广的领域,从底层的通用处理器架构、Linux内核与开发工具、OpenCL编程一直到上层的人工智能软件框架与应用。在我的印象中,国内关注基础概念、基本原理的图书已经不少,但这样侧重软硬件贯通的实战型技术类图书仍然很少。在我看来,这是一本难得的计算机系统领域的“实战手册”,可以帮助广大从业人员提升计算机系统实战技能,而这是在产业界所迫切需要的。
包云岗
中科院计算所研究员、副所长
人工智能、物联网、芯片自主、智能驾驶等新一代信息技术是当代智能科技的主要体现。目前,计算机基础教育的作用不言而喻,它是现代智能科技发展的核心支柱。计算机技术的底层包含芯片设计,中间层涉及操作系统,上层运行软件应用程序。在科技竞争的大环境下,我们迫切需要芯片的自主研发,需要操作系统的自主研发,需要系统软件的自主研发。唯有如此,我们才可以从根本上解决“卡脖子”的问题,所以学习计算机系统知识十分重要。
回顾历史,每一次智能终端的发展都会带来翻天覆地的变化。5G技术带来了低延时、高吞吐、广连接,促进了异构设备的蓬勃发展,我们正在进入万物互联的新世界。万物互联的世界对传统的芯片和操作系统提出了新的要求。顺应时代发展,芯片和操作系统都出现了相应的“革命”。例如,恩智浦的跨界处理器、壁仞的高端AI GPU、谷歌的TPU和Fuchsia以及华为的鸿蒙等都是新架构。
计算机系统涉及的内容很多,包含底层处理器的架构设计、汇编和编译技术,甚至还包含操作系统的运行等。没有进行系统化的学习,我们很难从根本上理解现代计算机系统的来龙去脉,创新也就无从谈起。学习计算机系统的门槛很高,不同层级之间又是相互关联的,想要精通这些内容,没有好的学习方法是不行的。
根据“战略上藐视技术,战术上重视技术”的原则,本书首先从处理器架构的原理出发,结合汇编和编译技术,揭开硬件执行的神秘面纱;然后以Linux操作系统为例,讲述内核中重要的模块——内存管理和进程管理;接着讲解人工智能技术的基础技术和相关框架,以及实现人工智能加速的常见方式——使用OpenCL;最后通过一些开源项目介绍硬件设计的常用工具和方法。希望本书能帮助读者对底层硬件设计、中间操作系统,以及目前火热的人工智能有所了解,能为国内的基础研究者提供一些帮助。
建议在阅读代码时注意逻辑性,不要过于关注细节,遇到难点可以选择性地跳过,结合整段代码要实现的功能去理解,在对整体框架有了一定的了解后再根据工作中的需要深挖细节。吾生也有涯,而知也无涯,要时刻记住自己想要解决的问题是什么,无关的内容可以先绕开。
本书重点讲解计算机系统的开发与实战。全书共有9章,由周文嘉、刘盼、王钰达等人编写。
第1章以ARM处理器为主,介绍通用处理器的架构。该章由周文嘉主编,参与编写的有张健、邵靖杰、彭杨益、朱志方等。
第2章介绍汇编和编译技术。该章由周文嘉主编,参与编写的有彭东林、李雄辉、张帅、汪涛等。
第3章介绍Linux内核中对内存的管理,包括从CPU的角度看内存、分区页帧分配器、slab分配器及kmalloc的实现、缺页异常处理等内容。该章由刘盼编写。
第4章介绍Linux内核中的进程管理,包括进程的创建、终止、调度和多核系统的负载均衡等内容。该章由刘盼编写。
第5章主要介绍Linux系统上的一些开发和调试工具。该章由周文嘉主编,参与编写的有雷波、刘雨、林舒萌、韩金科等。
第6章介绍人工智能技术。该章由赵刚主编,参与编写的有蒋仲明、魏凯、杨鹏、梁庆伟等。
第7章介绍OpenCL的编程技术。该章由谷镇佑编写。
第8章是一些基础软件开源项目的介绍。该章由张仁泽主编,参与编写的有李磊、马定桦、任泽龙等。
第9章介绍硬件架构。该章由余明辉主编,参与编写的有王钰达、郭论平等。
感谢Free time team,在这个平台上大家不论学历高低,都可以一起学习,学好了还可以参与社区贡献。本书的主要开发环境是Linux的Ubuntu发行版和x86架构。全书包含了大量实际案例,对应源代码参见GitHub网站。
感谢谢晓芳在书稿撰写期间对我们的大力支持,有了她的耐心指导,本书才能顺利出版。
感谢参与本书策划与封面设计的余扬(从事网站编辑工作,爱好画画、封面设计、策划),以及童昀(在读硕士研究生,爱好策划、设计)等。
周文嘉,目前就职于某国产AI GPU芯片公司,曾服务于ARM、阿里巴巴、HTC等公司,拥有10年以上工作经验,主要从事系统软件开发,涵盖系统库开发、指令集优化、Linux内核开发等,为某些开源社贡献过一定数量的补丁,担任Free time team创始人,致力于免费教育事业。
刘盼,目前就职于某国际芯片公司,曾服务于三星电子研究所、某自动驾驶科技公司,具有手机、汽车和芯片行业的工作经验,创办4万多人的极客社区——“人人都是极客”,担任某科技公司合伙人,是谷歌开发者社区优秀讲师。
王钰达,加州大学伯克利分校和伊利诺伊理工学院双硕士,目前专注于RISC-V工具链、NVDLA工具链、自定义自动驾驶相关加速器芯片前端和后端设计的敏捷开发。
张帅,曾就职于360、奇安信安全等公司,资深Linux高级安全专家,拥有10年云计算与网络安全研发工作经验,主导设计与研发国内首款Linux信创终端安全防御系统,拥有核心专利10余项。
邵靖杰,目前就职于某国产大型机ARM CPU研究所,主要从事众核处理器的系统级缓存研发工作。
张健,先后在SUSE、华为、区块链创业公司、寒武纪等公司工作,担任工程师、架构师、技术合伙人等,研究方向包括ARM、Linux发行版、Linux内核、RISC-V和虚拟化。
张仁泽,BiscuitOS创始人,目前就职于某一线互联网云厂商,主攻Linux内存管理,多年致力于为中国Linux社区提供一款用于内核开发的Linux发行版BiscuitOS,并坚持基于BiscuitOS不断向社区提供高质量开源Linux技术文档。
李雄辉,目前就职于某国产MCU芯片公司,曾开发JUICE VM RISC-V虚拟机,拥有6年以上开发经验,主要从事物联网开发、嵌入式软件开发、Linux内核开发等,是JUICE VM的作者,致力于免费教育事业。
任泽龙,目前就职于高通公司,主要从事平台软件开发方面的工作,精通Linux内核开发、虚拟化及QNX设备驱动开发等。
李磊,现就职于国内某大型存储公司,拥有10年以上工作经验,主要从事系统软件开发,精通内存管理及存储I/O栈。
彭东林,目前就职于国内某云计算公司,曾服务于某国产手机和芯片公司,拥有8年以上工作经验,主要从事系统软件开发,熟悉SoC、BootLoader、Linux内核等。
刘雨,科大讯飞嵌入式开发工程师,拥有6年以上开发经验,主要负责Linux系统集成、设备驱动开发、嵌入式平台GUI应用框架搭建,对智能语音产品和智能视觉产品开发有丰富的经验。
林舒萌,目前就职于某国产AI GPU芯片公司,曾服务于360企业安全公司,主要从事测试开发、DevOps方面的工作。
余明辉,目前就职于字节跳动公司,曾服务于Intel、阿里巴巴集团,主要从事机器学习系统及神经网络加速硬件方面的研发工作。
郭论平,目前就职于ARM中国,曾供职于AMD、展讯等芯片公司,曾从事Linux内核及驱动开发,现为一名芯片验证工程师,热衷于研究芯片设计/验证技术及底层软件开发。
谷镇佑,在音视频编解码、推流服务、视频渲染、算法实现、并行计算、性能优化等方面有5年以上的工作经验,目前从事某国产AI GPU芯片的软件生态研发工作。
蒋仲明,目前就职于某国产AI芯片公司,曾就职于多家多媒体芯片公司,主要从事嵌入式系统、多媒体驱动、多媒体框架、人工智能框架及人工智能算法的开发工作,致力于多媒体与人工智能芯片行业的前沿研究与产业化发展。
马定桦,一个开源软件的受益者,喜欢研究各种软硬件工具,主要从事系统软件开发,工作内容主要为U-Boot和Linux驱动开发、文件系统定制等,喜欢研究计算机体系结构、开源硬件定制等。
雷波,目前就职于国内某知名IC设计公司,曾任MTK蓝牙固件设计师、华为硬件系统设计师等,主要研究方向是嵌入式系统设计,具有丰富的嵌入式硬件和嵌入式软件设计开发经验,在通过自动化日志分析提升研发效率、通过自动测试保证高质量交付两个方面有建树。
韩金科,目前就职于滴滴出行科技有限公司,有十余年工作经验,前期主要做文件系统和内核稳定性方面的工作,之后致力于研究内核I/O和稳定性。
赵刚,目前就职于某国产AI GPU芯片公司,曾服务于龙芯、美国多核等公司,拥有14年以上工作经验,主要从事OpenGL、OpenCV、OpenCL相关软件开发,熟悉三维图形开发、图像算法优化等。
魏凯,就职于某国产AI GPU芯片公司,之前服务于国内视频监控产品公司,拥有5年以上工作经验,主要从事多媒体软件开发,包括音视频编解码驱动、多媒体框架、流媒体等。
梁庆伟,博士,目前就职于某系统优化的创业公司,曾在华为等公司做过AI算子优化等工作。
杨鹏,目前就职于某AI GPU芯片公司,担任软件工程师,从事并行化计算、CV算法研究和加速方面的工作,硕士毕业于北京邮电大学自动化学院,主要研究方向为机器人控制与导航。
彭杨益,目前就职于某国内操作系统公司,曾服务于万物云、小红书、HTC等公司;擅长全栈开发,热衷于软硬件结合,为谷歌、Mozilla贡献过补丁;Free time team重要成员,致力于免费教育事业。
汪涛,湖北省赤壁市教学研究室信息技术教研员,从教十余年,在关怀教育事业的闲暇,坚持不懈研究计算机基础技术。
朱志方,目前就职于某半导体设备公司,曾服务于自动化设备厂家,拥有10年左右工作经验,主要从事嵌入式软件的开发,熟悉自动化设备的设计开发、程序调试等。
本书由异步社区出品,社区(https://www.epubit.com/)为您提供后续服务。
您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e59288”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可,如下图所示。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区投稿(直接访问www.epubit.com/contribute即可)。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
异步社区
微信服务号
本书涉及的处理器架构内容中,绝大部分以ARMv8的64位架构为例。ARM架构从ARMv4指令集开始成熟,之后每个版本都会加入很多新的处理器特性(CPU feature)。ARMv5架构引入了VFP-V2、Jazelle。ARMv6架构引入了Thumb2、TrustZone、SIMD。ARMv7架构引入了VFP-V3/V4、NEON(SIMD扩展)。ARMv8架构引入了64位架构,可以通过一个开关切换到传统的32位架构,并且对之前的大部分处理器特性做了增强。64位平台上的虚拟地址空间也大大增加,避免了在32位架构上很多虚拟地址空间小导致的问题。ARMv8引入了新的异常模式,以应对复杂的运行环境,如日益严重的系统安全问题、虚拟化等。
和ARMv7一样,ARMv8分成A、R、M这3个系列,分别对应大型应用领域、嵌入式领域和微处理器领域,本书只介绍ARMv8-A。ARMv8-A支持AArch32和AArch64两种执行状态(execution state),分别对应32位和64位,本书只介绍AArch64。
AArch64提供了31个64位的通用寄存器,分别是X0~X30。寄存器长度都是64位,所有指令的长度都是32位,其中X30作为函数调用链接寄存器。64位的寄存器有程序计数器(Program Counter,PC)、栈指针(Stack Pointer,SP)寄存器,以及异常链接寄存器(Exception Link Register,ELR)。AArch64提供了32个128位的SIMD寄存器,用于为整数向量运算和浮点运算提供支持。AArch64还定义了4种异常级别EL0~EL3。
除以上基本寄存器之外,ARMv8架构还提供了功能丰富的系统控制寄存器,并且架构的每一次升级都会引入很多新的处理器特性,这些内容将会在其他章节探讨。
作为RISC架构,AArch64提供了大量的通用寄存器。除通用寄存器之外,本节还会介绍特殊寄存器、系统控制寄存器、处理器状态、函数调用标准。
通用寄存器分为两类。其中一类寄存器包括X0~X30,用于普通的指令集,每个寄存器都有64位(Xn)和32位(Wn)两种表示形式。其中32位的表示形式是64位表示形式的低32位。另一类寄存器包括V0~V31,用于浮点运算、SIMD、crypto等领域。每个寄存器长度都是128位(Qn),它们有64位(Dn)、32位(Sn)、16位(Hn)、8位(Bn)这4种表示形式。
以X0和V0为例,X0是64位寄存器,它的低32位是W0。V0也称为Q0,Q0是一个128位的寄存器,它的低64位称为D0,它的低32位称为S0,它的低16位称为H0,它的低8位称为B0,如图1.1所示。
图1.1 寄存器的表示形式
XZR和WZR分别对应64位与32位的零寄存器。对这些寄存器进行读操作,将会获取到0;对这些寄存器进行的写操作将会被处理器忽略。与ARM的32位架构不同,PC寄存器已经不再是一个通用寄存器,无法直接访问。ARM指令的长度是4字节,因此对于ARMv8上的纯ARM指令来说,PC寄存器是按字节对齐的。SP寄存器也不再是一个通用寄存器,SP寄存器强制按16字节对齐。
ARM的系统控制寄存器都以“_ELx”为后缀,其中“x”表示某异常级别(Exception Level,EL)的一个数字,如SCTLR_EL1。后缀的数字意味着能够访问该寄存器的最低异常级别,ARM的系统控制寄存器如表1.1所示。
表1.1 ARM的系统控制寄存器
| 寄存器 |
寄存器的访问权限 |
|---|---|
| TTBR0_EL1 |
能在EL1、EL2和EL3访问,不能在EL0访问 |
| TTBR0_EL2 |
能在EL2和EL3访问,不能在EL0和EL1访问 |
| TTBR0_EL3 |
只能在EL3访问 |
MRS和MSR指令用于读写系统控制寄存器,示例代码如下。
MRS X0, SCTLR_EL1 // X0 = SCTLR_EL1
MSR SCTLR_EL1, X0 // SCTLR_EL1 = X0常用的系统控制寄存器及其功能如表1.2所示。
表1.2 常用的系统控制寄存器及其功能
| 寄存器 |
名称 |
功能 |
|---|---|---|
| ACTLR_ELx |
辅助控制寄存器 |
用于控制特定处理器相关的特性 |
| CTR_EL0 |
缓存寄存器 |
与CPU缓存信息相关 |
| HCR_EL2 |
监督寄存器 |
用于控制虚拟化设定,只能在EL2中使用 |
| ID_AA64ISAR0_EL1 ID_AA64ISAR1_EL1 |
指令实现寄存器 |
用来描述处理器特性是否实现的相关信息,如当前架构是否支持CRC32硬件指令、是否支持AES加密指令等 |
| MIDR_EL1 |
主ID寄存器 |
用于描述处理器的身份信息 |
| MPIDR_EL1 |
多处理器亲和力寄存器 |
在多处理器环境中,调度相关的寄存器 |
| RNDR |
随机数寄存器 |
返回一个64位的随机数 |
| SCTLR_ELx |
系统控制寄存器 |
用于控制MMU、缓存、对齐检查等与ARM架构相关的特性 |
| SCR_EL3 |
安全配置寄存器 |
用来控制安全状态,只能在EL3使用 |
| VBAR_ELx |
异常向量表基址寄存器 |
用来保存异常向量表的基地址 |
ARM的系统控制寄存器数量庞大,详细的介绍可以参考文档DDI0487F_b_ARMv8_arm.pdf。
AArch64通过PSTATE(process state)的标志位来保存处理器的状态,处理器执行指令的时候,可以读取和设置这些标志位。这些标志位既可以通过mrs/msr指令进行访问,也可以通过DAIFSet、DAIFClr、SPSel、PAN、UAO等指令直接访问。PSTATE寄存器的标志位如表1.3所示。
表1.3 PSTATE寄存器的标志位
| 分类 |
域 |
描述 |
| 算术逻辑部件(Arithmetic and Logic Unit,ALU)状态标志位 |
N |
负数状态标志位 |
| Z |
零状态标志位 |
|
| C |
进位标志位 |
|
| V |
溢出标志位 |
|
| 异常掩码标志位 |
D |
调试异常掩码标志位 |
| A |
SError中断掩码标志位 |
|
| I |
IRQ掩码标志位 |
|
| F |
FIQ掩码标志位 |
|
| 异常态控制标志位 |
SS |
软件单步调试标志 |
| IL |
非法指令执行状态标志 |
|
| nRW |
当前执行状态 |
|
| EL |
当前异常态级别(current exception level) |
|
| SP |
SP寄存器选择位 |
|
| 访问控制标志位 |
PAN |
特权访问禁止位 |
| UAO |
用户访问重载 |
|
| TCO |
标记检查重载 |
|
| BTYPE |
分支目标识别 |
AArch64提供了31个64位的通用寄存器X0~X30,SP寄存器已经变成了一个专用寄存器。这些寄存器的描述如表1.4所示。
表1.4 AArch64寄存器的描述
| 寄存器 |
别名 |
描述 |
|---|---|---|
| SP |
— |
栈指针寄存器 |
| X30 |
LR |
链接寄存器,用来存放函数返回地址 |
| X29 |
FP |
栈帧寄存器,编译器可以通过开关关闭此功能,从而使其变成一个普通寄存器 |
| X19~X28 |
— |
被调函数负责保存的寄存器 |
| X18 |
— |
在特殊场合作为平台寄存器,其他情况下可以作为临时寄存器 |
| X17 |
IP1 |
可能会被链接器使用,其他情况下可以作为临时寄存器 |
| X16 |
IP0 |
可能会被链接器使用,其他情况下可以作为临时寄存器 |
| X9~X15 |
— |
临时寄存器 |
| X8 |
— |
间接结果位置寄存器 |
| X0~X7 |
— |
用来传递函数参数和函数返回值 |
值得注意的是,X16和X17寄存器在动态链接的时候,可能会被某些链接器用于实现特殊功能。X18寄存器在Darwin和Windows平台上会保留作为平台寄存器使用。在代码优化的时候,这3个寄存器要谨慎使用。
AArch64提供了32个128位的寄存器(V0~31),可以用来进行SIMD和浮点运算。
其中,V0~V7这8个寄存器用来传递参数和函数返回值,V8~V15这8个寄存器需要由被调函数保存(只需要保存这些寄存器的低64位即可)。
D8~D15是V8~V15寄存器的低64位,因此通过如下的代码片段保存D8~D15的内容,就可以在函数中使用V0~V31这32个寄存器了。
stp d8, d9, [sp, -192]!
stp d10, d11, [sp, 16]
stp d12, d13, [sp, 32]
stp d14, d15, [sp, 48]如果平台支持SVE扩展,那么AArch64会提供32个可变长的向量寄存器Z0~Z31。每个寄存器都可以用来进行SIMD和浮点运算,其中Z0~Z7用来传递参数和函数返回值。Z8~Z15这8个寄存器需要由被调函数保存(只需要保存这些寄存器的低64位即可)。
AArch64还为SVE提供了16个断言寄存器P0~P15,其中P0~P3这4个寄存器用来传递参数和函数返回值。
流水线是处理器微架构设计的一种技术,其主要目的在于提高处理器的性能。基本思想与工厂的流水线相似,将指令要完成的任务分为多个阶段,这样每个阶段都可以有一条正在执行的指令,每个阶段的指令执行完交给下一个阶段继续执行,如此可以大幅度提高处理器的指令吞吐量。
在流水线中加入超标量与乱序执行技术是现代主流高性能CPU的基本微架构。超标量技术是指使用多条特定执行功能的流水线,一次取多条指令同时放入不同流水线中执行。乱序执行技术是指在选择指令放入不同流水线执行时,不依赖指令顺序决定先后执行关系,只要指令所需资源已准备就绪即可执行。
由于连续的指令流之间存在很多相关性,因此处理器并不能使流水线中的指令直接地并行执行到结束。如对同一通用寄存器或同一存储器地址的先写后读(Read After Write,RAW)、先写后写(Write After Write,WAW)、先读后写(Write After Read,WAR)。在出现相关性的时候,使用很多额外的技术来处理这些情况,其中包括事务阻塞(stall)、旁路网络(bypassing network)、事务重发(replay)、寄存器重命名(register renaming)等技术。下面就基于AArch64中一款处理器——Cortex-A77来简单介绍这些技术和软件优化的关系。Cortex-A77是一款典型的超标量乱序流水线处理器,其微架构如图1.2所示。
简要介绍图中涉及的术语。
● 宏操作缓存:用于缓存指令译码后的宏操作。
● 返回地址栈:一种记录链接跳转类型指令将来返回时PC值的表,由于链接寄存器只有一个,因此一般链接跳转类型指令的返回地址会保存在栈中,此结构对这种特性进行了优化。
● 分支预测单元:通过记录曾经执行过的分支型指令的历史信息来预测将来指令流的PC值,如果预测错误,将会导致流水线中错误的指令全部被清除,这将严重影响性能。
● 重排序缓冲:记录指令执行的状态,对已执行完毕的指令按序使其从流水线中正式离开。
● 发射队列:存放待执行的指令信息,其中的指令准备就绪即会被交给相应的功能单元计算结果。
● 分发:将译码后的指令放入发射队列和重排序缓冲等状态信息记录表中。
此处理器的前端部分主要是分支预测、取指、译码。分支预测是为了在遇到分支指令时也可以保证连续地提供指令流而不需要等待分支类型指令的结果,通过与一个称作L0的MOP (Macro-Operation,宏操作)缓存协作,最终峰值可以输出6路宏操作。由于有预测错误代价,因此大量使用分支型指令会严重影响处理器的吞吐量。
在后端接收6路宏操作进行寄存器重命名、重排序缓冲、体系结构状态提交,得到10路微操作并进行分发(dispatch),通过发射队列(issue queue)进入12路执行单元通道。发射队列分为3个部分——整数运算、高级SIMD和访存。每路执行单元不关心其他单元的执行内容及情况。所以在程序中可以利用这种设计对代码中指令的顺序进行优化,进而获得更大的吞吐量。每种不同的指令在执行级使用的功能单元不同,功能单元执行内容和数量不同会导致不同类别指令在CPU中的吞吐量和延迟不同。详细参数请查阅ARM的官方手册。
图1.2 Cortex-A77的微架构
下面将根据Cortex-A77的结构特点,介绍如何在程序中针对指令吞吐量进行优化。
分发阶段经过调度分配给发射阶段中不同执行通道的微操作数量有限制。当代码中的指令堆叠不超过限制时,在分发阶段将不会出现硬件缺失导致的微操作停顿。所以尽量不要将超过限制的同类型指令堆叠在一起,这样对吞吐量是不利的,微架构缓冲将被迅速填满,阻碍后面指令的执行。将具有不同微操作且不相关的指令交错放在一起,可以达到尽可能充满执行单元通道的目的,这样利用率和吞吐量都是最优的。但这只是理想情况,大多数时候,前后指令是存在数据相关性的。这个时候,指令将会滞留在分发阶段,等待操作数准备就绪。
在上述限制内根据执行元件数量及执行元件流水线吞吐量安排同类型的指令可以得到最大的吞吐量。例如,FP divide, S-form(S指单精度浮点型数据)指令组,AArch64的指令有FDIV,这是一条浮点数除法指令,使用FP/ASIMD 0(V0)执行。SIMD器件可以并行对4组数据执行相同的操作,由于除法指令使用的是迭代算法完成,因此只有一个除法操作完成了才可以进行下一个除法操作。一个除法操作的执行延迟是7个CPU周期,所以在数据不相关的时候其吞吐量为4/7(指令数每周期),只需要将4条以上数据不相关的FDIV指令排列在一起即可达到这个最大吞吐量。这种方法等效于使用一条类似的SIMD指令,即指令组ASIMD FP divide,Q-form,F32(Q-form指4路数据并发,F32指单精度浮点型数据)中的FDIV指令(注意,这里指令名和之前相同,但是这里使用的是ASIMD寄存器)。
之前简单介绍过分支预测在流水线技术中的重要性,为了配合流水线技术获得最佳性能,代码中应尽量保证在一个对齐32字节的存储区中放置超过4条分支指令。要使内存复制操作具有较大的吞吐量,建议在原本的循环中展开6次循环,以适应具有6路宏操作输出的结构,避免过多地进行无谓的分支预测。
AArch64有4种异常级别(Exception Level)。4种异常级别分别是EL0、EL1、EL2、EL3,如图1.3所示。其中EL0的权限最低,EL3的权限最高,EL2和EL3两种安全级别是可选的。从信息安全的角度,系统分为安全世界(secure world)和非安全世界(non-secure world)。
从图1.3可以看到,在EL0和EL1,都存在非安全世界和安全世界。监控模式在EL3实现,一般用来运行固件代码。系统管理程序在EL2实现,一般用来运行虚拟化程序。如果EL2启用,那么在EL1就可以运行多个操作系统(例如,一个虚拟化系统上可以运行多个Linux操作系统)。一个操作系统(已启用EL1)上可以运行多个应用程序(EL0)。EL2和EL3是可选的,可以按需实现。
图1.3 AArch64的4种异常级别
为了对指令乱序、数据预取等进行更好的控制,ARMv8架构定义了两种类型的内存——普通内存和设备内存。普通内存主要用来存储数据和代码,处理器能够对这些内存做re-order、re-size、repeate、数据预取等操作。设备内存指的是I/O等内存,主要用于外设。对这部分内存的访问一般会产生副作用,因此读写设备内存会有更多的限制。设备内存不允许预读。对于某些内存区域,还可以赋予可执行(executable)、可共享(shareable)、可缓存(cacheable)等属性。
近年来,ARM服务器发展迅速,为了更好地兼容已有的服务器程序,被标记为普通内存的内存数据默认配置成可以非对齐访问。但是作为可选项,普通内存数据也可以配置成当访问非对齐内存数据时触发一个同步数据异常(synchronous data abort)。标记为设备内存的内存数据不支持非对齐访问。指令预取(instruction fetch)也必须是对齐的。
herd7是一个专门用来分析和检查内存模式问题的工具,使用以下代码可以在Ubuntu系统上安装herd7工具。
sudo apt-get install dune
sudo apt-get install opam
git clone ******//github****/herd/herdtools7.git
cd herdtools7/
sudo make all PREFIX=/usr/local
sudo make install PREFIX=/usr/local
diyone7 -arch AArch64 PodWW L Rfe A PodRR Fre
diyone7 -arch X86 PodWW L Rfe A PodRR Fre
herd7 -model ./sc.cat SB.litmus
herd7 a.litmusARMv6之前的处理器不支持对称式多处理机(Symmetric Multiprocessor,SMP),也没有提供原子指令,这个时候的原子操作都是通过关中断实现的。从ARMv7开始引入了LDREX/STREX原子指令,用于提高原子操作的性能。本节内容不探讨ARM架构原子指令的历史,这部分内容可以参考郭健在蜗窝科技论坛上发表的文章“Linux内核同步机制之(一):原子操作”。本节重点讨论ARMv8.1新引入的大系统扩展(ARMv8.1-LSE, ARMv8.1 Large System Extension)特性,这个特性主要是针对ARM服务器的高性能计算(High Performance Computing,HPC)引入的,同时对Android这种日趋复杂的大系统是一个“利好”。
AArch64引入了一组原子操作指令,包含3类,如表1.5所示。
表1.5 AArch64的原子操作指令
| 指令 |
描述 |
|---|---|
| CAS、CASP |
比较和交换指令 |
| LD<OP>、ST<OP> |
原子内存操作指令,其中<OP>可以是ADD、CLR、EOR等 |
| SWP |
交换指令 |
这些指令只存在于A64指令集中,在ARMv8.1上强制实现ARMv8.1的虚拟化扩展特性(ARMv8.1-VHE)依赖这个特性,这些指令只支持寄存器基地址的内存访问模式。通过检查系统控制寄存器ID_AA64ISAR0的Atomic控制域,判断当前的CPU是否支持该特性,如图1.4所示。处理器特性检测的更多内容可以参考1.10节的内容。
图1.4 处理器的特性检测
其中,ID_AA64ISAR0是一个64位的控制寄存器,Atomic控制域占用了第20~23位。当这4位的值为0b0000时,LSE原子指令没有实现;当这4位的值为0b0010时,LDADD、LDCLR、LDEOR、LDSET、LDSMAX、LDSMIN、LDUMAX、LDUMIN、CAS、CASP、SWP这些原子指令都支持;其他的值作为保留值。更多的LSE原子指令可以参考文档DDI0487F_b_ARMv8_arm.pdf。
GCC-8.2支持ARMv8A LSE处理器特性,如果要使能这个特性,则需要加上编译选项-march=ARMv8-a+lse。下面是使用C++代码实现的原子加法操作,使用两种方法通过GCC-8.2编译器生成汇编代码。
C++源代码如下。
#include <atomic>
main () {
std::atomic<unsigned long long int> value;
value = 1;
++value;
return value;
}不加ARMv8-a+lse编译选项生成的汇编代码如下。
//gcc 8.2 compiler option: -O3
main:
sub sp, sp, #16
mov x1, 1
add x0, sp, 8
stlr x1, [x0]
.L2:
ldaxr x1, [x0]
add x1, x1, 1
stlxr w2, x1, [x0]
cbnz w2, .L2
ldar x0, [x0]
add sp, sp, 16
ret添加ARMv8-a+lse编译选项生成的汇编代码如下。
//gcc 8.2 compiler option: -O3 -march=ARMv8-a+lse
main:
sub sp, sp, #16
mov x1, 1
add x0, sp, 8
stlr x1, [x0]
mov x1, 1
ldaddal x1, x1, [x0]
ldar x0, [x0]
add sp, sp, 16
ret通过以上示例代码可以看到,用传统的方法实现一个加法操作需要两条传统的原子指令LDAXR、STLXR和一条普通指令ADD。使能LSE之后,只需要一条新的原子指令LDADDAL即可。
本节内容以ARM Cortex A76处理器为例进行讨论。一般来说,每个核上存在一个独立的L1指令缓存和一个独立的L1数据缓存,并且一般都会共享一个更大的、统一的L2缓存。通常手机厂商可能会扩展出L3缓存,如图1.5所示。本书只讨论L1/L2缓存,不讨论L3缓存。
图1.5 处理器缓存
内存管理单元(Memory Management Unit,MMU)通过转换表(translation table)和转换寄存器(translation register)来控制哪些内存被缓存。所有缓存数据都以行为单位来处理,数据在缓存中的位置是通过物理地址来确定的。缓存数据的每一行包含tag bit、valid bit、dirty data bit。为了减少缓存争用(cache contention),ARM缓存被设计成组相联结构。4路组相联(4-way set-associative)结构中,缓存行的长度为64B,采用伪最近最少使用(pseudo-LRU)替换算法。
讨论分支预测离不开缓存。如前所述,通常应用处理器(Application Processor,AP)会配备两级或者更多级的缓存,例如在每个处理器核上配备L1指令缓存和L1数据缓存,然后配备一个更大的L2缓存。众所周知,MMU一般有3个用途——控制缓存策略,控制内存访问权限,提供虚拟地址到物理地址的转换。
安全威胁是各大处理器厂商都面临的一个重要问题。本节将会对ARM架构引入的部分安全相关的特性做简要介绍。
为了防止边信道攻击,ARM架构提供了一系列的屏障指令,用来保护敏感数据。主要包括CSDB、SSBB、CFP、CSV2、CSV3等。
ARMv8.1-PAN是PSTATE寄存器的一个标志位,当PAN标志位使能的时候,可以阻止处理器在EL1或者EL2访问EL0对应的数据。对于Linux系统来说,这会阻止内核访问用户空间的敏感数据。这个特性在ARMv8.1上要求强制实现。
下面以内核的copy_from_user函数为例来看一下PAN的用法。它依赖内核config: CONFIG_ARM64_PAN。以下的代码片段是__arch_copy_from_user的实现,这个函数的入口和结尾的地方分别会调用到uaccess_enable_not_uao和uaccess_disable_not_uao。函数uaccess_enable_not_uao会把PAN标志位清零,从而使能内核对用户数据的访问。函数uaccess_disable_not_uao会把PAN标志位置1,从而阻止内核对用户数据的访问。
./arch/arm64/lib/copy_from_user.S
ENTRY(__arch_copy_from_user)
uaccess_enable_not_uao x3, x4, x5
add end, x0, x2
#include "copy_template.S"
uaccess_disable_not_uao x3, x4
mov x0, #0
ret如果ARM硬件不支持PAN功能则可以使用代码来模拟PAN功能。以下代码是Linux内核软件模拟的PAN功能,实际上,这里通过使ttbr0指向一个非法地址来防止内核获取用户数据。
arch/arm64/include/asm/uaccess.h
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
static inline void __uaccess_ttbr0_disable(void)
{
unsigned long flags, ttbr;
local_irq_save(flags);
ttbr = read_sysreg(ttbr1_el1);
ttbr &= ~TTBR_ASID_MASK;
write_sysreg(ttbr - RESERVED_TTBR0_SIZE, ttbr0_el1);
isb();
write_sysreg(ttbr, ttbr1_el1);
isb();
local_irq_restore(flags);
}其中write_sysreg(ttbr - RESERVED_TTBR0_SIZE, ttbr0_el1)这条语句中的ttbr0指向一个非法地址,如下所示。
// reserved_ttbr0是一个全0的页的起始地址
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
reserved_ttbr0 = .;
. += RESERVED_TTBR0_SIZE;
#endif
swapper_pg_dir = .;
. += PAGE_SIZE;
swapper_pg_end = .;MTE是ARM v8.5引入的一个硬件特性,用来检测内存方面的问题。Google于2019年在它的官方博客上宣布了对此特性的支持。MTE包括两种执行模式:一种是精确模式(precise mode),它能提供详细的内存问题诊断信息;另一种是非精确模式(imprecise mode),它只会消耗很少的CPU资源,默认适合开启。
2012年,Red Hat、SUSE、Debian等发行版都具有ARM发行版的一些功能。每个发行版维护10000个左右的包,要编译和运行这些包,工作量十分庞大。
那时候量产的ARM系统都是32位的,除个别公司能买得起ARM公司的ARM64参考设计,ARM64基本还停留在大家的脑海里面。即使在10年前,大家用的PC普遍也是64位系统,为什么当时的各大发行版要移植到32位的ARM系统上呢?
这些问题都需要站在体系结构的实现和演进的角度来看。
从服务器到嵌入式设备,虚拟化无处不在。ARM系统目前的虚拟化支持已经比较完善了,而这些始于2011年,为了在演进中迭代ARM的虚拟化技术,ARM公司在32位的ARM系统上推出了虚拟化扩展。当时虚拟化扩展基于ARMv7a。在此之前,三星等公司通过ARM内存管理中的域(domain)机制,曾经做过ARM虚拟化的早期实验。
从虚拟化扩展开始,ARM从架构上正式具备了支持主流虚拟化能力的可能。由于ARM的虚拟化比x86晚很多年,因此ARM系统有机会继承x86虚拟化设计的一些经验,例如x86早期的虚拟化不支持二级页表转换,需要通过影子页表(shadow page table)把一级和二级页表合二为一,这造成了虚拟机切换时的性能开销。后来x86为了解决这个问题,引入了扩展页表(Extended Page Table,EPT)。有了这个经验,ARM系统在第一次做虚拟化时,就考虑到了二级页表转换的需求,也就避免了x86虚拟化早期在内存方面的性能瓶颈。
ARM的虚拟化扩展在日后的ARM64架构中得到了继承,虽然ARM32和ARM64的AArch64执行状态不同,但是虚拟化的设计及其包含的寄存器都是一致的。在ARM32上开发的虚拟化管理层和适配的上层软件,以及与ARM64汇编无关的部分,都可以直接从ARM32移植到ARM64,这大大缩短了ARM64系统上提供虚拟化支持的时间。同时,这更容易暴露出进一步的需求。下面结合技术演进介绍。
ARM KVM虚拟化是Christoffer Dall的工作。然而,ARM KVM虚拟化的实现和x86的KVM实现不同,站在ARM体系结构的设计上看,由于ARM已经有了安全世界和非安全世界,没有办法像x86一样设计出根模式(root mode)和非根模式(non-root mode)这样对称(或称为正交)的虚拟化方案。所以,ARM的虚拟化方案是在已有的操作系统内核层和最高特权级别(安全监视级别,从非安全世界到安全世界的门卫)之间增加了一层虚拟化层。这对于xen这种运行在操作系统下面的虚拟化机制(即type1虚拟化)是可行的,但是对KVM来说,Linux没办法直接运行在虚拟化层,因此在ARM KVM设计中,主机Linux内核(host Linux kernel)和虚拟机监控程序(hypervisor)之间需要频繁切换,这影响了性能。
造成了性能影响该怎么办?ARM没法引入根模式,怎么让主机Linux内核运行在虚拟化层呢?ARM的方案是使用虚拟化主机扩展(Virtualization Host Extension,VHE)机制,在VHE打开的情况下,主机Linux内核运行在VHE模式,也就是原有的虚拟化层,从而避免了上述性能影响。
除上面提到的计算和内存的虚拟化之外,虚拟化的设计还包括中断虚拟化和I/O虚拟化等。和ARM对于二级页表转换的支持一样,ARM对于I/O虚拟化所需的IOMMU也是从2012年才加入的,在ARM系统中称为SMMU。IOMMU和SMMU的作用是对设备地址做转换。这样虚拟机中的设备就可以通过客户机物理地址(Guest Physical Address,GPA)到主机物理地址(Host Physical Address,HPA)访问实际的物理设备。如果没有这个能力,则要么需要设备直通(passthrough)到虚拟机中,或者把设备地址一次性连续地映射到虚拟机中,或者在主机(host)里面把一个物理设备模拟为多个虚拟设备。前面两种方式没法让同一个物理设备支持多个虚拟机,会限制虚拟化系统的灵活性,最后一种方式则会有性能损失,只适合对性能要求不高的设备。
ARMv8是一个不断发展的架构,各种新的处理器特性不断地添加进来,因此很有必要对处理器特性、处理器架构的获取做一个深入研究。
用户空间的程序可以通过AT_HWCAP或者AT_HWCAP2来获取当前的CPU核是否支持某个特性。例如以下程序通过HWCAP_FP标志(hwcap flag)来检测当前CPU核是否支持浮点数。
bool floating_point_is_present(void)
{
unsigned long hwcaps = getauxval(AT_HWCAP);
if (hwcaps & HWCAP_FP)
return true;
return false;
}这些信息的底层实现实际上是通过读取ARM的系统控制寄存器获取的,有些寄存器需要EL1的权限,因此会陷入内核层执行。如下代码用于获取相关寄存器的信息,完整的代码这里不展示。
#define get_cpu_ftr(id) ({
unsigned long __val;
asm("mrs %0, "#id : "=r" (__val));
printf("%-20s: 0x%016lx\n", #id, __val);
})
...
get_cpu_ftr(ID_AA64ISAR0_EL1);
get_cpu_ftr(ID_AA64ISAR1_EL1);
get_cpu_ftr(ID_AA64MMFR0_EL1);
get_cpu_ftr(ID_AA64MMFR1_EL1);
get_cpu_ftr(ID_AA64PFR0_EL1);
get_cpu_ftr(ID_AA64PFR1_EL1);
get_cpu_ftr(ID_AA64DFR0_EL1);
get_cpu_ftr(ID_AA64DFR1_EL1);
get_cpu_ftr(MIDR_EL1);
get_cpu_ftr(MPIDR_EL1);
get_cpu_ftr(REVIDR_EL1);细心的读者可能会发现上一节用到的get_cpu_ftr()对于某些ARM64架构的机器,能够在用户空间正常运行,这个问题值得花费一些时间来研究。
对于一个典型的Linux系统,用户态对应EL0,内核态对应EL1,所以上面的代码在内核态正常运行。但在EL0(用户态)为什么可以访问EL1(内核态)的寄存器呢?
下面以在用户态访问ID_AA64ISAR0_EL1寄存器为例讨论这个问题。从ARM手册DDI0487F_b_armv8_arm.pdf中可找到如下这段伪代码。
if PSTATE.EL == EL0 then
if IsFeatureImplemented("ARMv8.4-IDST") then
if EL2Enabled() && !ELUsingAArch32(EL2) && HCR_EL2.TGE == '1' then
AArch64.SystemAccessTrap(EL2, 0x18);
else
AArch64.SystemAccessTrap(EL1, 0x18);
else
UNDEFINED;
elsif PSTATE.EL == EL1 then
if EL2Enabled() && !ELUsingAArch32(EL2) && HCR_EL2.TID3 == '1' then
AArch64.SystemAccessTrap(EL2, 0x18);
else
return ID_AA64ISAR0_EL1;
elsif PSTATE.EL == EL2 then
return ID_AA64ISAR0_EL1;
elsif PSTATE.EL == EL3 then
return ID_AA64ISAR0_EL1;从这段伪代码可以知道,当ARMv8.4-IDST特性实现的时候,EL0(用户态)可以通过0x18操作码陷入EL1(内核态),访问EL1的寄存器。对应的伪代码是AArch64.SystemAccessTrap (EL1, 0x18)。
以下重点分析AArch64.SystemAccessTrap(EL1, 0x18) 在Linux系统上的实现。前面提到的那些指令在用户空间中都属于非法指令,将会触发一个异常。以下内容摘录自ARM手册。
D1.12 Synchronous exception types, routing and priorities
Synchronous exceptions are:
Any exception generated by attempting to execute an instruction that is UNDEFINED , including:
—Attempts to execute instructions at an inappropriate Exception level.
—Attempts to execute instructions when they are disabled.
—Attempts to execute instruction bit patterns that have not been allocated.Linux内核中ARM64架构的代码近年来更新很频繁,我们以Linux kernel 5.7.0-rc3代码为例。
中断向量表定义在arch/arm64/kernel/entry.S文件中,代码如下。
/*
* 异常向量
*/
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
kernel_ventry 1, sync_invalid
kernel_ventry 1, irq_invalid
kernel_ventry 1, fiq_invalid
kernel_ventry 1, error_invalid
kernel_ventry 1, sync
kernel_ventry 1, irq
kernel_ventry 1, fiq_invalid
kernel_ventry 1, error
kernel_ventry 0, sync
kernel_ventry 0, irq
kernel_ventry 0, fiq_invalid
kernel_ventry 0, error用户空间碰到非法指令,会执行kernel_ventry 0, sync。
把kernel_ventry宏展开,可以看到它对应函数el0_sync。由以下代码流程和代码片段可知,el0_sync最终会通过0x18调用el0_sys,继续阅读相关代码,可以发现内核会对这条非法指令做进一步的处理。
el0_sync // arch/arm64/kernel/entry.S )
-> el0_sync_handler // arch/arm64/kernel/entry-common.c
asmlinkage void notrace el0_sync_handler(struct pt_regs *regs)
{
unsigned long esr = read_sysreg(esr_el1);
...
case ESR_ELx_EC_SYS64:
case ESR_ELx_EC_WFx:
el0_sys(regs, esr);
break;
#define ESR_ELx_EC_SYS64 (0x18) // arch/arm64/include/asm/esr.h本书后续章节会介绍一个专门用来学习内核知识的Linux发行版BiscuitOS。在以下配置代码中,QEMUT指定为./qemu-system-aarch64,CPU_TYPE指定为cortex-a53。
QEMUT=./qemu-system-aarch64
ARCH=arm64
CPU_TYPE=cortex-a53
OUTPUT=.
ROOTFS_NAME=ext4
CROSS_COMPILE=aarch64-linux-gnu
FS_TYPE=ext4
FS_TYPE_TOOLS=mkfs.ext4
ROOTFS_SIZE=1800
RAM_SIZE=512
CMDLINE="earlycon root=/dev/vda rw rootfstype=${FS_TYPE} console=ttyAMA0 init=/linuxrc loglevel=8"
RunBiscuitOS.sh启动BiscuitOS,然后读取cpuinfo可以得到CPU implementer和CPU part的信息。
-bash-5.0$ cat /proc/cpuinfo
processor : 0
BogoMIPS : 125.00
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd03
CPU revision : 4ARM架构提供了MIDR_EL1,用来描述CPU的主ID信息,从DDI0487F_b_armv8_arm.pdf中可以看到这个寄存器的详细描述,如图1.6所示。
图1.6 MIDR_EL1的描述
MIDR_EL1是一个64位的寄存器。其中,Implementer代表CPU implementer,从第24位到第31位,一共占8位;PartNum代表CPU part,从第4位到第15位,一共占用12位。它们都位于寄存器的第32位上,因此只需要一个32位的变量就可以保存这些信息。通过以下代码,我们也可以获取CPU的这些信息。
#include <sys/auxv.h>
typedef unsigned int uint32_t;
uint32_t get_micro_arch_id(void) {
uint32_t id;
if ((getauxval(AT_HWCAP) & HWCAP_CPUID)) {
asm("mrs %0, MIDR_EL1 " : "=r" (id));
}
return id;
}表1.6列举了主流ARM处理器厂商的芯片架构信息。
表1.6 主流ARM处理器厂商的芯片架构信息
| CPU名称 |
厂商代号 |
CPU part |
|---|---|---|
| cortex-a53 |
0x41 |
0xd03 |
| cortex-a57 |
0x41 |
0xd07 |
| cortex-a72 |
0x41 |
0xd08 |
| thunderxt83 |
0x43 |
0x0a3 |
| emag |
0x50 |
0x000 |
| qdf24xx |
0x51 |
0xc00 |
Cortex系列都是ARM公司生产的芯片,厂商代号(CPU implementer)是0x41。qdf24xx是高通公司生产的一款芯片,厂商代号是0x51。更多的ARM处理器型号信息,请参考gcc源代码下的config/aarch64/aarch64-cores.def文件。
表1.7所示为自ARMv8.1以来新增的主要处理器特性,旨在让读者对ARM架构的发展变化有一个大概的了解。
表1.7 自ARMv8.1以来新增的主要处理器特性
| 处理器特性 |
支持情况 |
描述 |
|---|---|---|
| SVE |
ARMv8.2,可选支持 |
继NEON之后,新一代的SIMD指令集 |
| ARMv8.1-LSE |
ARMv8.1,强制支持 |
大系统扩展,用来提升高端系统的吞吐量 |
| ARMv8.1-PAN |
ARMv8.1,强制支持 |
防止用户数据被非法访问 |
| ARMv8.1-PMU |
ARMv8.1,强制支持 |
PMU增强 |
| ARMv8.2-FP16 |
ARMv8.2,强制支持 |
半精度浮点指令支持 |
| ARMv8.2-LVA |
ARMv8.2,强制支持 |
大虚拟地址空间支持 |
| ARMv8.2-SHA |
ARMv8.2,可选支持 |
提供指令实现SHA系列算法(一种国际哈希算法标准) |
| ARMv8.2-SM |
ARMv8.2,可选支持 |
提供指令实现SM系列算法(中国哈希算法标准) |
| ARMv8.3-JSconv |
ARMv8.3,强制支持 |
双精度数转32位有符号整数,用于提升JavaScript的性能 |
| ARMv8.3-NV |
ARMv8.3,强制支持 |
嵌套虚拟化的支持 |
| ARMv8.3-PAuth |
ARMv8.3,强制支持 |
指针验证,防止黑客攻击 |
| ARMv8.4-RAS |
ARMv8.4,强制支持 |
对RAS的支持,提高服务器的稳定性、可靠性 |
| ARMv8.5-RNG |
ARMv8.5,强制支持 |
随机数生成 |
| ARMv8.5-MemTag |
ARMv8.5,强制支持 |
内存保护 |
学习一款处理架构的知识离不开实践,幸运的是GCC和QEMU模拟器一直在跟进ARM架构的新特性。我们可以通过GCC和某个特定的编译选项来学习编译器生成的汇编代码,也可以写一些与架构相关的测试代码,通过QEMU模拟器来运行。以下两节介绍各个版本的GCC/QEMU对ARMv8架构特性的支持。一般来说,测试一个更新的处理器架构,需要更新版本的编译器和模拟器,因此随时跟踪GCC/QEMU的最新版本是很有必要的。
现在较新的Linux发行版已经支持GCC-7及其以上的版本了,因此本节从GCC-7开始列举。GCC可以通过-mcpu或者-mtune编译选项来为某特定CPU生成良好的优化代码。例如,查阅表1.8可以发现GCC-7支持ARM Cortex-A73处理器,对应的编译器内部的代号是cortex-a73,因此使用-mcpu=cortex-a73或者-mtune=cortex-a73可以生成对ARM Cortex-A73处理器优化良好的代码。GCC也可以生成对big.LITTLE架构优化良好的代码,例如,GCC-8支持ARM Cortex-A55/CortexA75 big.LITTLE架构,因此使用-mcpu=cortex-a75.cortex-a55编译选项,就可以生成对ARM Cortex-A55/CortexA75 big.LITTLE架构优化良好的代码。
GCC通过-march=来指定某个ARM架构,查阅表1.8可知,GCC-8或者以上的编译器可以通过-march=ARMv8.4-a编译选项指定ARMv8.4。进一步,通过-march=ARMv8.4-a+aes编译选项打开对ARMv8.4 AES特性的支持。更多编译选项的解释,可以参考GCC的官方文档。
在撰写本书时,Ubuntu的最新版本是Ubuntu-20.04,因此读者可以在Ubuntu20.04上通过apt-get安装GCC-10来体验。表1.8展示了各版本GCC对处理器及ARM架构特性的支持。
表1.8 各版本GCC对处理器及ARM架构特性的支持
| GCC版本 |
对处理器的支持 |
对架构特性的支持 |
|---|---|---|
| GCC-7 |
ARM Cortex-A73(cortex-a73) |
-march可设置为ARMv8.3-a |
| GCC-8 |
ARM Cortex-A75(cortex-a75) |
-march可设置为ARMv8.4-a |
| GCC-9 |
ARM Cortex-A76(cortex-a76) |
-march可设置为ARMv8.3-a+fp16,或者 |
| GCC-10 |
ARM Cortex-A77、ARM Cortex-A76AE |
-march可设置为ARMv8.6-a |
QEMU模拟器对ARMv8架构的新特性的支持也比较及时。表1.9展示了各版本QEMU模拟器支持的ARMv8架构特性。更详细的描述可以参考QEMU的官方网站,一般需要使用源代码编译最新的QEMU模拟器来获取更多功能。
表1.9 各版本QEMU模拟器支持的ARMv8架构特性
| QEMU模拟器版本 |
对ARMv8架构特性的支持 |
|---|---|
| QEMU-4.0 |
支持ARMv8.0-SB、ARMv8.0-PredInv、ARMv8.1-HPD、ARMv8.1-LOR、ARMv8.2-FHM、ARMv8.3-PAuth、ARMv8.3-JSConv、ARMv8.5-BTI |
| QEMU-4.2 |
支持SVE |
| QEMU-5.0 |
支持ARMv8.1-VHE、ARMv8.1-VMID16、ARMv8.1-PAN、ARMv8.1-PMU、ARMv8.2-UAO、ARMv8.2-DCPoP、ARMv8.2-ATS1E1、ARMv8.2-TTCNP、ARMv8.3-RCPC、ARMv8.3-CCIDX、ARMv8.4-PMU、ARMv8.4-RCPC |
微信扫码关注【异步社区】微信公众号,回复“e59288”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。