首页
游戏
影视
直播
广播
听书
音乐
图片
更多
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
Search
1
在IC617中进行xa+vcs数模混仿
81 阅读
2
科普:Memory Compiler生成的Register file和SRAM有何区别?
73 阅读
3
virtuoso和empyrean alps模拟仿真和混仿教程
73 阅读
4
后仿中$setup,$hold与$setuphold
44 阅读
5
文档内容搜索哪家强? 15款文件搜索软件横向评测
35 阅读
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
登录
Search
标签搜索
python
Docker
vscode
linux
systemverilog
vcs
STM32
PyQT
EDA
FPGA
gvim
cadence
Alist
xilinx
UVM
uos
macos
package
MCU
risc-v
bennyhe
累计撰写
378
篇文章
累计收到
31
条评论
首页
栏目
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
页面
游戏
影视
直播
广播
听书
音乐
图片
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
搜索到
378
篇与
的结果
2026-01-12
CPU 的工作原理是什么?
想了解CPU的工作原理莫过于从头开始用最基础的元素打造一个简单CPU。接下来我会从最简单的晶体管开始一步步讲解CPU是如何构造出来的,明白了这个过程理解 CPU 的工作原理不在话下,在此之后我会从最基础的二进制机器指令一步步讲解高级语言的基本原理,通读本文后你将彻底明白CPU与高级语言的工作原理。以下内容出自我的两篇文章《你管这破玩意叫CPU?》《你管这破玩意叫编程语言?》。次回家开灯时你有没有想过,用你按的简单开关实际上能打造出复杂的 CPU 来,只不过需要的数量会比较多,也就几十亿个吧。伟大的发明过去200年人类最重要的发明是什么?蒸汽机?电灯?火箭?这些可能都不是,最重要的也许是这个小东西:这个小东西就叫晶体管,你可能会问,晶体管有什么用呢?实际上晶体管的功能简单到不能再简单,给一端通上电,那么电流可以从另外两端通过,否则不能通过,其本质就是一个开关。就是这个小东西的发明让三个人获得了诺贝尔物理学奖,可见其举足轻重的地位。无论程序员编写的程序多么复杂,软件承载的功能最终都是通过这个小东西简单的开闭完成的,除了神奇二字,我想不出其它词来。AND、OR、NOT现在有了晶体管,也就是开关,在此基础之上就可以搭积木了,你随手搭建出来这样三种组合:两个开关只有同时打开电流才会通过,灯才会亮两个开关中只要有一个打开电流就能通过,灯就会亮当开关关闭时电流通过灯会亮,打开开关灯反而电流不能通过灯会灭天赋异禀的你搭建的上述组合分别就是:与门,AND Gate、或门,OR gate、非门,NOT gate,用符号表示就是这样:道生一、一生二、二生三、三生万物最神奇的是,你随手搭建的三种电路竟然有一种很amazing的特性,那就是:任何一个逻辑函数最终都可以通过AND、OR以及NOT表达出来,这就是所谓的逻辑完备性,就是这么神奇。也就是说给定足够的AND、OR以及NOT门,就可以实现任何一个逻辑函数,除此之外我们不需要任何其它类型的逻辑门电路,这时我们认为{AND、OR、NOT}就是逻辑完备的。这一结论的得出吹响了计算机革命的号角,这个结论告诉我们计算机最终可以通过简单的{AND、OR、NOT}门构造出来,就好比基因。计算能力是怎么来的现在能生成万物的基础元素与或非门出现了,接下来我们着手设计CPU 最重要的能力:计算,以加法为例。由于CPU只认知 0 和 1,也就是二进制,那么二进制的加法有哪些组合呢:0 + 0,结果为0,进位为00 + 1,结果为1,进位为01 + 0,结果为1,进位为01 + 1,结果为0,进位为1,二进制嘛!注意进位一列,只有当两路输入的值都是 1 时,进位才是 1 ,看一下你设计的三种组合电路,这就是与门啊,有没有!再看下结果一列,当两路输入的值不同时结果为1,输入结果相同时结果为0,这就是异或啊,有没有!我们说过与或非门是逻辑完备可以生万物,异或逻辑当然不在话下,用一个与门和一个异或门就可以实现二进制加法:上述电路就是一个简单的加法器,就问你神奇不神奇,加法可以用与或非门实现,其它的也一样能实现,逻辑完备嘛。除了加法,我们也可以根据需要将不同的算数运算设计出来,负责计算的电路有一个统称,这就是所谓的arithmetic/logic unit,ALU,CPU 中专门负责运算的模块,本质上和上面的简单电路没什么区别,就是更加复杂而已。现在,通过与或非门的组合我们获得了计算能力,计算能力就是这么来的。但,只有计算能力是不够的,电路需要能记得住信息。神奇的记忆能力到目前为止,你设计的组合电路比如加法器天生是没有办法存储信息的,它们只是简单的根据输入得出输出,但输入输出总的有个地方能够保存起来,这就是需要电路能保存信息。电路怎么能保存信息呢?你不知道该怎么设计,这个问题解决不了你寝食难安,吃饭时在思考、走路时在思考,蹲坑时在思考,直到有一天你在梦中遇一位英国物理学家,他给了你这样一个简单但极其神奇的电路:这是两个NAND门的组合,不要紧张,NAND也是有你设计的与或非门组合而成的,所谓NAND门就是与非门,先与然后取非,比如给定输入1和0,那么与运算后为0,非运算后为1,这就是与非门,这些不重要。比较独特的是该电路的组合方式,一个NAND门的输出是两一个NAND门的输入,该电路的组合方式会自带一种很有趣的特性,只要给S和R段输入1,那么这个电路只会有两种状态:要么a端为1,此时B=0、A=1、b=0;要么a端为0,此时B=1、A=0、b=1;不会再有其他可能了,我们把a端的值作为电路的输出。此后,你把S端置为0的话(R保持为1),那么电路的输出也就是a端永远为1,这时就可以说我们把1存到电路中了;而如果你把R段置为0的话(S保持为1),那么此时电路的输出也就是a端永远为0,此时我们可以说把0存到电路中了。就问你神奇不神奇,电路竟然具备存储信息的能力了。现在为保存信息你需要同时设置S端和R端,但你的输入是有一个(存储一个bit位嘛),为此你对电路进行了简单的改造:这样,当D为0时,整个电路保存的就是0,否则就是1。寄存器与内存的诞生现在你的电路能存储一个比特位了,想存储多个比特位还不简单,复制粘贴就可以了:我们管这个组合电路就叫寄存器,你没有看错,我们常说的寄存器就是这个东西。你不满足,还要继续搭建更加复杂的电路以存储更多信息,同时提供寻址功能,就这样内存也诞生了。寄存器及内存都离不开上一节那个简单电路,只要通电,这个电路中就保存信息,但是断电后很显然保存的信息就丢掉了,现在你应该明白为什么内存在断电后就不能保存数据了吧。硬件的基本功让我们来思考一个问题,CPU怎么能知道自己要去对两个数进行加法计算,以及哪两个数进行加法计算呢?很显然,你得告诉CPU,该怎么告诉呢?还记得上一节中给厨师的菜谱吗?没错,CPU也需要一张菜谱告诉自己该接下来该干啥,在这里菜谱就是机器指令,指令通过我们上述实现的组合电路来执行。接下来我们面临另一个问题,那就是这样的指令应该会很多吧,废话,还是以加法指令为例,你可以让CPU计算1+1,也可以计算1+2等等,实际上单单加法指令就可以有无数种组合,显然CPU不可能去实现所有的指令。实际上CPU只需要提供加法操作,你提供操作数就可以了,CPU 说:“我可以打人”,你告诉CPU该打谁、CPU 说:“我可以唱歌”,你告诉CPU唱什么,CPU 说我可以做饭,你告诉CPU该做什么饭,CPU 说:“我可以炒股”,你告诉CPU快滚一边去吧韭菜。因此我们可以看到CPU只提供机制或者说功能(打人、唱歌、炒菜,加法、减法、跳转),我们提供策略(打谁、歌名、菜名,操作数,跳转地址)。CPU 表达机制就通过指令集来实现的。指令集指令集告诉我们 CPU 可以执行什么指令,每种指令需要提供什么样的操作数。不同类型的CPU会有不同的指令集。指令集中的指令其实都非常简单,画风大体上是这样的:从内存中读一个数,地址是abc对两个数加和检查一个数是不是大于6把这数存储到内存,地址是abc等等看上去很像碎碎念有没有,这就是机器指令,我们用高级语言编写的程序,比如对一个数组进行排序,最终都会等价转换为上面的碎碎念指令,然后 CPU 一条一条的去执行,很神奇有没有。接下来我们看一条可能的机器指令:这条指令占据16比特,其中前四个比特告诉CPU这是加法指令,这意味着该CPU的指令集中可以包含2^4也就是16个机器指令,这四个比特位告诉CPU该做什么,剩下的bit告诉CPU该怎么做,也就是把寄存器R6和寄存器R2中的值相加然后写到寄存器R6中。可以看到,机器指令是非常繁琐的,现代程序员都使用高级语言来编写程序,关于高级程序语言以及机器指令的话题请参见《你管这破玩意叫编程语言?》。大功告成现在我们有了可以完成各种计算的ALU、可以存储信息的寄存器以及控制它们协同工作的时钟信号,这些统称 Central Processing Unit,简称就是 CPU。接下来我们看一下CPU 与高级语言。程序员按照 CPU 的旨意直接用0和1编写指令,你没有看错,这破玩意就是代码了,就是这么原生态,然后放到打孔纸带上输入给CPU,CPU 开始工作,这时的程序可真的是看得见摸得着,就是有点浪费纸。这时程序员必须站在 CPU 的角度来写代码,画风是这样的:1101101010011010100100110010100111001000110111101011101101010010终于有一天程序员受够了说鸟语,好歹也是灵长类,叽叽喳喳说鸟语太没面子,你被委以重任:让程序员说人话。你没有苦其心志劳其筋骨,而是仔细研究了一下 CPU,发现 CPU 执行的指令集来来回回就那么几个指令,比如加法指令、跳转指令等等,因此你把机器指令和对应的具体操作做了一个简单的映射,把机器指令映射到人类能看懂的单词,这样上面的01串就变成了:sub $8, %rsp mov $.LC0, %edi call puts mov $0, %eax这样,程序员不必生硬的记住1011.....,而是记住人类可以认识的ADD SUB MUL DIV等这样的单词即可。汇编语言就这样诞生了,编程语言中首次出现了人类可以认识的东西细节 VS 抽象尽管汇编语言已经有人类可以认识的单词,但汇编语言和机器语言一样都属于低级语言。所谓低级语言是说你需要关心所有细节。关心什么细节呢?我们说过,CPU 是非常原始的东西,只知道把数据从一个地方搬到另一个地方,简单的操作一下再从一个地方搬到另一地方。因此,如果你想用低级语言来编程的话,你需要使用多个“把数据从一个地方搬到另一个地方,简单的操作一下再从一个地方搬到另一地方”这样的简单指令来实现诸如排序这样复杂的问题。有的同学可能对此感触不深,这就好比,本来你想表达“去给我端杯水过来”:如果你用汇编这种低级语言就得这样实现:我想你已经 Get 到了。弥补差异CPU 实在太简单了,简单到不能了理解任何稍微抽象一点诸如“给我端杯水”这样的东西,但人类天生习惯抽象化的表达,人类和机器的差距有办法来弥补吗?换句话说就是有没有一种办法可以自动把人类抽象的表达转为 CPU 可以理解的具体实现,这显然可以极大增强程序员的生产力,现在,这个问题需要你来解决。电光火石之间灵光乍现,你发现了满满的套路,或者说模式。大部分情况下 CPU 执行的指令平铺直叙的,就像这样:这些都是告诉 CPU 完成某个特定动作,你给这些平铺直叙的指令起了个名字,姑且就叫陈述句吧,statement。除此之外,你还发现了这样的套路,那就是需要根据某种特定状态决定走哪段指令,这个套路在人看来就是“如果。。。就。。。否则。。就。。。”:if * blablabla else * blablabla在某些情况下还需要不断重复一些指令,这个套路看起来就是原地打转:while * blablabla最后就是这里有很多看起来差不多的指令,就像这里:这些指令是重复的,只是个别细节有所差异,把这些差异提取出来,剩下的指令打包到一起,用一个代号来指定这些指令就好了,这要有个名字,就叫函数吧:func abc: blablabla现在你发现了所有套路:// 条件转移 if * blablablaelse * blablabla// 循环 while * blablabla// 函数 func abc: blablabla这些相比汇编语言已经有了质的飞跃,因为这已经和人类的语言非常接近了。接下来你发现自己面临两个问题:这里的blablabla该是什么呢?该怎样把上面的人类可以认识的字符串转换为 CPU 可以认识的机器指令递归:代码的本质不就是嵌套嘛,一层套一层嘛,递归天生就是来表达这玩意的 (提示:这里的表达并不完备,真实的编程语言不会这么简单,这里仅仅用作示例):if : if bool statement else statements for: while bool statements statement: if | for | statement让计算机理解递归现在还差一个问题,怎样才能把这语言最终转化为 CPU 可以认识的机器指令呢?人类可以按照语法写出代码,这些代码其实就是一串字符,怎么让计算机也能认识用递归语法表达的一串字符呢?这是一项事关人类命运的事情,你不禁感到责任重大,但这最后一步又看似困难重重,你不禁仰天长叹,计算机可太难了。此时你的初中老师过来拍了拍你的肩膀,递给了你一本初中植物学课本,你恼羞成怒,给我这破玩意干什么,我现在想的问题这么高深,岂是一本破初中教科书能解决的了的,抓过来一把扔在了地上。此时又一阵妖风挂过,书被翻到了介绍树的一章,你望着这一页不禁发起呆来:我们可以把根据递归语法写出来的的代码用树来表示啊!优秀的翻译官计算机处理编程语言时可以按照递归定义把代码用树的形式组织起来,由于这棵树是按照语法生成的,姑且就叫语法树吧。现在代码被表示成了树的形式,你仔细观察后发现,其实叶子节点的表达是非常简单的,可以很简单的翻译成对应的机器指令,只要叶子节点翻译成了机器指令,你就可以把此结果应用到叶子节点的父节点,父节点又可以把翻译结果引用到父节点的父节点,一层层向上传递,最终整颗树都可以翻译成具体的机器指令。完成这个工作的程序也要有个名字,根据“弄不懂原则”(该原则的解释见下文 :)总结本文我们从最基本的晶体管一路讲解到CPU的工作原理,再从最低级的二进制机器指令到高级语言,相信如果你能读到这里定能有所收获。最后,有同学问有没有书单,我也仔细回想自己认真读过的计算机数据,在这里也给出自认为很经典的几本,书单这东西贵精不贵多,我在这里精心挑选了10本 ,不要贪心,如果你真能把这里推荐的 10 本书读通,可以说你已经能超越 90% 的程序员了。
2026年01月12日
3 阅读
0 评论
0 点赞
2026-01-08
UVM入门
验证技术的发展历程这是我们作为一名合格验证人员装B必备,如下图所示:2000年:Verisity Design(现在的Cadence公司)引进了Verification Advisor(vAdvisor)采用了e语言,包含了激励的产生,自动比对的策略,覆盖率模型。e语言是面向对象语言,这是业界开始使用面向对象语言 进行测试平台的建立。2002年:Verisity公司公布了第一个验证库——e可重用方法学(eRM)。2003年:Synopsys公司公布了可重用验证方法学库(RVM),这个方法学采用了Synopsys公司的vera语言。2006年:Mentor公司公布了高级验证方法学(AVM)。这个方法学主要是采用了OSCI SystemC的事务抽象层方法学(TLM)标准,它是用SystemVerilog和SystemC两种语言实现的。2006年:Synopsys公司推出了验证方法学手册(VMM),这个是RVM从vera语言过度到SystemVerilog的方法学。2007年:Cadence公司推出了通用可重用验证方法学(URM),主要是从eRM从E语言过度到SystemVerilog的方法学,同时加入了TLM接口,工厂模式替换,配置机制,策励类等。2008年:Cadence公司和Mentor公司共同推出了OVM2010年:Synopsys公司推出了VMM1.2,基本上沿用了OVM的TLM通信机制,并采用了TLM2.0(OSCI最新的标准),采用OVM提出的implicit phase,并且将验证流程继续细化,工厂模式替换机制,建立类层次(建立parent关系)。并且在此基础上,提出了vmm_timeline的概念,方便各个phase之间实现跳转,增加phase或删除phase。增加了rtl_config等概念。Synopsys公司也随即宣布最新版本的VCS 同时支持UVM。2010年:ACCELLERA采用了OVM作为基础,推出了UVM验证方法学。同时引入了VMM的callbacks一些概念。作为业界方法学统一的一个雏形。2011年2月:Accellera通过了通用验证方法学1.0版,并得到三大厂商(Cadence,Synopsys和Mentor Graphics现英飞凌)的共同支持。此后Accellera陆续推出了UVM1.1, UVM1.1a,UVM1.1b,UVM1.1c,UVM1.1d这几个版本。2014年6月,Accellera有推出了通用验证方法学1.2版。目前UVM最新版是UVM-1800.2-2017-1.0(版本号命名都变了哈)UVM(Universal Verification Methodology) UVM几乎完全继承了OVM,同时又采纳了Synopsys在VMM中的寄存器解决方案RAL。UVM继承了OVM和VMM的优点,克服了各自的缺点,代表了验证方法学的发展方向。UVM介绍UVM是一个以SystemVerilog为主体的验证平台开发框架,验证工程师利用其可重用组件可以构建具有标准化层次结构和接口的功能验证环境。UVM是一个库,在这个库中,几乎所有的东西都是使用类(class)来实现的。类是面向对象编程语言中最伟大的发明之一,是面向对象的精髓所在。使用UVM的第一条原则是:验证平台中所有的组件都应该派生自UVM中的类。当要实现一个功能时,首先应该想到的就是从UVM的某个类派生出一个新的类,类中可以有成员变量,也可以有函数和任务,通过成员变量、函数或任务实现所期望的功能。对于验证方法学来说,分层的测试平台是一个关键的概念。虽然分层似乎会使测试平台变得更复杂,但它能够把代码分而治之,有助于减轻工作负担,而且重复利用效率提升。基于UVM的验证平台可以类似分为五个层次:信号层、命令层、功能层、场景层和测试层:如何学习UVMUVM1.2版本包含121个文件,311个类。从经验来说,我们搭建一个普通的UVM验证环境,大约需要编写10个文件,20个类左右。这里分享一下对初学者的个人建议:第一阶段-基础:学习UVM之前, 熟悉SV是必须的,关于SV的系统学习首推“绿皮书”。工作中“asic-world”这个网站可以作为我们的查询手册。第二阶段-学习:有了SV的基础和OOP的思想,我们就可以开始学习UVM了(很多人入门是看《UVM实战》-张强著 )。这时我们需要了解UVM构架,各种component, phase管理机制等。最好配合实例代码一边看书一边敲代码做练习。第三阶段-应用:当然我们有的朋友会说:"我现在的公司还没有用UVM来搭环境呀,怎么应用啊?" 但是,没有条件我们可以创造条件呀! 网上这么多开源的IP,找一个自己感兴趣的,把自己当成验证主管,自己玩儿呗。或者把已经做过的项目再用UVM搭一遍(别说你没时间哦~第四阶段-研究:这个时候你已经是熟手,并能够独立搭建复用性很强的UVM环境了。这时可以去看看UVM源码,帮助同事解决实际工作中的各种问题。研究一下UVM代码自动生成,UVMF是什么等等
2026年01月08日
2 阅读
0 评论
0 点赞
2026-01-08
CPU 系统级验证 —— 指令集验证 ——OpenHW core-v 验证环境及文件分析
本文记录的相关源工程和文件为:core-v RISCV 核功能验证工程:https://github.com/openhwgroup/core-v-verifcore-v 验证策略:https://core-v-docs-verif-strat.readthedocs.io/en/latest/#core-v 系列核 cva6 工程:https://github.com/openhwgroup/cva6core-v 系列核 cv32e40p 工程:https://github.com/openhwgroup/cv32e40pLowRISCV Ibex 核工程:https://github.com/lowRISC/ibexcore-v RISCV 核相关说明文档:https://github.com/openhwgroup/core-v-docsRISCV ISA 指令流(instruction stream)生成器:https://github.com/openhwgroup/force-riscvcore-v 系列指令流(instruction stream)生成器:https://github.com/openhwgroup/core-v-isgsail 语言介绍:https://www.cl.cam.ac.uk/~pes20/sail/REMS 的 sail RISCV 开源项目:https://github.com/rems-project/sail-riscvopenHW 全称为 open-source hardware,是一个支持开源软件和硬件的非盈利组织。该组织开源的内容包括开源 CPU 核、相关 IP、工具和软件等。指令集验证指令集验证的需求能够使用操作数产生合法的基本整数运算指令在指令执行完成后能够检查通用寄存器的状态指令执行完成后能检查副效应,例如溢出等。指令集验证内容指令集的验证需要验证工程师思考该指令需要测试的特征是什么,为了确保指令的正确执行需要检查的内容是什么,为了确保该特征被测试需要进行哪些激励和配置等。验证的充不充分取决于验证人员对 CPU 微架构的理解和风险应对能力。(1)RISCV 基本指令定向测试设计出来的 CPU 首先需要兼容 RISCV 基本指令,即需要满足 RISCV 基本整数指令及相关标准指令集。主要测试的内容有:溢出检测及相关标志位下溢检测及相关标志位无指令执行的副效应,比如异常的通用处理寄存器变化,异常的条件代码使用 x0-x31 作为 rs1/rs2使用 x0-x31 作为 rdx0 寄存器的值始终为 0set/clear 所有立即数的比特位set/clear 源寄存器 rs1/rs2 的比特位根据 7/8 两条 set/clear 目的寄存器 rd 的比特位(2)自定义扩展指令定向测试对于自定义扩展指令集的验证,该验证需要修改相关的工具链并通过相关测试验证工具链的修改成功,再进行(1)中的测试。(3)随机约束测试对进行大量的随机指令测试指令集验证激励的种类指令集测试激励分为自检(self-checking)和预存(pre-existing)两种。自检测试激励运行时按照源文件中指令顺序依次,并同时将指令执行结果和参考值作检查,若出错则程序跳转到 fail,打断正常执行顺序,立即执行完成。类似的测试集有 riscv-tests、riscv-compilance-tests 等预存测试激励运行时不会打断执行顺序,执行的结果会和指令集模拟器(ISS)的结果作对比,最终统计对比的结果。类似的测试集有 riscv-dv 等指令集验证 pass/fail 的标志测试的 pass/fail 与上述激励的种类有关self checking:自检性质的测试激励在每条指令执行时就会同时将结果和参考值作对比,若失败则跳转到 failsignature check:特征检查,相对自检来说更加复杂深入。测试的结果将被用来计算该指令的某个特征,这将会和预先确定好的较标准特征作对比。采用该检查的为 riscv-complience-tests。check against ISS:与指令集模拟器对比检查。该情况下,testcase 并不知道测试的参考值,仅仅给 DUT 提供激励,testbench 会对比 DUT 和 ISS 的输出结果确定测试的 pass/fail。该检查较被经常使用,因为它是的 testcase 更加简单。采用该检查的如 riscv-dvcheck against RM:与参考模型对比检查,该检查方式与第三点类似,但相对 ISS 更加通用。通常用作 ISS 检测的补充。assertion check:通过断言判断是否执行出错。相关 CPU 核的验证工程cv32e40pCV32E40P (原先 PULP 开发的 RI5CY) 是一个 32bit 的 4 级流水线核,支持整数运算指令、乘法除法指令、单浮点运算指令、压缩指令以及 DSP 扩展指令(包括硬件循环、SIMD 扩展、位操作和增量指令)该核的验证环境有两套,一套是专门的 core testbench;一套是 UVM 环境搭建的 testbench,该环境下 testcase 的产生是可以基于 UVM 环境的(这种方式类似于 Ibex 核验证环境)。两套环境均由 systemverilog 编写。均包含了指令集验证、中断验证、CSR 寄存器验证、异常验证、debug 验证等。1 core testbench该工程是 PULP 遗留下的产物,OpenHW 做了一定修改并持续维护。验证环境中已集成了指令集模拟器。该工程的测试集激励包含指令兼容性测试(偏定向测试,如 riscv-tests 和 riscv-compilance-tests)和随机约束(google riscv-dv),和其他的一些 fireware 测试用例。测试环境结构如下:该 testbench 简单,运行较快,通过 verilator 运行,但是有额外的开销且不能获取覆盖率。2 UVM 验证环境(uvmt_cv32)cv32 核的 UVM 验证环境已搭建完成,在 DSIM(Metrics)运行也很稳定。验证环境中已集成了指令集模拟器。uvm 验证环境的 testcase 均是由 uvm 相关类生成,同时该环境还集成了 riscv-dv,但具体的激励生成还需要再分析。具体验证环境结构UVM 内部组件关系debug、interrupt、control/status 均通过 UVM_agent 组件产生,且通过 uvm_sequence 组件输入激励。UVM 运行流程采用 UVM 搭环境的优势a) 便于验证环境的结构建模b) UVM 环境类支持完全的 UVM 运行流程和 log 服务c) 使用 UVM sequence-item 类可以产生随即约束激励d) 使用内建于验证环境中的参考模型预测执行的结果。(imperas 有现成的开源 ISS)e) scoreboard 可以比较参考模型和 RTL 的结果。f) 功能覆盖率和代码覆盖率确保验证的完全性。且 UVM 测试环境可复用性较高,也可以用在 CVA6 核上。CVA6CVA6(ariane) 是一个 6 级流水线、单发射、顺序执行的 RV32GC 或 RV64GC 核,支持 M/U/S 三种模式,支持 linux 操作系统。该核现存的验证环境尚未成熟,但同样可以构建 UVM 的验证环境。该工程的指令集测试激励包括 riscv 官方测试套件(即 riscv-tests)。IbexIbex 不是 openHW 的 core-v 系列,而是 lowRISC 持续维护的较为成熟的一款核,验证环境也较为成熟,其结构和运行非常类似于 CV32E 和 CVA6 的激励随机约束。该验证环境激励的产生同样是基于 GOOGLE 的随机指令生成器验证环境特征运行有效性testcase 在验证环境中会持续运行完成,除非激励运行出错方便调试能够增加功能覆盖率能够对比检查 RTL 运行的结果指令集描述语言 ——sailsail 是 REMS 构建的一种描述指令集的语言,可以认为是一种机器可读的形式化 ISA 模型。REMS 是英国的一家学术组织,该组织已构建出 RV32IMAC 和 RV64IMAC 指令集的 sail model,链接见上。riscv 基金会有意用其来描述 RISCV 指令集。初步来看,sail 可以用来形式化 testbench 断言的参考模型,这些 assertion 可以根据 sail spec 来验证基于 RISCV ISA 的某微架构。目前该领域 OneSpin GapFree 已经在 sail model 和 RTL 代码间做了可比性的 check,该公司仍在探索如何充分使用 sail model该语言将持续关注。
2026年01月08日
2 阅读
0 评论
0 点赞
2026-01-08
从操作系统内存管理来说,malloc申请一块内存的背后原理是什么?
作者:码农的荒岛求生链接:https://www.zhihu.com/question/33979489/answer/1849544189来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。对内存分配器透彻理解是编程高手的标志之一。如果你不能理解malloc之类内存分配器实现原理的话,那你可能就写不出高性能程序,写不出高性能程序就很难参与核心项目,参与不了核心项目那么很难升职加薪,很难升级加薪就无法走向人生巅峰,没想到内存分配竟如此关键,为了走上人生巅峰你也要势必读完本文。现在我们知道了,对内存分配器透彻的理解是写出高性能程序的关键所在,那么我们该怎样透彻理解内存分配器呢?还有什么能比你自己动手实现一个理解的更透彻吗?接下来,我们就自己实现一个malloc内存分配器。读完本文后内存分配对你将不再是一个神秘的黑盒。在讲解实现原理之前,我们需要回答一个基本问题,那就是我们为什么要发明内存分配器这种东西。内存申请与释放程序员经常使用的内存申请方式被称为动态内存分配,Dynamic Memory Allocation。我们为什么需要动态的去进行内存分配与释放呢?答案很简单,因为我们不能提前知道程序到底需要使用多少内存。那我们什么时候才能知道呢?答案是只有当程序真的运行起来后我们才知道。这就是为什么程序员需要动态的去申请内存的原因,如果能提前知道我们的程序到底需要多少内存,那么直接知道告诉编译器就好了,这样也不必发明malloc等内存分配器了。知道了为什么要发明内存分配器的原因后,接下来我们着手实现一个。在此之前,顺便给大家推荐一份学习算法刷题的绝技资料,除了本文讲到的底层技术,算法也不可忽视,如果想进BAT、TMD、快手这样的一线大厂,认认真真过上一遍,这些大厂算法面试一关大部分题目都不在话下:Github疯传!阿里P8大佬写的Leetcode刷题笔记,秒杀80%的算法题!mp.weixin.qq.com/s/A-HPH3Tkl8KOvZgdRArvIg程序员应如何看待内存实际上,现在这个年代的程序员是很幸福的,程序员很少去关心内存分配的问题。作为程序员,可以简单的认为我们的程序独占内存,注意,是独占哦。写程序时你从来没有关心过如果我们的程序占用过多内存会不会影响到其它程序,我们可以简单的认为每个程序(进程)独占4G内存(32位操作系统),即使我们的物理内存512M。不信你可以去试试,在即使只有512M大小的内存上你依然可以申请到2G内存来使用,可这是为什么呢?关于这个问题我们在后续文章中会详细阐述。总之,程序员可以放心的认为我们的程序运行起来后在内存中是这样的:作为程序员我们应该知道,内存动态申请和释放都发生在堆区,heap。我们使用的malloc或者C++中的new申请内存时,就是从堆区这个区域中申请的。接下来我们就要自己管理堆区这个内存区域。堆区这个区域实际上非常简单,真的是非常简单,你可以将其看做一大数组,就像这样:从内存分配器的角度看,内存分配器根本不管你是整数、浮点数、链表、二叉树等数据结构、还是对象、结构体等这些花哨的概念,在内存分配器眼里不过就是一个内存块,这些内存块中可以装入原生的字节序列,申请者拿到该内存块后可以塑造成整数、浮点数、链表、二叉树等数据结构以及对象、结构体等。我们要在这片内存上解决两个问题:实现一个malloc函数,也就是如果有人向我申请一块内存,我该怎样从堆区这片区域中找到一块返回给申请者。实现一个free函数,也就是当某一块内存使用完毕后,我该怎样还给堆区这片区域。这是内存分配器要解决的两个最核心的问题,接下来我们先去停车场看看能找到什么启示。从停车场到内存管理实际上你可以把内存看做一条长长的停车场,我们申请内存就是要找到一块停车位,释放内存就是把车开走让出停车位只不过这个停车场比较特殊,我们不止可以停小汽车、也可以停占地面积很小的自行车以及占地面积很大的卡车,重点就是申请的内存是大小不一的,在这样的条件下你该怎样实现以下两个目标呢?快速找到停车位,在内存申请中,这涉及到以最大速度找到一块满足要求的空闲内存尽最大程度利用停车场,我们的停车场应该能停尽可能多的车,在内存申请中,这涉及到在给定条件下尽可能多的满足内存申请需求现在,我们已经清楚的理解任务了,那么该怎么实现呢?任务拆分现在我们已经明确要实现什么以及衡量其好坏的标准,接下来我们就要去设计实现细节了,让我们把任务拆分一下,怎么拆分呢?我们可以自己想一下从内存的申请到释放需要哪些细节。申请内存时,我们需要在内存中找到一块大小合适的空闲内存分配出去,那么我们怎么知道有哪些内存块是空闲的呢?因此,第一个实现细节出现了,我们需要把内存块用某种方式组织起来,这样我们才能追踪到每一块内存的分配状态。现在空闲内存块组织好了,那么一次内存申请可能有很多空闲内存块满足要求,那么我们该选择哪一个空闲内存块分配给用户呢?因此,第二个实现细节出现了,我们该选择什么样的空闲内存块给到用户。接下来我们找到了一块大小合适的内存块,假设用户需要16个字节,而我们找到的这块空闲内存块大小为32字节,那么将16字节分配给用户后还剩下16字节,这剩下的内存该怎么处理呢?因此,第三个实现细节出现了,分配出去内存后,空闲内存块剩余的空间该怎么处理?最后,分配给用户的内存使用完毕,这是第四个细节出现了,我们该怎么处理用户还给我们的内存呢?以上四个问题是任何一个内存分配器必须要回答的,接下来我们就一一解决这些问题,解决完这些问题后一个崭新的内存分配器就诞生啦。管理空闲内存块空闲内存块的本质是需要某种办法来来区分哪些是空闲内存哪些是已经分配出去的内存。有的同学可能会说,这还不简单吗,用一个链表之类的结构记录下每个空闲内存块的开始和结尾不就可以了,这句话也对也不对。说不对,是因为如果要申请内存来创建这个链表那么这就是不对的,原因很简单,因为创建链表不可避免的要申请内存,申请内存就需要通过内存分配器,可是你要实现的就是一个内存分配器,你没有办法向一个还没有实现的内存分配器申请内存。说对也对,我们确实需要一个类似链表这样的结构来维护空闲内存块,但这个链表并不是我们常见的那种。因为我们无法将空闲内存块的信息保存在其它地方,那么没有办法,我们只能将维护内存块的分配信息保存在内存块本身中,这也是大多数内存分配器的实现方法。那么,为了维护内存块分配状态,我们需要知道哪些信息呢?很简单:一个标记,用来标识该内存块是否空闲一个数字,用来记录该内存块的大小为了简单起见,我们的内存分配器不对内存对齐有要求,同时一次内存申请允许的最大内存块为2G,注意,这些假设是为了方便讲解内存分配器的实现而屏蔽一些细节,我们常用的malloc等不会有这样的限制。因为我们的内存块大小上限为2G,因此我们可以使用31个比特位来记录块大小,剩下的一个比特位用来标识该内存块是空闲的还是已经被分配出去了,下图中的f/a是free/allocate,也就是标记是已经分配出去还是空闲的。这32个比特位就是header,用来存储块信息。剩下的灰色部分才是真正可以分配给用户的内存,这一部分也被称为负载,payload,我们调用malloc返回的内存起始地址正是这块内存的起始地址。现在你应该知道了吧,不是说堆上有10G内存,这里面就可以全部用来存储数据的,这里面必然有一部分要拿出来维护内存块的一些信息,就像这里的header一样。跟踪内存分配状态有了上图,我们就可以将堆这块内存区域组织起来并进行内存分配与释放了,如图所示:在这里我们的堆区还很小,每一方框代表4字节,其中红色区域表示已经分配出去的,灰色区域表示空闲内存,每一块内存都有一个header,用带斜线的方框表示,比如16/1,就表示该内存块大小是16字节,1表示已经分配出去了;而32/0表示该内存块大小是32字节,0表示该内存块当前空闲。细心的同学可能会问,那最后一个方框0/1表示什么呢?原来,我们需要某种特殊标记来告诉我们的内存分配器是不是已经到末尾了,这就是最后4字节的作用。通过引入header我们就能知道每一个内存块的大小,从而可以很方便的遍历整个堆区。遍历方法很简单,因为我们知道每一块的大小,那么从当前的位置加上当前块的大小就是下一个内存块的起始位置,如图所示:通过每一个header的最后一个bit位就能知道每一块内存是空闲的还是已经分配出去了,这样我们就能追踪到每一个内存块的分配信息,因此上文提到的第一个问题解决了。接下来我们看第二个问题。怎样选择空闲内存块当应用程序调用我们实现的malloc时,内存分配器需要遍历整个空闲内存块找到一块能满足应用程序要求的内存块返回,就像下图这样:假设应用程序需要申请4字节内存,从图中我们可以看到有两个空闲内存块满足要求,第一个大小为8字节的内存块和第三个大小为32字节的内存块,那么我们到底该选择哪一个返回呢?这就涉及到了分配策略的问题,实际上这里有很多的策略可供选择。First Fit最简单的就是每次从头开始找起,找到第一个满足要求的就返回,这就是所谓的First fit方法,教科书中一般称为首次适应方法,当然我们不需要记住这样拗口的名字,只需要记住这是什么意思就可以了。这种方法的优势在于简单,但该策略总是从前面的空闲块找起,因此很容易在堆区前半部分因分配出内存留下很多小的内存块,因此下一次内存申请搜索的空闲块数量将会越来越多。Next Fit该方法是大名鼎鼎的Donald Knuth首次提出来的,如果你不知道谁是Donald Knuth,那么数据结构课上折磨的你痛不欲生的字符串匹配KMP算法你一定不会错过,KMP其中的K就是指Donald Knuth,该算法全称Knuth–Morris–Pratt string-searching algorithm,如果你也没听过KMP算法那么你一定听过下面这本书:这就是更加大名鼎鼎的《计算机程序设计艺术》,这本书就是Donald Knuth写的,如果你没有听过这本书请面壁思过一分钟,比尔盖茨曾经说过,如果你看懂了这本书就去给微软投简历吧,这本书也是很多程序员买回来后从来不会翻一眼只是拿来当做镇宅之宝用的。不止比尔盖茨,有一次乔布斯见到Knuth老爷子后。。算了,扯远了,有机会再和大家讲这个故事,拉回来。Next Fit说的是什么呢?这个策略和First Fit很相似,是说我们别总是从头开始找了,而是从上一次找到合适的空闲内存块的位置找起,老爷子观察到上一次找到某个合适的内存块的地方很有可能剩下的内存块能满足接下来的内存分配请求,由于不需要从头开始搜索,因此Next Fit将远快于First Fit。然而也有研究表明Next Fit方法内存使用率不及First Fit,也就是同样的停车场面积,First Fit方法能停更多的车。Best FitFirst Fit和Next Fit都是找到第一个满足要求的内存块就返回,但Best Fit不是这样。Best Fit算法会找到所有的空闲内存块,然后将所有满足要求的并且大小为最小的那个空闲内存块返回,这样的空闲内存块才是最Best的,因此被称为Best Fit。就像下图虽然有三个空闲内存块满足要求,但是Best Fit会选择大小为8字节的空闲内存块。显然,从直觉上我们就能得出Best Fit会比前两种方法能更合理利用内存的结论,各项研究也证实了这一点。然而Best Fit最大的缺点就是分配内存时需要遍历堆上所有的空闲内存块,在速度上显然不及前面两种方法。以上介绍的这三种策略在各种内存分配器中非常常见,当然分配策略远不止这几种,但这些算法不是该主题下关注的重点,因此就不在这里详细阐述了,假设在这里我们选择First Fit算法。没有银弹重要的是,从上面的介绍中我们能够看到,没有一种完美的策略,每一种策略都有其优点和缺点,我们能做到的只有取舍和权衡。因此,要实现一个内存分配器,设计空间其实是非常大的,要想设计出一个通用的内存分配器,就像我们常用的malloc是很不容易的。其实不止内存分配器,在设计其它软件系统时我们也没有银弹。分配内存现在我们找到合适的空闲内存块了,接下来我们又将面临一个新的问题。如果用户需要12字节,而我们的空闲内存块也恰好是12字节,那么很好,直接返回就可以了。但是,如果用户申请12字节内存,而我们找到的空闲内存块大小为32字节,那么我们是要将这32字节的整个空闲内存块标记为已分配吗?就像这样:这样虽然速度最快,但显然会浪费内存,形成内部碎片,也就是说该内存块剩下的空间将无法被利用到。一种显而易见的方法就是将空闲内存块进行划分,前一部分设置为已分配,返回给内存申请者使用,后一部分变为一个新的空闲内存块,只不过大小会更小而已,就像这样:我们需要将空闲内存块大小从32修改为16,其中消息头header占据4字节,剩下的12字节分配出去,并将标记为置为1,表示该内存块已分配。分配出16字节后,还剩下16字节,我们需要拿出4字节作为新的header并将其标记为空闲内存块。释放内存到目前为止,我们的malloc已经能够处理内存分配请求了,还差最后的内存释放。内存释放和我们想象的不太一样,该过程并不比前几个环节简单。首先,释放内存时不需要指定大小,原因很简单,因为我们从要释放的指针地址上移就能知道该内存块的所有信息:其次我们要考虑到的关键一点就在于,与被释放的内存块相邻的内存块可能也是空闲的。如果释放一块内存后我们仅仅简单的将其标志位置为空闲,那么可能会出现下面的场景:从图中我们可以看到,被释放内存的下一个内存块也是空闲的,如果我们仅仅将这16个字节的内存块标记为空闲的话,那么当下一次申请20字节时图中的这两个内存块都不能满足要求,尽管这两个空闲内存块的总数要超过20字节。因此一种更好的方法是当应用程序向我们的malloc释放内存时,我们查看一下相邻的内存块是否是空闲的,如果是空闲的话我们需要合并空闲内存块,就像这样:在这里我们又面临一个新的决策,那就是释放内存时我们要立即去检查能否够合并相邻空闲内存块吗?还是说我们可以推迟一段时间,推迟到下一次分配内存找不到满足要的空闲内存块时再合并相邻空闲内存块。释放内存时立即合并空闲内存块相对简单,但每次释放内存时将引入合并内存块的开销,如果应用程序总是释放12字节然后申请12字节,然后在释放12字节等等这样重复的模式:free(ptr);obj* ptr = malloc(12);free(ptr);obj* ptr = malloc(12);...那么这种内存使用模式对立即合并空闲内存块这种策略非常不友好,我们的内存分配器会有很多的无用功。但这种策略最为简单,在这里我们依然选择使用这种简单的策略。实际上我们需要意识到,实际使用的内存分配器都会有某种推迟合并空闲内存块的策略。高效合并空闲内存块合并空闲内存块的故事到这里就完了吗?问题没有那么简单。让我们来看这样一个场景:使用的内存块其前和其后都是空闲的,在当前的设计中我们可以很容易的知道后一个内存块是空闲的,因为我们只需要从当前位置向下移动16字节就是下一个内存块,但我们怎么能知道上一个内存块是不是空闲的呢?我们之所以能向后跳是因为当前内存块的大小是知道的,那么我们该怎么向前跳找到上一个内存块呢?还是我们上文提到的Donald Knuth,老爷子提出了一个很聪明的设计,我们之所以不能往前跳是因为不知道前一个内存块的信息,那么我们该怎么快速知道前一个内存块的信息呢?Knuth老爷子的设计是这样的,我们不是有一个信息头header吗,那么我们就在该内存块的末尾再加一个信息尾,footer,footer一词用的很形象,header和footer的内容是一样的。因为上一内存块的footer和下一个内存块的header是相邻的,因此我们只需要在当前内存块的位置向上移动4直接就可以等到上一个内存块的信息,这样当我们释放内存时就可以快速的进行相邻空闲内存块的合并了。收工至此,我们的内存分配器就已经设计完毕了。我们的简单内存分配器采用了First Fit分配算法;找到一个满足要求的内存块后会进行切分,剩下的作为新的内存块;同时当释放内存时会立即合并相邻的空闲内存块,同时为加快合并速度,我们引入了Donald Knuth的设计方法,为每个内存块增加footer信息。这样,我们自己实现的内存分配就可以运行起来了,可以真正的申请和释放内存。总结本文从0到1实现了一个简单的内存分配器,但不希望这里的阐述给大家留下内存分配器实现很简单的印象,实际上本文实现的内存分配器还有大量的优化空间,同时我们也没有考虑线程安全问题,但这些都不是本文的目的。本文的目的在于把内存分配器的本质告诉大家,对于想理解内存分配器实现原理的同学来说这些已经足够了,而对于要编写高性能程序的同学来说实现自己的内存池是必不可少的,内存池实现也离不开这里的讨论。
2026年01月08日
2 阅读
0 评论
0 点赞
2026-01-07
怎么搭建学习Linux内核的运行、调试环境?
作者:Tools boy链接:https://www.zhihu.com/question/66594120/answer/355840304来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。一步一步教你搭建linux内核调试环境,因为毕设跟os有关,最近在学习ulk3(深入理解linux内核第三版),感觉光看书不动手实践是很容易遗忘知识,于是开始动手调试书上对应版本的linux2.6内核环境: ubuntu-14.04.1 32位桌面版 内核版本:3.13.0-32-generic 调试的内核为:linux-2.6.26版本调试工具:qemu,gdb,busybox 1.编译x86下的linux2.6内核首先下载linux-2.6.26版本内核Index of /pub/linux/kernel/v2.6/. ubuntu安装qemusudo apt-get install qemutar -zvxf linux-2.6.26.tar.gzsudo apt-get install libncurses5-dev (执行linux+busybox菜单式的配置,需要的库)进入linux2.6-26根目录,执行以下命令make menuconfig,选择 kernel hacking—>[*] compile the kernel with debug info 让其携带调试信息make defconfig 生成配置文件make 编译kernel接着make时,会出现 undefined reference to __mutex_lockslowpath解决方法:需要去kernel/目录找到mutex.c修改,在static跟void之间加上__used,同样__mutex_unlockslowpath也要加上__usedstatic __used void fastcall noinline __sched __mutex_lock_slowpath继续make,会出现以下错误gcc: error: elf_i386: No such file or directorymake[1]: * [arch/x86/vdso/vdso32-int80.so.dbg] Error 1make: * [arch/x86/vdso] Error 2原因在于我的宿主机是gcc 4.8.2,编译器版本过高,不再支持 linker-style 架构.需要修改arch/x86/vdso/Makefile,大约在28,29行 找到 VDSO_LDFLAGS_vdso.lds = -m elf_x86_64 -Wl,-soname=linux-vdso.so.1 \ -Wl,-z,max-page-size=4096 -Wl,-z,common-page-size=4096 把"-m elf_x86_64" 替换为 "-m64"2然后再继续找,大约在72行左右,找到VDSO_LDFLAGS_vdso32.lds = -m elf_i386 -Wl,-soname=linux-gate.so.1中的 "-m elf_i386" 替换为 "-m32"接着继续执行make,就可以继续编译内核,出现以下字样就表示内核编译ok了Root device is (8, 1)Setup is 12160 bytes (padded to 12288 bytes).System is 2653 kBCRC ae462731Kernel: arch/x86/boot/bzImage is ready (#4) Building modules, stage 2. MODPOST 1 modules2.接着使用busybox制作文件镜像建立目标根目录映像dd if=/dev/zero of=myinitrd4M.img bs=4096 count=1024mke2fs myinitrd4M.imgmkdir rootfssudo mount -o loop myinitrd4M.img rootfs准备dev目录sudo mkdir rootfs/devlinux启动过程中会启用 console设备sudo mknod rootfs/dev/console c 5 1另外需要提供一个linux根设备,我们使用 ramsudo mknod rootfs/dev/ram b 1 0接着下载busyboxhttps://www.busybox.net/downloads/busybox-1.20.1.tar.bz2 执行以下命令make defconfigmake menuconfig修改如下配置:busybox settings –> build options –> build busyboxas a static binary( no share libs)执行make在busybox目录下sudo make CONFIG_PREFIX=(path to rootfs)/ installsudo umount rootfs完成根文件系统的制作接着执行qemu-system-i386 -kernel ../linux-2.6.26/arch/x86/boot/bzImage -initrd myinitrd4M.img -append "root=/dev/ram init=/bin/ash"看是否可以顺利的进入到busybox提供的shell环境作者:Tools boy链接:https://www.zhihu.com/question/66594120/answer/355840304来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。3.qemu+gdb调试内核首先执行以下指令(就上条指令后面+ -s -S)qemu-system-i386 -kernel ../linux-2.6.26/arch/x86/boot/bzImage -initrd myinitrd4M.img -append "root=/dev/ram init=/bin/ash" -s -S可以看到在新打开的qemu虚拟机上,整个是一个黑屏,此时qemu在等待gdb的链接接着开启另外一个终端,执行gdb接着在gdb界面中 targe remote之前加载符号表file ../linux-2.6.26/vmlinux target remote: 1234 则可以建立gdb和gdbserver之间的连接.很多教程说接下来直接b startkernel,然后就可以调试内核了,然而我一直没有成功,这个bug卡了我两天,一直无法断点,后来查了英文资料得知(gdbserver inside qemu does not stop on breakpoints),一开始必须使用硬件断点hk startkernel才可以断点接着在start_kernel下硬件断点 hk startkernel(,普通的break断点无法使得gdb在断点处运行,这是一个gdb的bug(),接下来就可以愉快的调试内核了
2026年01月07日
2 阅读
0 评论
0 点赞
1
2
3
...
76