在IA32的操作系统中,段被分为了4个特权级,分别为0-3级,有时候我们也叫做ring0-ring3,其中,数值越小特权级越高。如下图所示:
图中,核心代码和数据所在的段的特权级都比较高,一般在ring0,而用户程序所在的段的特权级较低,一般在ring3。当低特权级的任务试图在未被允许的情况下访问高特权级的段时,将会产生常规保护错误。
而处理器是如何区分所在段的特权级,进而对其进行保护的呢?这就不得不提到CPL、DPL和RPL三者了。但是在开始之前,我们需要先了解一下一致代码段和非一致代码段。
在操作系统中,我们有些高特权级的代码是希望被低特权级的程序所访问的,比如一些库函数,于是我们将这些高特权级代码放在一个叫做一致代码段的段里。而有些高特权级的代码,我们并不想让低特权级的程序所访问,于是我们把他们放在一个叫做非一致代码段的段里。具体来说,当通过call或者jmp指令转移到其它段时(即访问其他段),当转移的目标是一个优先级更高的一致代码段时,我们是可以进行访问的,但是当前的特权级会被延续下去;当转移的目标是一个优先级更高的非一致代码段时,这时的访问会引起常规保护错误(除非使用调用门或任务门)。
总结来说:
一致代码段:由系统(高特权级)共享给低特权级的程序的代码所在的段,主要有下面两点限制:
高特权级程序不能访问低特权级的数据
低特权级的程序可以访问高特权级的代码,但是特权级不会改变,还是保持低特权级程序的特权级
非一致代码段:为了避免被低特权级程序所访问而保护起来的代码段,主要有一点限制:
只允许同级之间访问
另外,数据段都是非一致的
所遵循的规则如下图所示:
特权级是保护模式下一个重要的概念,CPL,RPL和DPL是其中的核心概念,查阅资料无数,总结如下:
简单解释:
--------------------------------------------------------------------------------
CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。
当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}
下面打一个比方,中国官员分为6级国家主席1、总理2、省长3、市长4、县长5、乡长6,假设我是当前进程,级别总理(CPL=2),我去聊城市(DPL=4)考察(呵呵),我用省长的级别(RPL=3 这样也能吓死他们:-))去访问,可以吧,如果我用县长的级别,人家就不理咱了(你看看电视上的微服私访,呵呵),明白了吧!为什么采用RPL,是考虑到安全的问题,就好像你明明对一个文件用有写权限,为什么用只读打开它呢,还不是为了安全!
全面解释:
--------------------------------------------------------------------------------
RPL是段选择子里面的bit 0和bit 1位组合所得的值,但这里要首先搞清楚什么是段选择子,根据Intel 的文件(IA-32 IntelR Architecture Software Developer's Manual, Volume 3System Programming Guide)它是一个16Bit identifier (原文:A segment selector is a 16-bit identifier for a segment). 但 identifier 又是什么. identifier 可以是一个变数的名字( An identifier is a name for variables), 简单的说它可以就是一般意义的变数. 这里 16-bit identifier for a segment 可以就是一个一般意义的16bit变数但同时要求对它的值解释的时候必须跟据Intel定下的规则---也就是bit 0和bit 1位的组合值就是RPL等等… 因此在程序里如果有需要的话你可以声明一个或者多个变数来代表这些段选择子,这样的话你的程序在某一时刻就可以有很多段选择子,当然有那么多段选择子就有那么多RPL.可以这样说程序有多少个是RPL是你怎样看待你自己声明的变数. |
程序的CPL(CS.RPL)是CS register 里bit 0和bit 1 位组合所得的值.在某一时刻就只有这个值唯一的代表程序的CPL.
而DPL是段描述符中的特权级, 它的本意是用来代表它所描述的段的特权级. 一个程序可以使用很多段(Data,Code,Stack)也可以只用一个code段等.在正常的情况下当程序的环境建立好后,段描述符都不需要改变-----当然DPL也不需要改变.
一、对数据段和堆栈段访问时的特权级控制:
要求访问数据段或堆栈段的程序的CPL≤待访问的数据段或堆栈段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL,即程序访问数据段或堆栈段要遵循一个准则:只有相同或更高特权级的代码才能访问相应的数据段。这里,RPL可能会削弱CPL的作用,访问数据段或堆栈段时,默认用CPU和RPL中的最小特权级去访问数据段,所以max {CPL, RPL} ≤ DPL,否则访问失败。
二、对代码段访问的特权级控制(代码执行权的特权转移):
让我们先来记一些“定律”:
所有的程序转跳,CPU都不会把段选择子的RPL赋给转跳后程序的CS.RPL. .
转跳后程序的CPL(CS.RPL)只会有下面的俩种可能
转跳后程序的CPL(CS.RPL) = 转跳前程序的CPL(CS.RPL)
或
转跳后程序的CPL(CS.RPL) = 转跳后程序的CodeDescriptor.DPL
以 Call 为例(只能跳到等于当前特权级或比当前特权级更高的段):
怎样决定这两种选择,这就要首先知道转跳后程序的段是一致代码段还是非一致代码段.其实也很简单,规则如下:
如果能成功转跳到一致代码段, 转跳后程序的CPL(CS.RPL) = 转跳前程序的CPL(CS.RPL),(转跳后程序的CPL继承了转跳前程序的CPL)
如果能成功转跳到非一致代码段, 转跳后程序的CPL(CS.RPL) =转跳后程序的Descriptor.DPL。(转跳后程序的CPL变成了该代码段的特权级.我在前面提到DPL是段描述符中的特权级, 它的本意是用来代表它所描述的段的特权级)怎样才能成功转跳啦?
这里有四个重要的概念:
1).段的保护观念是高特权级不找低特权级办事,低特权级找高特权级帮忙,相同的一定没问题.(这样想逻辑是没错,事实对不对就不知道.)也就是县长不找乡长,乡长不求农民,反过来农民求乡长,乡长找县长.这个概念是最重要的。
2) 一致代码段的意义: 让客人很方便的利用主人(一致代码段)的东西为自己办事.但客人这身份没有改变NewCS.RPL=OldCS.RPL所以只能帮自己办事。比方说乡长有一头牛,农民可以借来帮自己种田,但不能种别人的田.但是如果你是乡长当然可以种乡里所有的田。
3) 非一致代码段的意义:主人(非一致代码段)可以帮客人但一定是用自己的身份NewCS.RPL= DestinationDescriptorCode.DPL这里可能有安全的问题, 搞不好很容易农民变县长。主人太顽固了一定要坚持自己的身份,有什么方法变通一下,来个妥协好不好。好的,它就是RPL的用处。
4) RPL: 它让程序有需要的时候可以表示一个特权级更低的身份Max(RPL,CPL)而不会失去本身的特权级CPL(CS.RPL),有需要的时候是指要检查身份的时候。事实上RPL跟段本身的特权级DPL和当前特权级CPL没有什么关系,因为RPL的值在成功转跳后并不赋给转跳后的CS.RPL。
还是要问怎样才能成功转跳啦?这里分两种情况:
普通转跳(没有经过Gate 这东西):即JMP或Call后跟着48位全指针(16位段选择子+32位地址偏移),且其中的段选择子指向代码段描述符,这样的跳转称为直接(普通)跳转。普通跳转不能使特权级发生跃迁,即不会引起CPL的变化,看下面的详细描述:
目标是一致代码段:
要求:CPL(CS.RPL)>=DestinationDescriptorCode.DPL ,其他RPL是不检查的。
转跳后程序的CPL(NewCS.RPL) = 转跳前程序的CPL( OldCS.RPL)
上面的安排就是概念1,2的意思,此时,CPL没有发生变化,纵使它执行了特权级(DPL)较高的代码。若访问时不满足要求,则发生异常。
目标是非一致代码段:
要求:CPL(CS.RPL)=DestinationDescriptorCode.DPL AND RPL≤CPL(CS.RPL)
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
上面的安排就是概念3的意思和部分1的意思----主人(一致代码段)只帮相同特权级的帮客人做事。因为前提是CPL=DPL,所以转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL不会改变CPL的值,特权级(CPL)也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。
通过调用门的跳转:当段间转移指令JMP和段间转移指令CALL后跟着的目标段选择子指向一个调用门描述符时,该跳转就是利用调用门的跳转。这时如果选择子后跟着32位的地址偏移,也不会被cpu使用,因为调用门描述符已经记录了目标代码的偏移。使用调门进行的跳转比普通跳转多一个步骤,即在访问调用门描述符时要将描述符当作一个数据段来检查访问权限,要求指示调用门的选择子的RPL≤门描述符DPL,同时当前代码段CPL≤门描述符DPL,就如同访问数据段一样,要求访问数据段的程序的CPL≤待访问的数据段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL。只有满足了以上条件,CPU才会进一步从调用门描述符中读取目标代码段的选择子和地址偏移,进行下一步的操作。
从调用门中读取到目标代码的段选择子和地址偏移后,我们当前掌握的信息又回到了先前,和普通跳转站在了同一条起跑线上(普通跳转一开始就得到了目标代码的段选择子和地址偏移),有所不同的是,此时,CPU会将读到的目标代码段选择子中的RPL清0,即忽略了调用门中代码段选择子的RPL的作用。完成这一步后,CPU开始对当前程序的CPL,目标代码段选择子的RPL(事实上它被清0后总能满足要求)以及由目标代码选择子指示的目标代码段描述符中的DPL进行特权级检查,并根据情况进行跳转,具体情况如下:
目标是一致代码段:
要求:CPL(CS.RPL)≥DestinationDescriptorCode.DPL ,RPL不检查,因为RPL被清0,所以事实上永远满足RPL≤DPL,这一点与普通跳转一致,适用于JMP和CALL。
转跳后程序的CPL(NewCS.RPL) = 转跳前程序的CPL( OldCS.RPL),因此特权级没有发生跃迁。
目标是非一致代码段:
当用JMP指令跳转时:
要求:CPL(CS.RPL)=DestinationDescriptorCode.DPL AND RPL<= CPL(CS.RPL)(事实上因为RPL被清0,所以RPL≤CPL总能满足,因此RPL与CPL的关系在此不检查)。若不满足要求则程序引起异常。
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
因为前提是CPL=DPL,所以转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL不会改变CPL的值,特权级也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。
当用CALL指令跳转时:
要求:CPL(CS.RPL)≥DestinationDescriptorCode.DPL(RPL被清0,不检查),若不满足要求则程序引起异常。
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
当条件CPL=DPL时,程序跳转后CPL=DPL,特权级不发生跃迁;当CPL>DPL时,程序跳转后CPL=DPL,特权级发生跃迁,这是我们当目前位置唯一见到的使程序当前执行忧先级(CPL)发生变化的跳转方法,即用CALL指令+调用门方式跳转,且目标代码段是非一致代码段。
总结:以上介绍了两种情况的跳转,分别是普通跳转和使用调用门的跳转,其中又可细分为JMP跳转和CALL跳转,跳转成功已否是由CPL,RPL和DPL综合决定的。所有跳转都是从低特权级代码向同级或更高特权级(DPL)跳转,但保持当前执行特权级(CPL)不变,这里有点难于区别为什么说向高特权级跳转,又说特权级没变,这里“高特权级”是指目标代码段描述符的DPL,它规定了可以跳转到该段代码的最高特权级;而后面的CPL不变才真正说明了特权级未发生跃迁。我们可以看到,只有用CALL指令+调用门方式跳转,且目标代码段是非一致代码段时,才会引起CPL的变化,即引起代码执行特权级的跃迁,这是目前得知的改变执行特权级的唯一办法,
一、引入
保护模式下的段寄存器 由 16位的选择器 与 64位的段描述符寄存器 构成
段描述符寄存器: 存储段描述符
选择器:存储段描述符的索引
PS:原先实模式下的各个段寄存器作为保护模式下的段选择器,80486中有6个(即CS,SS,DS,ES,FS,GS)80位的段寄存器。由选择器CS对应表示的段仍为代码段,选择器SS对应表示的段仍为堆栈段。
二、详解
先说明一下概念
(1)全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。
基地址指定GDT表中字节0在线性地址空间中的地址,表长度指明GDT表的字节长度值。指令LGDT和SGDT分别用于加载和保存GDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。在保护模式初始化过程中必须给GDTR加载一个新值。
(2)段选择子(Selector)由GDTR访问全局描述符表是通过“段选择子”(实模式下的段寄存器)来完成的。段选择子是一个16位的寄存器(同实模式下的段寄存器相同)
段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。他的index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符。然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址,段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。
关于特权级的说明:任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。
例如给出逻辑地址:21h:12345678h转换为线性地址
a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1
b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h
(3)局部描述符表LDT(Local Descriptor Table)局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图
LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。LDTR记录局部描述符表的起始位置,与GDTR不同,LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。LDTR可以在程序中随时改变,通过使用lldt指令。如上图,如果装载的是Selector 2则LDTR指向的是表LDT2。举个例子:如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h。
1. 首先需要装载LDTR使它指向LDT2 使用指令lldt将Select2装载到LDTR
2. 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择,此时的SEL值为1Ch(二进制为11 1 00b)。OFFSET=12345678h。逻辑地址为1C:12345678h
3. 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h
4. 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector 1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)
由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。
当进行任务切换时,处理器会把新任务LDT的段选择符和段描述符自动地加载进LDTR中。在机器加电或处理器复位后,段选择符和基地址被默认地设置为0,而段长度被设置成0xFFFF。
三、实例(对理解非常有用)
1:访问GDT
当TI=0时表示段描述符在GDT中,如上图所示:
①先从GDTR寄存器中获得GDT基址。
②然后再GDT中以段选择器高13位位置索引值得到段描述符。
③段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
2:访问LDT
当TI=1时表示段描述符在LDT中,如上图所示:
①还是先从GDTR寄存器中获得GDT基址。
②从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。
③以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。
④用段选择器高13位位置索引值从LDT段中得到段描述符。
⑤段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
扩展
除了GDTR、LDTR外还有IDTR和TR
(1)中断描述符表寄存器IDTR
与GDTR的作用类似,IDTR寄存器用于存放中断描述符表IDT的32位线性基地址和16位表长度值。指令LIDT和SIDT分别用于加载和保存IDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。
(2)任务寄存器TR
TR用于寻址一个特殊的任务状态段(Task State Segment,TSS)。TSS中包含着当前执行任务的重要信息。
TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。
Makefile 主要的 5个部分 (显示规则, 隐晦规则, 变量定义, 文件指示, 注释)
Makefile基本格式如下:
target ... : prerequisites ...
command
...
...
其中,
target - 目标文件, 可以是 Object File, 也可以是可执行文件
prerequisites - 生成 target 所需要的文件或者目标
command - make需要执行的命令 (任意的shell命令), Makefile中的命令必须以 [tab] 开头
显示规则 :: 说明如何生成一个或多个目标文件(包括 生成的文件, 文件的依赖文件, 生成的命令)
隐晦规则 :: make的自动推导功能所执行的规则
变量定义 :: Makefile中定义的变量
文件指示 :: Makefile中引用其他Makefile; 指定Makefile中有效部分; 定义一个多行命令
注释 :: Makefile只有行注释 "#", 如果要使用或者输出"#"字符, 需要进行转义, "\#"
1.2 GNU make 的工作方式
读入主Makefile (主Makefile中可以引用其他Makefile)
读入被include的其他Makefile
初始化文件中的变量
推导隐晦规则, 并分析所有规则
为所有的目标文件创建依赖关系链
根据依赖关系, 决定哪些目标要重新生成
执行生成命令
2. Makefile 初级语法
2.1 Makefile 规则
2.1.1 规则语法
规则主要有2部分: 依赖关系 和 生成目标的方法.
语法有以下2种:
target ... : prerequisites ...
command
...
或者
target ... : prerequisites ; command
command
...
*注* command太长, 可以用 "\" 作为换行符
2.1.2 规则中的通配符
* :: 表示任意一个或多个字符
? :: 表示任意一个字符
[...] :: ex. [abcd] 表示a,b,c,d中任意一个字符, [^abcd]表示除a,b,c,d以外的字符, [0-9]表示 0~9中任意一个数字
~ :: 表示用户的home目录
2.1.3 路径搜索
当一个Makefile中涉及到大量源文件时(这些源文件和Makefile极有可能不在同一个目录中),
这时, 最好将源文件的路径明确在Makefile中, 便于编译时查找. Makefile中有个特殊的变量 VPATH 就是完成这个功能的.
指定了 VPATH 之后, 如果当前目录中没有找到相应文件或依赖的文件, Makefile 回到 VPATH 指定的路径中再去查找..
VPATH 使用方法:
vpath <directories> :: 当前目录中找不到文件时, 就从<directories>中搜索
vpath <pattern> <directories> :: 符合<pattern>格式的文件, 就从<directories>中搜索
vpath <pattern> :: 清除符合<pattern>格式的文件搜索路径
vpath :: 清除所有已经设置好的文件路径
# 示例1 - 当前目录中找不到文件时, 按顺序从 src目录 ../parent-dir目录中查找文件
VPATH src:../parent-dir
# 示例2 - .h结尾的文件都从 ./header 目录中查找
VPATH %.h ./header
# 示例3 - 清除示例2中设置的规则
VPATH %.h
# 示例4 - 清除所有VPATH的设置
VPATH
2.2 Makefile 中的变量
2.2.1 变量定义 ( = or := )
OBJS = programA.o programB.o
OBJS-ADD = $(OBJS) programC.o
# 或者
OBJS := programA.o programB.o
OBJS-ADD := $(OBJS) programC.o
其中 = 和 := 的区别在于, := 只能使用前面定义好的变量, = 可以使用后面定义的变量
测试 =
# Makefile内容
OBJS2 = $(OBJS1) programC.o
OBJS1 = programA.o programB.o
all:
@echo $(OBJS2)
# bash中执行 make, 可以看出虽然 OBJS1 是在 OBJS2 之后定义的, 但在 OBJS2中可以提前使用
$ make
programA.o programB.o programC.o
测试 :=
# Makefile内容
OBJS2 := $(OBJS1) programC.o
OBJS1 := programA.o programB.o
all:
@echo $(OBJS2)
# bash中执行 make, 可以看出 OBJS2 中的 $(OBJS1) 为空
$ make
programC.o
2.2.2 变量替换
# Makefile内容
SRCS := programA.c programB.c programC.c
OBJS := $(SRCS:%.c=%.o)
all:
@echo "SRCS: " $(SRCS)
@echo "OBJS: " $(OBJS)
# bash中运行make
$ make
SRCS: programA.c programB.c programC.c
OBJS: programA.o programB.o programC.o
2.2.3 变量追加值 +=
# Makefile内容
SRCS := programA.c programB.c programC.c
SRCS += programD.c
all:
@echo "SRCS: " $(SRCS)
# bash中运行make
$ make
SRCS: programA.c programB.c programC.c programD.c
2.2.4 变量覆盖 override
作用是使 Makefile中定义的变量能够覆盖 make 命令参数中指定的变量
语法:
override <variable> = <value>
override <variable> := <value>
override <variable> += <value>
下面通过一个例子体会 override 的作用:
# Makefile内容 (没有用override)
SRCS := programA.c programB.c programC.c
all:
@echo "SRCS: " $(SRCS)
# bash中运行make
$ make SRCS=nothing
SRCS: nothing
#################################################
# Makefile内容 (用override)
override SRCS := programA.c programB.c programC.c
all:
@echo "SRCS: " $(SRCS)
# bash中运行make
$ make SRCS=nothing
SRCS: programA.c programB.c programC.c
2.2.5 目标变量
作用是使变量的作用域仅限于这个目标(target), 而不像之前例子中定义的变量, 对整个Makefile都有效.
语法:
<target ...> :: <variable-assignment>
<target ...> :: override <variable-assignment> (override作用参见 变量覆盖的介绍)
示例:
# Makefile 内容
SRCS := programA.c programB.c programC.c
target1: TARGET1-SRCS := programD.c
target1:
@echo "SRCS: " $(SRCS)
@echo "SRCS: " $(TARGET1-SRCS)
target2:
@echo "SRCS: " $(SRCS)
@echo "SRCS: " $(TARGET1-SRCS)
# bash中执行make
$ make target1
SRCS: programA.c programB.c programC.c
SRCS: programD.c
$ make target2 <-- target2中显示不了 $(TARGET1-SRCS)
SRCS: programA.c programB.c programC.c
SRCS:
2.3 Makefile 命令前缀
Makefile 中书写shell命令时可以加2种前缀 @ 和 -, 或者不用前缀.
3种格式的shell命令区别如下:
不用前缀 :: 输出执行的命令以及命令执行的结果, 出错的话停止执行
前缀 @ :: 只输出命令执行的结果, 出错的话停止执行
前缀 - :: 命令执行有错的话, 忽略错误, 继续执行
示例:
# Makefile 内容 (不用前缀)
all:
echo "没有前缀"
cat this_file_not_exist
echo "错误之后的命令" <-- 这条命令不会被执行
# bash中执行 make
$ make
echo "没有前缀" <-- 命令本身显示出来
没有前缀 <-- 命令执行结果显示出来
cat this_file_not_exist
cat: this_file_not_exist: No such file or directory
make: *** [all] Error 1
###########################################################
# Makefile 内容 (前缀 @)
all:
@echo "没有前缀"
@cat this_file_not_exist
@echo "错误之后的命令" <-- 这条命令不会被执行
# bash中执行 make
$ make
没有前缀 <-- 只有命令执行的结果, 不显示命令本身
cat: this_file_not_exist: No such file or directory
make: *** [all] Error 1
###########################################################
# Makefile 内容 (前缀 -)
all:
-echo "没有前缀"
-cat this_file_not_exist
-echo "错误之后的命令" <-- 这条命令会被执行
# bash中执行 make
$ make
echo "没有前缀" <-- 命令本身显示出来
没有前缀 <-- 命令执行结果显示出来
cat this_file_not_exist
cat: this_file_not_exist: No such file or directory
make: [all] Error 1 (ignored)
echo "错误之后的命令" <-- 出错之后的命令也会显示
错误之后的命令 <-- 出错之后的命令也会执行
2.4 伪目标
伪目标并不是一个"目标(target)", 不像真正的目标那样会生成一个目标文件.
典型的伪目标是 Makefile 中用来清理编译过程中中间文件的 clean 伪目标, 一般格式如下:
.PHONY: clean <-- 这句没有也行, 但是最好加上
clean:
-rm -f *.o
2.5 引用其他的 Makefile
语法: include <filename> (filename 可以包含通配符和路径)
示例:
# Makefile 内容
all:
@echo "主 Makefile begin"
@make other-all
@echo "主 Makefile end"
include ./other/Makefile
# ./other/Makefile 内容
other-all:
@echo "other makefile begin"
@echo "other makefile end"
# bash中执行 make
$ ll
total 20K
-rw-r--r-- 1 wangyubin wangyubin 125 Sep 23 16:13 Makefile
-rw-r--r-- 1 wangyubin wangyubin 11K Sep 23 16:15 makefile.org <-- 这个文件不用管
drwxr-xr-x 2 wangyubin wangyubin 4.0K Sep 23 16:11 other
$ ll other/
total 4.0K
-rw-r--r-- 1 wangyubin wangyubin 71 Sep 23 16:11 Makefile
$ make
主 Makefile begin
make[1]: Entering directory `/path/to/test/makefile'
other makefile begin
other makefile end
make[1]: Leaving directory `/path/to/test/makefile'
主 Makefile end
2.6 查看C文件的依赖关系
写 Makefile 的时候, 需要确定每个目标的依赖关系.
GNU提供一个机制可以查看C代码文件依赖那些文件, 这样我们在写 Makefile 目标的时候就不用打开C源码来看其依赖那些文件了.
比如, 下面命令显示内核源码中 virt/kvm/kvm_main.c 中的依赖关系
$ cd virt/kvm/
$ gcc -MM kvm_main.c
kvm_main.o: kvm_main.c iodev.h coalesced_mmio.h async_pf.h <--这句就可以加到Makefile中作为编译kvm_main.o的依赖关系
2.7 make 退出码
Makefile的退出码有以下3种:
0 :: 表示成功执行
1 :: 表示make命令出现了错误
2 :: 使用了 "-q" 选项, 并且make使得一些目标不需要更新
2.8 指定 Makefile, 指定特定目标
默认执行 make 命令时, GNU make在当前目录下依次搜索下面3个文件 "GNUmakefile", "makefile", "Makefile",
找到对应文件之后, 就开始执行此文件中的第一个目标(target). 如果找不到这3个文件就报错.
非默认情况下, 可以在 make 命令中指定特定的 Makefile 和特定的 目标.
示例:
# Makefile文件名改为 MyMake, 内容
target1:
@echo "target [1] begin"
@echo "target [1] end"
target2:
@echo "target [2] begin"
@echo "target [2] end"
# bash 中执行 make
$ ls
Makefile
$ mv Makefile MyMake
$ ls
MyMake
$ make <-- 找不到默认的 Makefile
make: *** No targets specified and no makefile found. Stop.
$ make -f MyMake <-- 指定特定的Makefile
target [1] begin
target [1] end
$ make -f MyMake target2 <-- 指定特定的目标(target)
target [2] begin
target [2] end
2.9 make 参数介绍
make 的参数有很多, 可以通过 make -h 去查看, 下面只介绍几个我认为比较有用的.
参数
含义
--debug[=<options>] 输出make的调试信息, options 可以是 a, b, v
-j --jobs 同时运行的命令的个数, 也就是多线程执行 Makefile
-r --no-builtin-rules 禁止使用任何隐含规则
-R --no-builtin-variabes 禁止使用任何作用于变量上的隐含规则
-B --always-make 假设所有目标都有更新, 即强制重编译
2.10 Makefile 隐含规则
这里只列一个和编译C相关的.
编译C时,<n>.o 的目标会自动推导为 <n>.c
# Makefile 中
main : main.o
gcc -o main main.o
#会自动变为:
main : main.o
gcc -o main main.o
main.o: main.c <-- main.o 这个目标是隐含生成的
gcc -c main.c
2.11 隐含规则中的 命令变量 和 命令参数变量
2.11.1 命令变量, 书写Makefile可以直接写 shell时用这些变量.
下面只列出一些C相关的
变量名
含义
RM rm -f
AR ar
CC cc
CXX g++
示例:
# Makefile 内容
all:
@echo $(RM)
@echo $(AR)
@echo $(CC)
@echo $(CXX)
# bash 中执行make, 显示各个变量的值
$ make
rm -f
ar
cc
g++
2.11.2 命令参数变量
变量名
含义
ARFLAGS AR命令的参数
CFLAGS C语言编译器的参数
CXXFLAGS C++语言编译器的参数
示例: 下面以 CFLAGS 为例演示
# test.c 内容
#include <stdio.h>
int main(int argc, char *argv[])
{
printf ("Hello Makefile\n");
return 0;
}
# Makefile 内容
test: test.o
$(CC) -o test test.o
# bash 中用 make 来测试
$ ll
total 24K
-rw-r--r-- 1 wangyubin wangyubin 69 Sep 23 17:31 Makefile
-rw-r--r-- 1 wangyubin wangyubin 14K Sep 23 19:51 makefile.org <-- 请忽略这个文件
-rw-r--r-- 1 wangyubin wangyubin 392 Sep 23 17:31 test.c
$ make
cc -c -o test.o test.c
cc -o test test.o <-- 这个是自动推导的
$ rm -f test test.o
$ make CFLAGS=-Wall <-- 命令中加的编译器参数自动追加入下面的编译中了
cc -Wall -c -o test.o test.c
cc -o test test.o
2.12 自动变量
Makefile 中很多时候通过自动变量来简化书写, 各个自动变量的含义如下:
自动变量
含义
$@ 目标集合
$% 当目标是函数库文件时, 表示其中的目标文件名
$< 第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标
$? 比目标新的依赖目标的集合
$^ 所有依赖目标的集合, 会去除重复的依赖目标
$+ 所有依赖目标的集合, 不会去除重复的依赖目标
$* 这个是GNU make特有的, 其它的make不一定支持
3. Makefile 高级语法
3.1 嵌套Makefile
在 Makefile 初级语法中已经提到过引用其它 Makefile的方法. 这里有另一种写法, 并且可以向引用的其它 Makefile 传递参数.
示例: (不传递参数, 只是调用子文件夹 other 中的Makefile)
# Makefile 内容
all:
@echo "主 Makefile begin"
@cd ./other && make
@echo "主 Makefile end"
# ./other/Makefile 内容
other-all:
@echo "other makefile begin"
@echo "other makefile end"
# bash中执行 make
$ ll
total 28K
-rw-r--r-- 1 wangyubin wangyubin 104 Sep 23 20:43 Makefile
-rw-r--r-- 1 wangyubin wangyubin 17K Sep 23 20:44 makefile.org <-- 这个文件不用管
drwxr-xr-x 2 wangyubin wangyubin 4.0K Sep 23 20:42 other
$ ll other/
total 4.0K
-rw-r--r-- 1 wangyubin wangyubin 71 Sep 23 16:11 Makefile
$ make
主 Makefile begin
make[1]: Entering directory `/path/to/test/makefile/other'
other makefile begin
other makefile end
make[1]: Leaving directory `/path/to/test/makefile/other'
主 Makefile end
示例: (用export传递参数)
# Makefile 内容
export VALUE1 := export.c <-- 用了 export, 此变量能够传递到 ./other/Makefile 中
VALUE2 := no-export.c <-- 此变量不能传递到 ./other/Makefile 中
all:
@echo "主 Makefile begin"
@cd ./other && make
@echo "主 Makefile end"
# ./other/Makefile 内容
other-all:
@echo "other makefile begin"
@echo "VALUE1: " $(VALUE1)
@echo "VALUE2: " $(VALUE2)
@echo "other makefile end"
# bash中执行 make
$ make
主 Makefile begin
make[1]: Entering directory `/path/to/test/makefile/other'
other makefile begin
VALUE1: export.c <-- VALUE1 传递成功
VALUE2: <-- VALUE2 传递失败
other makefile end
make[1]: Leaving directory `/path/to/test/makefile/other'
主 Makefile end
补充* export 语法格式如下:
export variable = value
export variable := value
export variable += value
3.2 定义命令包
命令包有点像是个函数, 将连续的相同的命令合成一条, 减少 Makefile 中的代码量, 便于以后维护.
语法:
define <command-name>
command
...
endef
示例:
# Makefile 内容
define run-hello-makefile
@echo -n "Hello"
@echo " Makefile!"
@echo "这里可以执行多条 Shell 命令!"
endef
all:
$(run-hello-makefile)
# bash 中运行make
$ make
Hello Makefile!
这里可以执行多条 Shell 命令!
3.3 条件判断
条件判断的关键字主要有 ifeq ifneq ifdef ifndef
语法:
<conditional-directive>
<text-if-true>
endif
# 或者
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
示例: ifeq的例子, ifneq和ifeq的使用方法类似, 就是取反
# Makefile 内容
all:
ifeq ("aa", "bb")
@echo "equal"
else
@echo "not equal"
endif
# bash 中执行 make
$ make
not equal
示例: ifdef的例子, ifndef和ifdef的使用方法类似, 就是取反
# Makefile 内容
SRCS := program.c
all:
ifdef SRCS
@echo $(SRCS)
else
@echo "no SRCS"
endif
# bash 中执行 make
$ make
program.c
3.4 Makefile 中的函数
Makefile 中自带了一些函数, 利用这些函数可以简化 Makefile 的编写.
函数调用语法如下:
$(<function> <arguments>)
# 或者
${<function> <arguments>}
<function> 是函数名
<arguments> 是函数参数
3.4.1 字符串函数
字符串替换函数: $(subst <from>,<to>,<text>)
功能: 把字符串<text> 中的 <from> 替换为 <to>
返回: 替换过的字符串
# Makefile 内容
all:
@echo $(subst t,e,maktfilt) <-- 将t替换为e
# bash 中执行 make
$ make
makefile
模式字符串替换函数: $(patsubst <pattern>,<replacement>,<text>)
功能: 查找<text>中的单词(单词以"空格", "tab", "换行"来分割) 是否符合 <pattern>, 符合的话, 用 <replacement> 替代.
返回: 替换过的字符串
# Makefile 内容
all:
@echo $(patsubst %.c,%.o,programA.c programB.c)
# bash 中执行 make
$ make
programA.o programB.o
去空格函数: $(strip <string>)
功能: 去掉 <string> 字符串中开头和结尾的空字符
返回: 被去掉空格的字符串值
# Makefile 内容
VAL := " aa bb cc "
all:
@echo "去除空格前: " $(VAL)
@echo "去除空格后: " $(strip $(VAL))
# bash 中执行 make
$ make
去除空格前: aa bb cc
去除空格后: aa bb cc
查找字符串函数: $(findstring <find>,<in>)
功能: 在字符串 <in> 中查找 <find> 字符串
返回: 如果找到, 返回 <find> 字符串, 否则返回空字符串
# Makefile 内容
VAL := " aa bb cc "
all:
@echo $(findstring aa,$(VAL))
@echo $(findstring ab,$(VAL))
# bash 中执行 make
$ make
aa
过滤函数: $(filter <pattern...>,<text>)
功能: 以 <pattern> 模式过滤字符串 <text>, *保留* 符合模式 <pattern> 的单词, 可以有多个模式
返回: 符合模式 <pattern> 的字符串
# Makefile 内容
all:
@echo $(filter %.o %.a,program.c program.o program.a)
# bash 中执行 make
$ make
program.o program.a
反过滤函数: $(filter-out <pattern...>,<text>)
功能: 以 <pattern> 模式过滤字符串 <text>, *去除* 符合模式 <pattern> 的单词, 可以有多个模式
返回: 不符合模式 <pattern> 的字符串
# Makefile 内容
all:
@echo $(filter-out %.o %.a,program.c program.o program.a)
# bash 中执行 make
$ make
program.c
排序函数: $(sort <list>)
功能: 给字符串 <list> 中的单词排序 (升序)
返回: 排序后的字符串
# Makefile 内容
all:
@echo $(sort bac abc acb cab)
# bash 中执行 make
$ make
abc acb bac cab
取单词函数: $(word <n>,<text>)
功能: 取字符串 <text> 中的 第<n>个单词 (n从1开始)
返回: <text> 中的第<n>个单词, 如果<n> 比 <text> 中单词个数要大, 则返回空字符串
# Makefile 内容
all:
@echo $(word 1,aa bb cc dd)
@echo $(word 5,aa bb cc dd)
@echo $(word 4,aa bb cc dd)
# bash 中执行 make
$ make
aa
dd
取单词串函数: $(wordlist <s>,<e>,<text>)
功能: 从字符串<text>中取从<s>开始到<e>的单词串. <s>和<e>是一个数字.
返回: 从<s>到<e>的字符串
# Makefile 内容
all:
@echo $(wordlist 1,3,aa bb cc dd)
@echo $(word 5,6,aa bb cc dd)
@echo $(word 2,5,aa bb cc dd)
# bash 中执行 make
$ make
aa bb cc
bb
单词个数统计函数: $(words <text>)
功能: 统计字符串 <text> 中单词的个数
返回: 单词个数
# Makefile 内容
all:
@echo $(words aa bb cc dd)
@echo $(words aabbccdd)
@echo $(words )
# bash 中执行 make
$ make
1
首单词函数: $(firstword <text>)
功能: 取字符串 <text> 中的第一个单词
返回: 字符串 <text> 中的第一个单词
# Makefile 内容
all:
@echo $(firstword aa bb cc dd)
@echo $(firstword aabbccdd)
@echo $(firstword )
# bash 中执行 make
$ make
aa
aabbccdd
3.4.2 文件名函数
取目录函数: $(dir <names...>)
功能: 从文件名序列 <names> 中取出目录部分
返回: 文件名序列 <names> 中的目录部分
# Makefile 内容
all:
@echo $(dir /home/a.c ./bb.c ../c.c d.c)
# bash 中执行 make
$ make
/home/ ./ ../ ./
取文件函数: $(notdir <names...>)
功能: 从文件名序列 <names> 中取出非目录部分
返回: 文件名序列 <names> 中的非目录部分
# Makefile 内容
all:
@echo $(notdir /home/a.c ./bb.c ../c.c d.c)
# bash 中执行 make
$ make
a.c bb.c c.c d.c
取后缀函数: $(suffix <names...>)
功能: 从文件名序列 <names> 中取出各个文件名的后缀
返回: 文件名序列 <names> 中各个文件名的后缀, 没有后缀则返回空字符串
# Makefile 内容
all:
@echo $(suffix /home/a.c ./b.o ../c.a d)
# bash 中执行 make
$ make
.c .o .a
取前缀函数: $(basename <names...>)
功能: 从文件名序列 <names> 中取出各个文件名的前缀
返回: 文件名序列 <names> 中各个文件名的前缀, 没有前缀则返回空字符串
# Makefile 内容
all:
@echo $(basename /home/a.c ./b.o ../c.a /home/.d .e)
# bash 中执行 make
$ make
/home/a ./b ../c /home/
加后缀函数: $(addsuffix <suffix>,<names...>)
功能: 把后缀 <suffix> 加到 <names> 中的每个单词后面
返回: 加过后缀的文件名序列
# Makefile 内容
all:
@echo $(addsuffix .c,/home/a b ./c.o ../d.c)
# bash 中执行 make
$ make
/home/a.c b.c ./c.o.c ../d.c.c
加前缀函数: $(addprefix <prefix>,<names...>)
功能: 把前缀 <prefix> 加到 <names> 中的每个单词前面
返回: 加过前缀的文件名序列
# Makefile 内容
all:
@echo $(addprefix test_,/home/a.c b.c ./d.c)
# bash 中执行 make
$ make
test_/home/a.c test_b.c test_./d.c
连接函数: $(join <list1>,<list2>)
功能: <list2> 中对应的单词加到 <list1> 后面
返回: 连接后的字符串
# Makefile 内容
all:
@echo $(join a b c d,1 2 3 4)
@echo $(join a b c d,1 2 3 4 5)
@echo $(join a b c d e,1 2 3 4)
# bash 中执行 make
$ make
a1 b2 c3 d4
a1 b2 c3 d4 5
a1 b2 c3 d4 e
3.4.3 foreach
语法:
$(foreach <var>,<list>,<text>)
示例:
# Makefile 内容
targets := a b c d
objects := $(foreach i,$(targets),$(i).o)
all:
@echo $(targets)
@echo $(objects)
# bash 中执行 make
$ make
a b c d
a.o b.o c.o d.o
3.4.4 if
这里的if是个函数, 和前面的条件判断不一样, 前面的条件判断属于Makefile的关键字
语法:
$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)
示例:
# Makefile 内容
val := a
objects := $(if $(val),$(val).o,nothing)
no-objects := $(if $(no-val),$(val).o,nothing)
all:
@echo $(objects)
@echo $(no-objects)
# bash 中执行 make
$ make
a.o
nothing
3.4.5 call - 创建新的参数化函数
语法:
$(call <expression>,<parm1>,<parm2>,<parm3>...)
示例:
# Makefile 内容
log = "====debug====" $(1) "====end===="
all:
@echo $(call log,"正在 Make")
# bash 中执行 make
$ make
====debug==== 正在 Make ====end====
3.4.6 origin - 判断变量的来源
语法:
$(origin <variable>)
返回值有如下类型:
类型
含义
undefined <variable> 没有定义过
default <variable> 是个默认的定义, 比如 CC 变量
environment <variable> 是个环境变量, 并且 make时没有使用 -e 参数
file <variable> 定义在Makefile中
command line <variable> 定义在命令行中
override <variable> 被 override 重新定义过
automatic <variable> 是自动化变量
示例:
# Makefile 内容
val-in-file := test-file
override val-override := test-override
all:
@echo $(origin not-define) # not-define 没有定义
@echo $(origin CC) # CC 是Makefile默认定义的变量
@echo $(origin PATH) # PATH 是 bash 环境变量
@echo $(origin val-in-file) # 此Makefile中定义的变量
@echo $(origin val-in-cmd) # 这个变量会加在 make 的参数中
@echo $(origin val-override) # 此Makefile中定义的override变量
@echo $(origin @) # 自动变量, 具体前面的介绍
# bash 中执行 make
$ make val-in-cmd=val-cmd
undefined
default
environment
file
command line
override
automatic
3.4.7 shell
语法:
$(shell <shell command>)
它的作用就是执行一个shell命令, 并将shell命令的结果作为函数的返回.
作用和 `<shell command>` 一样, ` 是反引号
3.4.8 make 控制函数
产生一个致命错误: $(error <text ...>)
功能: 输出错误信息, 停止Makefile的运行
# Makefile 内容
all:
$(error there is an error!)
@echo "这里不会执行!"
# bash 中执行 make
$ make
Makefile:2: *** there is an error!. Stop.
输出警告: $(warning <text ...>)
功能: 输出警告信息, Makefile继续运行
# Makefile 内容
all:
$(warning there is an warning!)
@echo "这里会执行!"
# bash 中执行 make
$ make
Makefile:2: there is an warning!
这里会执行!
3.5 Makefile中一些GNU约定俗成的伪目标
如果有过在Linux上, 从源码安装软件的经历的话, 就会对 make clean, make install 比较熟悉.
像 clean, install 这些伪目标, 广为人知, 不用解释就大家知道是什么意思了.
下面列举一些常用的伪目标, 如果在自己项目的Makefile合理使用这些伪目标的话, 可以让我们自己的Makefile看起来更专业, 呵呵 ![]()
伪目标
含义
all 所有目标的目标,其功能一般是编译所有的目标
clean 删除所有被make创建的文件
install 安装已编译好的程序,其实就是把目标可执行文件拷贝到指定的目录中去
print 列出改变过的源文件
tar 把源程序打包备份. 也就是一个tar文件
dist 创建一个压缩文件, 一般是把tar文件压成Z文件. 或是gz文件
TAGS 更新所有的目标, 以备完整地重编译使用
check 或 test 一般用来测试makefile的流程
emacs下载地址:
http://mirrors.ustc.edu.cn/gnu/emacs/
一、编译安装
更换aliyun源
(3)安装编译所需的支持包,依环境而定
sudo apt-get install libgtk2.0-dev xserver-xorg-dev xorg-dev libncurses5 libncurses5-dev libidl.dev
sudo apt-get install libxpm-dev
sudo apt-get install libjpeg62-dev
sudo apt-get install libgif-dev
sudo apt-get install libtiff5-dev
sudo apt-get install libncurses5-dev
sudo apt install gnutls-dev
sudo apt-get install libgtk2.0-dev
编译、安装
注:最好指定一个安装目录,要不然编译出来的binary会被分散装到不同的地方
./configure --prefix=/usr/local/emacs --enable-font-backend --with-xft --with-freetype --with-x-toolkit=gtk2
参数解释:
–prefix=/usr/local/emacs 指定emacs安装目录,默认为/usr/local
–enable-font-backend 让emacs支持雅黑字体
–with-freetype 支持freetype字体
–with-x-toolkit=gtk 指定环境为gtk
二、功能插件配置(代码阅读神器)
把emacs变成类似sourceinsight代码浏览器
所需软件:
cscope-15.5.tar.gz http://sourceforge.net/projects/cscope
ecb-2.32.tar.gz http://sourceforge.net/projects/ecb
但是对于一般安装的GNU emacs来说还需要三个额外的包支持即eieio, semantic, speedbar
http://sourceforge.net/projects/cedet
将这三个包的下载并拷贝到/usr/local/emacs下
eieio-0.17.tar.gz
semantic-1.4.4.tar.gz
speedbar-0.14beta4.tar.gz
安装ecb和三个支持包:
cd /usr/local/emacs/site-lisp
tar zxfv ecb-2.32.tar.gz
tar zxfv eieio-0.17.tar.gz
tar zxfv semantic-1.4.4.tar.gz
tar zxfv speedbar-0.14beta4.tar.gz
做四个连接
ln -s ecb-2.32 ecb
ln -s eieio-0.17 eieio
ln -s semantic-1.4.4 semantic
ln -s speedbar-0.14beta4 speedbar
然后修改
site-start.el文件(有些系统如ubuntu,site-start.el文件在/etc/emacs目录下)
添加以下五行
(setq load-path (append load-path '("/usr/local/emacs/site-lisp/eieio")))
(setq load-path (append load-path '("/usr/local/emacs/site-lisp/semantic")))
(setq load-path (append load-path '("/usr/local/emacs/site-lisp/speedbar")))
(setq load-path (append load-path '("/usr/local/emacs/site-lisp/ecb")))
(require 'ecb)
重新启动一下emacs
M-x ecb-activate
看看出现了什么
cscope安装更为简单
$tar zxfv cscope-15.5.tar.gz
$cd cscope-15.5
$./configure
$make
make install
然后把contrib/xcscope/目录下的cscope-indexer复制到PATH目录比如/usr/local/bin
然后把xcscope.el复制到
/usr/local/emacs/site-lisp
修改/usr/local/emacs/site-lisp/site-start.el
添加
(require 'xcscope)
重新启动emacs 并且打开一个C文件看看有什么变化?
上述的两个软件的使用说明看看他们自带的文档,非常清楚
ubuntu快速启动文件
ubuntu 18.04系统目录:/usr/share/applications/
ubuntu 20.04系统目录:~/.local/share/applications
emacs.desktop
[Desktop Entry]
Name=Emacs
Comment=Emacs
Exec=/usr/local/emacs/bin/emacs
Icon=/usr/local/emacs/share/icons/hicolor/128x128/apps/emacs.png
Terminal=false
StartupNotify=true
Type=Application
附注:系统为ubuntu
Categories=Application;Development;
# Copyright 2022 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
EAPI=8
DESCRIPTION="Show Chinese lunisolar calender date"
HOMEPAGE="http://www.vinoca.org/"
SRC_URI="https://files.vinoca.org/${PN}-${PV}.tar.gz"
LICENSE="BSD"
SLOT="0"
KEYWORDS="~amd64"
src_configure() {
econf --prefix=/usr/local/bin
econf --with-posix-regex
}
src_install() {
emake DESTDIR="${D}" install
}
一、为什么要学习 Linux 内核
大部分程序员可能永远没有机会开发Linux内核或者驱动Linux,那么我们为什么还需要学习Linux内核呢?Linux的源代码和架构都是开放的,我们可以学到很多操作系统的概念和实现原理。Linux的设计哲学体系继承了UNIX,现在整个设计体系相当稳定和简化,这是大部分服务器使用Linux的重要原因。
那学习Linux内核的原因就在于此。
进一步了解内核的原理,有助于你更好地使用命令和程序设计,让你的面试和开发更上一层楼。但是不建议直接看源代码,因为Linux代码太大,容易丢失。
而最好的办法是,先了解一下Linux内核机制,知道基本的原理与流程。
不过,Linux内核机制也非常复杂,而且其中互相关联。
比如说,进程运行要分配内存,内存映射涉及文件的关联,文件的读写需要经过块设备,从文件中加载代码才能运行起来进程。这些知识点要反复对照,才能理清。
但是一旦攻克!你会发现Linux这个复杂的系统开始透明起来。
二、如何学习Linux内核?
内核的知识就像下面的绳结一样,一环扣一环,我们要解开它们,就必须要先找到线头也就是内核中的函数接口。初学阶段,我们一般不深入的研究内核代码,会使用内核的接口函数就不错了。
下面提供了如何学习这些内核函数的方法,就像解绳子一样
在我们学习Linux内核之前,我们首先需要掌握以下几点:
二、如何学习Linux内核?
内核的知识就像下面的绳结一样,一环扣一环,我们要解开它们,就必须要先找到线头也就是内核中的函数接口。初学阶段,我们一般不深入的研究内核代码,会使用内核的接口函数就不错了。
下面提供了如何学习这些内核函数的方法,就像解绳子一样
在我们学习Linux内核之前,我们首先需要掌握以下几点:
(1)如何学习内核,先了解Linux内核由哪些组成?
(2)须知Linux内核源码(下载的链接 )组织结构?
(3)重点需要学习地知识点有哪些?
(4)最后依据我为大家提供的的学习资料,开启我们的Linux内核学习之旅。
(5)全网最牛Linux内核Makefile系统文件详解(纯文字代码)
(6)全网最详细的Intel CPU体系结构分析(内核源码)
(7)深入理解Linux Kernel内核整体架构(图文详解)
(8)QEMU调试Linux内核环境搭建
(9)网友说Linux驱动讲不彻底,原来这才是Linux驱动
(10)一文让你深度了解Linux内核架构和工作原理
(11)从Linux内核看socket底层的本质(IO)
(12)Linux用户空间与内核空间通信(Netlink通信机制)
二,学习资料
2.1操作系统原理
【 强烈推荐阅读】一文带你彻底了解,零拷贝Zero-Copy技术(图解)
Linux操作系统学习——启动
Linux操作系统学习——内核运行
Linux操作系统学习——内核初始化
操作系统原理(一):操作系统原理与概述(流程图)
操作系统原理(二):Linux操作系统基础的常用命令
操作系统原理(三):Linux操作系统I/O机制原理(流程图详解)
操作系统原理(四):内存管理RAID磁盘阵列与配置
操作系统原理(五):内存管理之磁盘高速缓存机制原理
操作系统原理(六):存储管理之页式、段式、段页式存储
系统操作原理(七):进程的状态和转换(五态模型)
操作系统原理(八):进程同步的几种方式及基本原理
操作系统原理(九):处理器调度基本准则和实现原理
系统操作原理(十):多进程,多线程,并发执行中的死锁问题
系统操作原理(十一):操作系统原理:进程同步的几种方式及基本原理
系统操作原理(十二):趣谈操作系统原理,存储管理之页式、段式、段页式存储
系统操作原理(十三):操作系统:通过实战理解CPU上下文切换
汇编语言基础(十一):汇编语言基础知识(图文代码)
汇编语言入门(十二):汇编指令入门级整理,这些你必须要知道
汇编语言指令(十三):汇编语言的所有指令总结,一篇就够了
汇编语言进阶(十四):ARM体系结构处理器机制原理与实现
汇编语言进阶(十五): ARM指令集与汇编语言程序设计
2.2内存管理专题
【 强烈推荐阅读】尽情阅读,技术进阶,详解mmap原理
内存是什么?一文让你了解内存是怎么实现的
嵌入式开发必备技能,Linux内核源码组织结构
一文了解Linux内存管理,malloc、free 实现原理
内存管理系列(一):Linux操作系统内存管理(思维导图详解)
内存管理系列(二):Linux内存管理原理知识大总结
内存管理系列(三):学完操作系统内存管理,能回答这8个问题吗?
内存管理系列(四):理解 Memory barrier(内存屏障)
内存管理系列(五):内存回收之LRU链表机制原理
内存管理系列(六):虚拟内存和物理内存机制原理
内存管理系列(七):Malloc缺页中断不同情况处理总结及反向映射RMAP
内存管理系列(八):C/C++开发中的Malloc函数的实现原理
内存管理系列(九):深入理解glibc malloc:内存分配器实现原理
内存管理系列(十):操作系统是如何对内存进行管理的,内存与CPU之间的关系
内存管理系列(十一):为什么Linux需要虚拟内存,虚拟内存对操作系统有哪些作用
内存管理系列(十二):用户态内存内存映射函数Mmap的好处
内存管理系列(十三):内存管理:详解虚拟地址空间-MMU
内存管理系列(十四):C语言中的Malloc/free是如何分配内存的
内存管理系列(十五):从虚拟寻址到开源项目,Linux下的内存管理详解
内存管理系列(十六):一文带你了解,虚拟内存、内存分页、分段、段页式内存管理
内存管理系列(十七):Linux应用程序究竟消耗了多少内存
内存管理系列(十八):虚拟地址到物理地址,是什么时候开始映射
内存管理系列(十九):浅析Linux内存管理中SLAB分配器(源码分析)
内存管理系列(二十):基于Linux内存管理的内存分配(伙伴算法和slab算法)
内存管理系列(二十一):探索内存原理的内存映射文件(图文详解)
内存管理系列(二十二):吊打字节面试官,CPU缓存一致性协议MESI
内存管理系列(二十三):深入理解Linux内核页表映射分页机制原理
内存管理系列(二十四):谈谈物理内存与虚拟内存之间的映射(超详细~)
内存管理系列(二十五):内存管理:C/C++开发中的malloc函数的实现原理
内存管理系列(二十六):熬夜肝翻Linux内存管理所有知识点(图解)
2.3进程管理专题
进程管理系列(一):Linux进程管理原理详解(代码演示)
进程管理系列(二):十分钟让你像大佬一样快速了解进程状态(二种模型)
进程管理系列(三):作为互联网程序员,应该了解Linux进程六种状态吗?
进程管理系列(四):五分钟让你快速了解Linux进程管理实时调度与SMP
进程管理系列(六):浅析Linux的进程优先级(代码演示)
进程管理系列(七):进程管理|浅析C语言中并发同步与原子操作,锁三者是什么关系
进程管理系列(八):进程管理|深入理解Linux进程述符和进程状态
进程管理系列(九):一文读懂Linux内核中的任务间调度策略
进程管理系列(十):Linux内核之进程和线程的创建和派生
进程管理系列(十一):基于Linux有几种进程状态
进程管理系列(十二):操作系统的几种CPU调度策略
进程管理系列(十二):Linux 进程管理之调度和进程切换
进程管理系列(十三):一文搞懂六大进程通信机制原理(全网最详细)
进程管理系列(十四):超详细的Socket通信原理和实例讲解(白嫖走起~)
进程管理系列(十五):这是一份很全很全的IO基础知识与概念
进程管理系列(十六):深入理解Linux内核进程的管理与调度(全知乎最详细)
2.4网络协议栈专题
【 强烈推荐阅读】嵌入式必备:如何学习Linux内核网络协议栈
趣谈网络协议栈(一):套接字缓冲区原理
趣谈网络协议栈(二):数据包是如何处理的过程
趣谈网络协议栈(三):七层模型下三层数据通信
趣谈网络协议栈(四):传输的Arp报文结构
趣谈网络协议栈(五):Socket编程常用函数的原理及代码实现
趣谈网络协议栈(六):学习select和poll函数的内核实现
趣谈网络协议栈(七):Epoll从用户态到内核态过程分析
趣谈网络协议栈(八):套接字发送网络数据的过程
2.5设备驱动专题
浅谈设备驱动(一):操作系统 I/O 流程详解
浅谈设备驱动(二):Linux操作系统学习之字符设备
浅谈设备驱动(三):结合设备信息集合,探究设备和驱动是如何绑定的
2.6文件系统
详谈文件系统(一):一文让你彻底了解Linux内核文件系统(大总结)
2.7面试题/经验
【 强烈推荐阅读】从事十年嵌入式转内核开发(23K到45K),给兄弟们的一些建议
谈谈Linux内核的学习路线,具体要怎么学?
2022年嵌入式开发想进互联网大厂,你技术过硬吗?
嵌入式Linux内核学习经验总结,一篇让你掌握方法
盘点Linux内核(驱动开发,嵌入式,内核人群)必问的面试题
2022春招大厂-嵌入式开发经典笔试面试题目大整理
2.8内核书籍
《深入了解Linux内核》《Linux就该这么学》《Linux内核完全注释V3.0书签版》《Linux命令行大全 - 绍茨 (william E.shotts)》《Linux命令速查手册》《Linux性能优化大师》《Linux环境编程:从应用到内核》《Linux集群和自动化运维 余洪春》《Linux驱动程序开发实例(第2版)》《Linux高级程序设计(第3版)》《构建高可用Linux服务器(第4版)》
书籍免费领取地址:https://docs.qq.com/doc/DTkZRWXRFcWx1bWVx
三,内核学习路线
很多同学对Linux接触很少,对Linux平台的开发一无所知。现在,趋势越来越表明,作为一个优秀的软件开发者或者计算机IT从业者,掌握Linux是一个非常重要的谋生资源和手段。接下来我将结合我个人几年的开发经验,谈谈Linux的学习方法和学习中应该注意的一些事情,特别是关于Linux,类UNIX系统和开源软件文化。
就像我刚才说的,很多同学之前可能连Linux是什么都不知道,更别说UNIX了。所以我们从最基础的一点开始,Linux和UNIX的历史我们就不多说了,直接进入入门学习。
Linux入门非常简单。问题是你有没有耐心,有没有爱折腾,有没有不排除重装之类的大修。可以说不折腾是学不好Linux的。鸟哥说你要真正了解Linux的分区机制,并且对LVM的使用相当熟练。不超过20次是无法积累Linux安装经验的,所以不要怕折腾。
既然之前大家都用Windows,我也尽量照顾这些“菜鸟”。我的推荐,如果你是第一次接触Linux,那就先在虚拟机里试试。我推荐虚拟机用的Virtual Box。我不提倡使用VM,因为VM是开源的,是收费的。我不想推广盗版。当然,如果你有足够的钱,你可以试试VM,但我想说的是,即使是VM也不一定好。付费软件不一定好。首先,虚拟盒子很小。Windows平台下安装包80MB左右,而VM每转600MB。虽然很强大,但是消耗了很多资源。更何况虚拟盒子完全可以满足你的需求。所以,还是自己选比较好。如何使用虚拟机是你的事。这个就不教你了,因为很简单。如果不能,可以用谷歌或者百度。如果你英语好,可以直接看官方文件。
现在介绍Linux发行版的知识。正如你所见,Linux发行版并非Linux,Linux仅是指操作系统的内核,作为科班出生的你不要让我解释,我也没时间。
我推荐的发行版如下:
UBUNTU适合纯新手,追求稳定的官方支持,对系统稳定性要求弱,喜欢最新的应用,相对不喜欢折腾开发者。比UBUNTU难很多的发行版Debian,特点是稳定易用的包管理系统,缺点是缺乏企业支持,以社区开发为驱动。Arch,追逐时尚的开发者首选,优点是包更新相当快,升级无缝。基本上一次安装就可以一直工作,没有UBUNTU那样的版本概念。专业点叫滚动升级,让你的系统保持最新。缺点很明显,不稳定。同时安装配置也比Debian麻烦。比Arch更难的Gentoo,考验用户的综合水平。从系统安装到微调,内核编译都是手把手。是高手和黑客展示自己技术手段,按需配置符合自己要求的系统的首选。
Slackware与Gentoo类似:
社区维护的RedHat的副本CentOS,完全是用RedHat的源代码重新编译的,理论上和RedHat的兼容性是最好的。如果你专注于Linux服务器,比如网络管理和网站建设,那么CentOS就是你的选择。
LFS,终极黑客炫耀工具,完全从源代码安装编译系统。在安装之前,您只能获得一个文档。您所要做的就是按照文档中的说明,一步一步,一个订单一个订单地,一个一个地构建您的Linux包。完全在你的掌控之中,你想要什么就有什么。如果你制作了LFS,那就证明你的Linux技术相当不错。如果你能借鉴LFS文档,把Linux从源代码移植到嵌入式系统,我敢说你能在中国企业做得很好。
你得挑一个适合自己的系统,然后装在虚拟机里开始用。如果你想快速学习Linux,我有一个建议,你应该忘记图形界面。不要去想图形界面能不能为你的问题提供答案,而是去世界各地寻找,询问如何用命令行解决你的问题。在这个过程中,你最好掌握好Linux的命令,至少要知道常用的命令,同时要建立自己的知识库,里面包含了你积累的知识。
下一阶段需要学习Linux平台的C++/C++开发,以及Bash脚本编程,如果对Java有很深的兴趣,还需要学习Java。同样,我建议你抛弃图形界面的IDE,从VIM开始。为什么是VIM而不是Emacs?我无意挑起编辑器大战,但我认为VIM适合新手和手笨脑慢的开发者。Emacs的按键太多,太复杂,我很害怕。然后是GCC,Make,Eclipse(Java,C++或者)。虽然Eclipse中列出了C++,但是我不建议用IDE开发C++,因为这不是Linux的文化,你很容易忽略一些应该注意的问题。IDE让你懒的跟猪一样懒。如果你对程序调试和测试感兴趣,你必须学好GDB。如果不是GDB,这也是一门必修课。这是发展的第一步。注意,我没有提到任何关于Linux API的东西,现阶段也不关心这个。你要做的就是积累经验,Linux平台开发的经验。我推荐的书如下:《C语言编程》,或者谭浩强的。c,白皮书当然更好。++C++ Primer Plus是C推荐的,我不喜欢Java,所以不推荐。工具推荐VIM的官方手册,GCC中文文档,GDB中文文档,GNU开源软件开发指南(电子书),汇编语言编程(让你对库,链接,嵌入式汇编,编译器优化选项有个初步的了解,不深入)。
如果过不了这个阶段,就不用做了。这是底线,也是最基本的基础。否则,离开,不要开发Linux。不专业的Linux开发者做出来的程序与Linux文化或者UNIX文化相悖,程序走不了多远,也不可能像Bash、VIM这样神奇的产品。所以做不好就走人。
接下来进入Linux系统编程,唯一的选择,APUE,UNIX环境下的高级编程。反复读,10遍太少。如果你在大学能把这本书砸了,里面的内容都练过了,有作品,口语表达能力足够强,面试的时候就能说服所有考官。(可能有点夸张,但APUE绝对是圣经读物,连Windows程序员都从中汲取养分。谷歌创始人的案头书,扎伯克的床头读物。)
看完这本书,你会对Linux系统编程有很好的了解。Linux和Windows平台有什么区别?它们的优缺点是什么?我的总结如下:Windows平台开发难。微软的系统API一直在扩展。如果你想使用最新最高效的功能,你必须时刻学习最适合当前流行系统的功能。不,Linux有大约100个核心API,所以你可以用很好的记忆力记住它们。而且会长期不变。为什么不呢?因为它兼容UNIX,符合POSIX标准。因此,Linux平台的开发大多集中在底层或服务器编程上。这是它的优势。当然图形是Linux的软肋,但从一个开发者的角度来说,我不在乎,因为我也能适应命令行。如果有更好的图形界面,我会把它作为礼物。另外,Windows是关闭的,你甚至不知道系统做了什么。你将永远被微软牵着鼻子走。想想吧。如果微软说Win8不支持QQ,腾讯也不会哭死。而且Linux是完全开源的。如果不喜欢,可以自己改,只要足够熟练。另外,虽然Windows使用的人很多,但是使用的场合比较单一,以桌面为主。Linux各方面都有发展,尤其是云计算、服务器软件、嵌入式领域、企业应用,兼容性一流。由于POSIX可以在UNIX系统上无缝运行,因此Apple Mac和IBM AS400系列都完全支持它。另外,Linux的开发环境支持绝对一流,无论是C/C++,Java,Bash,Python,PHP,Javascript,。。。。。。连C#都支持。而且微软除了Visual Stdio套件都不太友好吧?
如果你看了APUE后有很多感触,想验证你的一些想法或经验,推荐UNIX编程艺术,世界顶尖黑客将与你分享他们的观点。现在是时候转移注意力了。总的来说,我分为四个方向:网络、图形、嵌入式、设备驱动。
如果选择网络,细分的话,其他的不太熟悉,只说服务器软件编写和高性能并发程序编写。相对来说,这是网络编程中技术含量最高的,也是最底层的。需要很多经验,看很多书,做很多项目。
我的看法是以下面的顺序来看书:
APUE的深度阅读——尤其是进程、线程、IPC、套接字多核编程——Pthread一定要吃透,你是NBUNIX网络编程–第1卷,第2卷TCP/IP网络详解——是时候再看一遍以上两本书了。TCP/IP网络的详细说明–第2卷。我觉得看第二卷就差不多了。当然,最好还是看第三卷。尽力去看吧。Lighttpd源代码——这个服务器也很有名。NGX源代码——与Apache相比,Nginx的源代码更少。如果能大致看一下,就是NB了。看源码主要是学习里面的socket编程和并发控制,想想就激动。如果你有这些技能,你可以试试给暴雪发简历,给他们写服务器后台,以为全世界的魔兽都运行在你的服务器软件上。Linux内核TCP/IP协议栈——深入了解TCP/IP实现如果还是喜欢驱动设计,可以看看底层协议,比如链路层。给路由器,网卡,网络设备,嵌入式系统软件写驱动应该不是问题。当然,一般的网络公司,哪怕是百度级别的,都应该毫不犹豫的录用你。看后面的书只需要时间和经验,所以35岁之前就做吧!跳槽到给你未来的地方!
图形方向,我觉得图形方向也是很有前途的,以下几个方面:
Opengl的工业和游戏开发在国外已经比较成熟。
动画特效,比如皮克斯,在国外也比较成熟。
GPU计算技术可以应用于浏览器网页渲染和GPU计算资源利用。因为开源,所以有很多文档程序可以参考。如果能进入火狐开发,或者谷歌做浏览器开发,应该很不错。
嵌入式方向:嵌入式方向没说的,Linux很重要
掌握多种架构,不仅仅是X86,ARM,MCU等。必须理解。如果你不懂硬件,我预见你会死在路上,我也想往嵌入式方向走,但是我觉得就算是学电子的学生也比不过学校教嵌入式的方式。我劝你,做之前一定要了解硬件。如果你去做嵌入式应用开发,只能祝你好运了。不要碰上诺基亚、惠普这样的公司,否则你会很惨。
驱动设计:软件开发周期很长,硬件不一样,很快。每个月都有这么多新硬件诞生,如何让它们在Linux上工作是你的工作。因为Linux兼容性好,如果不是太低级的驱动,基本的C语言就可以了,系统架构影响不大。由于系统支持,您可能可以在ARM上使用PC硬件,但需要做一些更改。所以硬件驱动开发不像嵌入式,对硬件知识要求很高。可能的方向很多,比如家电,特别是像索尼、日立、希捷、富士康这样的工厂,比较稀缺。
四,学习Linux内核
学习linux内核不像学习语言。一个月或者三月就能掌握C或者java。学习linux内核需要循序渐进,掌握正确的linux内核学习路线非常重要。本文将分享一些学习linux内核的建议。
1.了解操作系统的基本概念。如果没有,可以学习《操作系统:设计与实现》,Andrew S.Tanenbaum写的那本,以MINIX为例解释操作系统的概念。非常推荐。
2.有了操作系统的基本概念,你就可以理解Linux的机制了。推荐罗伯特·拉芙写的Linux内核设计与实现。这本书从概念上解释了Linux有什么以及它是如何工作的。这本书应该反复仔细阅读。
3.有了Linux内核的知识,我们还需要具体学习Linux内核源代码。经典的是丹尼尔·p·博韦特写的《深入理解Linux内核》。学习这本书的时候,要看看内核代码。这本书学起来挺费劲的,所以有很多代码要研究。但是,如果这本书很好理解,那么恭喜你,你已经对Linux内核很熟悉了。
4.如果你想开发设备驱动,可以向O 'Reilly Press学习Linux设备驱动。这本书是驾驶入门的好材料。还有一本很好的教材,精通Linux驱动开发,可以参考一下。开车,难免要学习一些硬件协议和资料。如果你研究的是哪一种,可以找相应的硬件文档,了解硬件的工作原理。这些我就不细说了。
5.网络部分,学习一些Linux网络部分学习《深入了解LINUX网络技术内幕》。这本书把Linux的网络部分讲得非常清楚透彻。不过我们一般不做这方面的研究,也不需要做那么多研究。毕竟现在相关岗位很少。
6.现在Linux相关的工作大多集中在一些嵌入式开发领域,如arm、mips等。你要学习以下关于架构的信息,了解CPU的设计和工作模式。看看ARM对应的芯片手册就知道了,很详细的。mips随便看看MIPS运行,有一两个版本。两个版本有些区别,建议全看。
7.补充一点经验。不要以为Linux庞大复杂,就很难学。认真学习,什么都可以学。就看你的毅力和恒心了。另外,不要走弯路,不要看市面上那些讲Linux0.11的书,学你想学的就好。就像学C语言看谭浩强一样,走弯路,费力气,严重影响学习效果。
关于linux内核学习路线,再多说几句应用编程,有时候经常会需要的:
1. 学习Linux应用编程,建议看《unix环境高级编程》,把里面的例子都做一遍,会对整个Linux编程有系统都认识。
2. 针对Linux,有本 《Linux系统编程》,学完上一本,这本很快看一遍就懂了。主要是针对Linux具体懂一些内容,讲的挺全了,很实用。
3. Linux网络编程,系统的学习一下《unix网络编程。卷1,套接字联网api》,基本上网络应用相关的程序就都没问题了。
这些内容,分几年时间,分步计划学习,就会成为Linux高手了。个人建议参加零声教育的培训,学习效率会高很多,有目的性的参加培训,缩短周期,快速成型才是时代所需。
官方地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂
以上就是Linux内核学习路线,关于学习Linux内核的建议,希望对小伙伴们有帮助。
在LINUX系统中有一个重要的概念:一切都是文件,既Linux以文件的形式对计算机中的数据和硬件资源进行管理。 在LINUX系统中,把一切资源都看作是文件,包括硬件设备。每个硬件都被看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。
一、Linux文件体系
Linux文件的类型有很多种(下面会详细说),这些文件被LINUX使用目录树进行管理,所谓的目录树就是以根目录(/)为主,向下呈现分支状的一种文件结构。不同于纯粹的ext2之类的文件系统,我把它称为文件体系,一切皆文件和文件目录树的资源管理方式一起构成了Linux的文件体系,让Linux操作系统可以方便使用系统资源。
文件系统比文件体系涵盖的内容少很多,Linux文件体系主要在于把操作系统相关的东西用文件这个载体实现:文件系统挂载在操作系统上,操作系统整个系统又放在文件系统里。
1.1、Linux中的文件类型
那就先简单说说Linux中的文件类型,主要关注普通文件、目录文件和符号连接文件。
普通文件(-)
从Linux的角度来说,类似mp4、pdf、html这样应用层面上的文件类型都属于普通文件
Linux用户可以根据访问权限对普通文件进行查看、更改和删除
目录文件(d,directory file)
目录文件对于用惯Windows的用户来说不太容易理解,目录也是文件的一种
目录文件包含了各自目录下的文件名和指向这些文件的指针,打开目录事实上就是打开目录文件,只要有访问权限,你就可以随意访问这些目录下的文件(普通文件的执行权限就是目录文件的访问权限),但是只有内核的进程能够修改它们
虽然不能修改,但是我们能够通过vim去查看目录文件的内容
符号链接(l,symbolic link)
这种类型的文件类似Windows中的快捷方式,是指向另一个文件的间接指针,也就是我们常说的软链接
块设备文件(b,block)和字符设备文件(c,char)
这些文件一般隐藏在/dev目录下,在进行设备读取和外设交互时会被使用到
比如磁盘光驱就是块设备文件,串口设备则属于字符设备文件
系统中的所有设备要么是块设备文件,要么是字符设备文件,无一例外
FIFO(p,pipe)
管道文件主要用于进程间通讯。比如使用mkfifo命令可以创建一个FIFO文件,启用一个进程A从FIFO文件里读数据,启动进程B往FIFO里写数据,先进先出,随写随读。
套接字(s,socket)
用于进程间的网络通信,也可以用于本机之间的非网络通信
这些文件一般隐藏在/var/run目录下,证明着相关进程的存在
Linux 的文件是没有所谓的扩展名的,一个 Linux文件能不能被执行与它是否可执行的属性有关,只要你的权限中有 x ,比如[ -rwx-r-xr-x ] 就代表这个文件可以被执行,与文件名没有关系。跟在 Windows下能被执行的文件扩展名通常是 .com .exe .bat 等不同。
不过,可以被执行跟可以执行成功不一样。比如在 root 主目彔下的 install.log 是一个文本文件,修改权限成为 -rwxrwxrwx 后这个文件能够真的执行成功吗? 当然不行,因为它的内容根本就没有可以执行的数据。所以说,这个 x 代表这个文件具有可执行的能力, 但是能不能执行成功,当然就得要看该文件的内容了。
虽然如此,不过我们仍然希望能从扩展名来了解该文件是什么东西,所以一般我们还是会以适当的扩展名来表示该文件是什么种类的。
所以Linux 系统上的文件名真的只是让你了解该文件可能的用途而已, 真正的执行与否仍然需要权限的规范才行。比如常见的/bin/ls 这个显示文件属性的指令要是权限被修改为无法执行,那么ls 就变成不能执行了。这种问题最常发生在文件传送的过程中。例如你在网络上下载一个可执行文件,但是偏偏在你的 Linux 系统中就是无法执行,那就可能是档案的属性被改变了。而且从网络上传送到你 的 Linux 系统中,文件的属性权限确实是会被改变的
1.2、Linux目录树
对Linux系统和用户来说,所有可操作的计算机资源都存在于目录树这个逻辑结构中,对计算机资源的访问都可以认为是目录树的访问。就硬盘来说,所有对硬盘的访问都变成了对目录树中某个节点也就是文件夹的访问,访问时不需要知道它是硬盘还是硬盘中的文件夹。
目录树的逻辑结构也非常简单,就是从根目录(/)开始,不断向下展开各级子目录。
目录
从上到下,你所看到的目录如下
/bin
/bin 目录是包含一些二进制文件的目录,即可以运行的一些应用程序。 你会在这个目录中找到上面提到的 ls 程序,以及用于新建和删除文件和目录、移动它们基本工具。还有其它一些程序,等等。文件系统树的其他部分有更多的 bin 目录,但我们将在一会儿讨论这些目录。
/boot
/boot 目录包含启动系统所需的文件,包括系统核心文件。我必须要说吗? 好吧,我会说:不要动它! 如果你在这里弄乱了其中一个文件,你可能无法运行你的 Linux,修复被破坏的系统是非常痛苦的一件事。 另一方面,不要太担心无意中破坏系统:你必须拥有超级用户权限才能执行此操作。
/dev
/dev 目录包含设备文件,,如打印机,硬盘等外围设备等。 其中许多是在启动时或甚至在运行时生成的。 例如,如果你将新的网络摄像头或 USB 随身碟连接到你的机器中,则会自动弹出一个新的设备条目。
/etc
/etc 的目录很重要 ,是“ 要配置的所有内容(Everything To Configure)”,因为它包含大部分(如果不是全部的话)的系统配置文件以及管理相关软件。 例如,包含系统名称、用户及其密码、网络上计算机名称以及硬盘上分区的安装位置和时间的文件都在这里。 再说一遍,如果你是 Linux 的新手,最好是不要在这里接触太多,直到你对系统的工作有更好的理解。
/home
/home 是存放用户专属目录。在我的情况下,/home 下有两个目录:/home/paul,其中包含我所有的东西;另外一个目录是 /home/guest 目录,以防有客人需要使用我的电脑。
/lib
/lib 存放一些共享的函数库。库是包含应用程序可以使用的代码文件。它们包含应用程序用于在桌面上绘制窗口、控制外围设备或将文件发送到硬盘的代码片段。
在文件系统周围散布着更多的 lib 目录,但是这个直接挂载在 / 的 /lib 目录是特殊的,除此之外,它包含了所有重要的内核模块。 内核模块是使你的显卡、声卡、WiFi、打印机等工作的驱动程序。
/media
在 /media 目录中,当你插入外部存储器试图访问它时,将自动挂载它。与此列表中的大多数其他项目不同,/media 并不追溯到 1970 年代,主要是因为当计算机正在运行而动态地插入和检测存储(U 盘、USB 硬盘、SD 卡、外部 SSD 等),这是近些年才发生的事。
/mnt
然而,/mnt 目录是一些过去的残余。这是你手动挂载存储设备或分区的地方。现在不常用了。
/opt
/opt 目录通常是你编译软件(即,你从源代码构建,并不是从你的系统的软件库中安装软件)的地方。应用程序最终会出现在 /opt/bin 目录,库会在 /opt/lib 目录中出现。
稍微的题外话:应用程序和库的另一个地方是 /usr/local,在这里安装软件时,也会有 /usr/local/bin和 /usr/local/lib 目录。开发人员如何配置文件来控制编译和安装过程,这就决定了软件安装到哪个地方。
/proc
/proc目录中存放系统核心和执行程序之间的信息就像 /dev 是一个虚拟目录。它包含有关你的计算机的信息,例如关于你的 CPU 和你的 Linux 系统正在运行的内核的信息。与 /dev 一样,文件和目录是在计算机启动或运行时生成的,因为你的系统正在运行且会发生变化。
/root
/root 是系统的超级用户(也称为“管理员”)的主目录。 它与其他用户的主目录是分开的,因为你不应该动它。 所以把自己的东西放在你自己的目录中,伙计们。
/run
/run 是另一个新出现的目录。系统进程出于自己不可告人的原因使用它来存储临时数据。这是另一个不要动它的文件夹。
/sbin
/sbin 与 /bin 类似,但它包含的应用程序只有超级用户(即首字母的 s )才需要。你可以使用 sudo命令使用这些应用程序,该命令暂时允许你在许多 Linux 发行版上拥有超级用户权限。/sbin 目录通常包含可以安装、删除和格式化各种东西的工具。你可以想象,如果你使用不当,这些指令中有一些是致命的,所以要小心处理。
/usr
/usr 目录是在 UNIX 早期用户的主目录所处的地方。然而,正如我们上面看到的,现在 /home 是用户保存他们的东西的地方。如今,/usr 包含了大量目录,而这些目录又包含了应用程序、库、文档、壁纸、图标和许多其他需要应用程序和服务共享的内容。
你还可以在 /usr 目录下找到 bin,sbin,lib 目录,它们与挂载到根目录下的那些有什么区别呢?现在的区别不是很大。在早期,/bin 目录(挂载在根目录下的)只会包含一些基本的命令,例如 ls、mv 和 rm ;这是一些在安装系统的时候就会预装的一些命令,用于维护系统的一个基本的命令。 而 /usr/bin 目录则包含了用户自己安装和用于工作的软件,例如文字处理器,浏览器和一些其他的软件。
但是许多现代的 Linux 发行版只是把所有的东西都放到 /usr/bin 中,并让 /bin 指向 /usr/bin,以防彻底删除它会破坏某些东西。因此,Debian、Ubuntu 和 Mint 仍然保持 /bin 和 /usr/bin (和 /sbin 和 /usr/sbin )分离;其他的,比如 Arch 和它衍生版,只是有一个“真实”存储二进制程序的目录,/usr/bin,其余的任何 bin 目录是指向 /usr/bin` 的“假”目录。
/srv
/srv 目录包含服务器的数据。如果你正在 Linux 机器上运行 Web 服务器,你网站的 HTML文件将放到 /srv/http(或 /srv/www)。 如果你正在运行 FTP 服务器,则你的文件将放到 /srv/ftp。
/sys
/sys 是另一个类似 /proc 和 /dev 的虚拟目录,它还包含连接到计算机的设备的信息。
在某些情况下,你还可以操纵这些设备。 例如,我可以通过修改存储在 /sys/devices/pci0000:00/0000:00:02.0/drm/card1/card1-eDP-1/intel_backlight/brightness 中的值来更改笔记本电脑屏幕的亮度(在你的机器上你可能会有不同的文件)。但要做到这一点,你必须成为超级用户。原因是,与许多其它虚拟目录一样,在 /sys 中打乱内容和文件可能是危险的,你可能会破坏系统。直到你确信你知道你在做什么。否则不要动它。
/tmp
/tmp 包含临时文件,通常由正在运行的应用程序放置。文件和目录通常(并非总是)包含应用程序现在不需要但以后可能需要的数据。
你还可以使用 /tmp 来存储你自己的临时文件 —— /tmp 是少数挂载到根目录下而你可以在不成为超级用户的情况下与它进行实际交互的目录之一。
/var
/var 目录中 存放经常变动的文件,如日志文件,临时文件,电子邮箱。最初被如此命名是因为它的内容被认为是 可变的(variable),因为它经常变化。今天,它有点用词不当,因为还有许多其他目录也包含频繁更改的数据,特别是我们上面看到的虚拟目录。
不管怎样,/var 目录包含了放在 /var/log 子目录的日志文件之类。日志是记录系统中发生的事件的文件。如果内核中出现了什么问题,它将被记录到 /var/log 下的文件中;如果有人试图从外部侵入你的计算机,你的防火墙也将记录尝试。它还包含用于任务的假脱机程序。这些“任务”可以是你发送给共享打印机必须等待执行的任务,因为另一个用户正在打印一个长文档,或者是等待递交给系统上的用户的邮件。
————————————————
版权声明:本文为CSDN博主「轩阁楼主」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xuangelouzhu/article/details/118082541
一、先聊聊保护模式的基本概念:为什么叫保护模式?到底保护了啥?是怎么保护的?
1、保护模式:要搞定出保护模式的意义,需要理解另一个概念:实模式;实模式下,存在以下重大的安全隐患:
(1)内存没权限区别:用户程序可以访问任意内存,包括操作系统运行的内存、其他程序运行的内存(个人观点:这是万恶之源。病毒、木马、外挂都是想尽各种办法访问操作系统或其他进程的代码/数据,达到自己的某些特定目的),所有的代码和数据都是能查询和删改的,毫无隐私可言
(2)用户程序可以随意访问和删除段寄存器,达到和上述同样的效果
(3)对I/O端口无限制的访问,比如打开文件、发送网络数据包、打印到屏幕或分配内存等;
(4)可以执行所有指令
2、保护模式保护了啥? 本质上:
(1)限制应用程序对内存、寄存器的访问,保护操作系统、其他进程的代码/数据不被恶意访问、甚至删改!
(2)限制用户程序对某些I/O端口的访问。
(3)限制用户程序执行某些指令
3、怎么保护的?
(1)通过GDT/LDT/IDT表、CS/DS/SS等段寄存器,严格限制用户程序对内存的读写:访问内存前,CS中的CPL先和selector选择子中的RPL对比,数值小的再和GDT表的DPL比,如果数值小于等于,说明是有权限的,才能继续访问对应的内存!(这里多说几句: 64位的windows已经平坦了,进程访问的虚拟地址已经拉通,段寄存器名存实亡;进程之间隔离主要通过CR3,把不同进程同样的虚拟地址映射到不同的物理地址;换句话说,内存保护主要靠页表、MMU等隔离不同进程的物理内存)
附:
CPL:全称current privilege level,存放在代码段寄存器中(cs),代表当前执行程序的特权级;常见的调试器都能查到:一般都是11,即所谓的3环!
RPL: 全称request privilege level,请求特权级,存放在段选择子中。注意,不是段寄存器中,不是段寄存器中,不是段寄存器中;它的含义是当前我想以 RPL 这个级别来请求你把这个段选择子置入段寄存器。实际上 RPL 并没有什么用(个人观点,仅供参考),因为请求者任何时刻都可以让 RPL = 0。但是如果请求者是 CPL = 0 的程序,用 RPL = 3 的级别来请求 DPL = 0的数据段,必然会失败;
DPL: 全称descriptor privilege level,存放在段描述符中,用于表示段的特权级
(2)要想访问必须通过操作系统内核,比如打开文件、发送网络数据包、打印到屏幕或分配内存
二、这里简化一下说说要点:
1、 生成并加载GDT表
实模式下任何进程可以无限制读写任何内存,甚至os的内存,毫无安全性可言;需要对用户进程读写内存的地址做严格限制,衍生出了保护模式;保护模式将内存分成不同的段,段基址、limit、各种属性存放在GDT表;用户程序读写段内存时需要先通过段寄存器的selector在GDT找到段描述符,查看是否有权限、偏移地址是否超过limit等。如果一切ok,可以继续读写段内数据;
cs、ds、ss段的描述符:
注意点:(1)用户程序运行在3环,是没有权限更改GDT的,所以这种方式完全可以限制3环程序对内存的读写;windows要想改GDT,要么连接windbg,要么写驱动;
(2)lgdt [cs:gdt_size],从操作数指向的地址取6字节,高4字节作为gdt的基址,低2字节作为gdt中描述符的个数,如下:
这6字节早在编译时就确定了:
(3)GDT的机制是CPU定的,操作系统负责运维和使用各段的数据
2、打开A20
8086下,地址线有20位,寻址范围从0x00000~0xfffff,超过0xfffff的地址会被cpu重新从0x00000开始,相当于丢掉进位(或把地址对0xfffff取模)。80286以后,地址总线扩展到24位,为了访问0x100000~0x10FFEF之间的内存,而不是象8086/8088那样回绕到0,需要开启A20地址线,方法如下:
mov dx,0x92 ;南桥ICH芯片内的端口0x92
in al,dx
or al,0x02
out dx,al ;打开A203、开启保护模式
CPU提供的控制寄存器CR0~CR3用于控制CPU的运行模式。CR0第一位(0位)是保护模式允许位(Protection Enable,PE),如果把这个位置为1,那么处理器将会进入保护模式,核心代码如下:
cli ;关闭中断,但后面一直没打开
mov eax,cr0
or eax,0x01
mov cr0,eax ;设置PE位,处理器进入保护模式注意:要先调用cli把if置0,关闭中断,避免给CR0赋值时被打断;
4、清空段寄存器中的缓存和旧流水线指令
(1)段寄存器实际上有96位,保护模式下的汇编指令只能操作位于低16位的selector,剩余80位中部分作为缓存(可用sreg命令查看段寄存器dh、dl的值,这些都是描述符的缓存),存储了段基址。只要段不改变,缓存就不会更新;但保护模式下不能直接用实模式的地址(权限不够、位数不对等),需要清空;
(2)cpu和内存的速度差异很大,为了提高效率,cpu会提前预测并执行某些分支指令(幽灵漏洞就是这么来的,细节可参考B站的一个科普视频:https://www.bilibili.com/video/BV1eW411i7ZM?from=search&seid=2138940898008952062),这就是所谓的乱序执行,进入保护模式后这些指令的逻辑结果可能是有问题的,也要马上清除;
清除各个段寄存器、乱序指令缓存的办法:马上调用jmp指令跳转,让cpu认为当前各种缓存已经失效,核心代码如下:
;保护模式
jmp 0x0008:flush-$$ ;现在是在16位保护模式下,0x0008依然是段的选择子,而flush则是偏移地址
[bits 32]
flush: ;头部加了vstart=0x7c00,这里变成了0x7c85,所以上面的jmp中的偏移要减去开头的基址,得到偏移
mov cx,0x0010
mov ds,cx5、完整代码:
;---------------------保护模式主引导扇区程序---------------------
SECTION protectModel vstart=0x7c00 align=16
mov ax,0x00
mov ss,ax
mov sp,0x7c00
mov ax,[cs:gdt_base];ax=0x7e00
mov dx,[cs:gdt_base+0x02];dx=0x0000;
;mov ax,[cs:gdt_base+0x7c00];这段已经被加载到0x7c00,所以需要加上;ax=0x7e00
;mov dx,[cs:gdt_base+0x7c00+0x02];dx=0x0000;
mov bx,0x10
div bx
mov ds,ax ;得到base基地址,ds=0x7e0
mov bx,dx ;得到偏移地址,这里是0x0000
;---------------------安装描述符---------------------
;描述符0
mov dword [ebx+0x00],0x00 ;第一个描述符必须是0;这里默认是ds段,也就是gdt_base的基址
mov dword [ebx+0x04],0x00 ;ebx是gdt表的偏移,ds是gdt的基址
;描述符1
mov dword [ebx+0x08],0x7c0001FF
mov dword [ebx+0x0c],0x00409800 ;基地址0x00007c00,段界限0x001FF,粒度是字节,
;长度是512字节,在内存中的32位段,特权级为0,只能执行的代码段
;描述符2
mov dword [ebx+0x10],0x8000FFFF
mov dword [ebx+0x14],0x0040920B ;基地址0x000B8000,段界限0x0FFFF,粒度是字节,
;长度是64KB,在内存中的32位段,特权级为0,可以读写的向上拓展的数据段
;描述符3
mov dword [ebx+0x18],0x00007A00
mov dword [ebx+0x1c],0x00409600 ;基地址0x00000000,段界限0x07A00,粒度是字节,
;在内存中的32位段,特权级为0,可以读写的向下拓展的栈段
mov word [cs:gdt_size],31;写入GDT段界限,4个描述符是32个字节,所以界限就是31
lgdt [cs:gdt_size] ;load gdt
;mov word [cs:gdt_size+0x7c00],31;写入GDT段界限,4个描述符是32个字节,所以界限就是31
;lgdt [cs:gdt_size+0x7c00] ;load gdt
mov dx,0x92 ;南桥ICH芯片内的端口0x92
in al,dx
or al,0x02
out dx,al ;打开A20
cli ;关闭中断,但后面一直没打开
mov eax,cr0
or eax,0x01
mov cr0,eax ;设置PE位,处理器进入保护模式
;保护模式
jmp 0x0008:flush-$$ ;现在是在16位保护模式下,0x0008依然是段的选择子,而flush则是偏移地址
[bits 32]
flush: ;头部加了vstart=0x7c00,这里变成了0x7c85,所以上面的jmp中的偏移要减去开头的基址,得到偏移
mov cx,0x0010
mov ds,cx
;以下在屏幕上显示"Protect mode OK."
mov byte [0x00],'P'
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],' '
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
mov byte [0x18],' '
mov byte [0x1a],'O'
mov byte [0x1c],'K'
;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作
mov cx,00000000000_11_000B ;加载堆栈段选择子
mov ss,cx
mov esp,0x7c00
mov ebp,esp ;保存堆栈指针
push byte '.' ;压入立即数(字节)
sub ebp,4
cmp ebp,esp ;判断压入立即数时,ESP是否减4
jnz ghalt
pop eax
mov [0x1e],al ;显示句点
ghalt:
hlt ;已经禁止中断,将不会被唤醒
;-------------------------------------------------------------------------------
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址,主引导扇区是512个字节,这个地址刚好在主引导扇区之后
times 510-($-$$) db 0
db 0x55,0xaa说明:
(1)整段代码被加载到0x7c00处,所以在开头额外加vstart=0x7c00,让nasm从这个地址开始编译;
紧接着这4行代码也要更改:去掉0x7c00,因为gdt_base和gdt_size已经相对0x7c00计算偏移
mov ax,[cs:gdt_base+0x7c00]
mov dx,[cs:gdt_base+0x7c00+0x02]
mov word [cs:gdt_size+0x7c00],31;写入GDT段界限,4个描述符是32个字节,所以界限就是31
lgdt [cs:gdt_size+0x7c00]
(2)flush也是相对0x7c00开始计算偏移的,具体偏移是0x7c85,远超代码段的limit,导致出错异常,又跳回biso启动处执行
解决办法也简单,直接改成jmp 0x0008:flush-$$ 即可,让后面的偏移值相对于段开始的地方,这次对了,如下:
(3)81行代码:push byte '.' ,但nasm还是编译成push 0x0000002e, 估计是为了内存对齐
(4)效果:在频幕上打印一行字
参考:
1、https://manybutfinite.com/post/cpu-rings-privilege-and-protection/ cpu运行级别和保护机制
2、 https://www.cnblogs.com/Philip-Tell-Tru … 11248.html
3、x86汇编:从实模式到保护模式
4、 https://blog.csdn.net/q1007729991/artic … s/52727332 cpu特权等级
用户按下开机键,几秒的时间,都经历了啥?
1、cpu各个寄存器赋初始值,cs.base=0xffff0000, eip=0xfff0,其他寄存器都是0,这时cs:ip得到的物理地址:0xfffffff0;
cpu上电后为啥会把cs:ip赋成这种初始值了? 可能是希望把BIOS-ROM放在可寻址4GB最高端,给操作系统和用户程序大段完整的RAM空间,便于后者在运行时的内存管理
2、cpu跳转到0xffff0执行。但由于该地址距离0xfffff(实模式下内存空间只有1M)仅16byte,空间十分有限,无法执行复杂逻辑,只能jmp到其0xf000:e05b继续执行;
3、0xf000:e05b任然是BIOS的地址,继续执行检测代码,看看内存(RAM)、显示器、键盘、鼠标、硬盘等外设是否完好。如有问题,会发出长短不等的滴滴声响,可凭此判断故障类型
4、外设检测完,如果一切正常,会查找用户设置的启动顺序。普通用户首次安装OS时一般选择从CD/DVD启动,装OS;装好后取出光盘,BIOS会自动从磁盘加载MBR到0x7c00;
5、加载MBR到0x7c00后,jmp到这里继续执行;由于只加载一个扇区,能执行的代码不超过510字节(还有2字节是扇区结尾的标识:0xaa55),能干的事也有限,所以MBR一般会继续从磁盘其他地方把os代码都拷贝到内存,同时重定位代码,完成os的加载;
6、继续jmp到os代码执行;
这6步中,1-4部不用我们操心,厂家的产品在出厂前已经做好;第5部,从磁盘的0柱面、0磁头、1扇区加载MBR到内存0x7c00处也BIOS干的,不需要开发人员操心;真正需要开发人员编写代码的地方:
MBR的代码,这部分代码被bios加载到内存后需要做什么?
MBR代码能运行的代码不超过510字节,真正的os肯定不止这点代码,剩余代码怎么办?
既然MBR能运行的代码不超过510字节,能干的活有限,那么干脆简单点,把os或用户程序剩余代码加载到内存,完成重定位,再跳到这些代码执行,具体代码如下:
1、MBR代码
;MBR 主引导扇区
;开机上电后,BIOS会自动从0x7c00处执行
lba_num equ 100;一共101个扇区,用户程序在硬盘中的逻辑扇区号
SECTION mbr vstart=0x7c00 align=16;以16位对齐
;cs和IP已经运行到这里,不用再设置了
mov ax,0
mov ss,ax;堆栈段从0开始
mov sp,ax;
mov ax,[cs:phy_address];目前在cs段,如果不写,默认读ds段;
mov dx,[cs:phy_address+0x2]
mov bx,0x10
div bx;相当于右移4bit,得到0x1000,就是段地址,放在ax
mov ds,ax;
xor bx,bx;
mov si,lba_num
xor di,di
call read_disk;先读第一个扇区,把用户程序的头部加载到内存,才能得到重定位表
mov ax,[0];program_len分别放在ax和bx;
mov dx,[2];从内存读数据,不加段前缀的默认是ds;
mov bx,512
div bx;ax = 用户程序的扇区个数 dx=扇区余数,也就是最后不满一个扇区内偏移
cmp dx,0; test dx,dx
jnz cantDiv;不能被整除,说明有数据不满一个扇区的数据,但也要占用一个扇区的空间
dec ax;扇区数减一:前面已经读了一个扇区。
cantDiv:
cmp ax,0;已经读完了,可以直接重定位
jz realloc;
mov cx,ax;剩余扇区数放入cx,方便后续loop
push ds
Continue_Read:
inc si
mov ax,ds
add ax,0x20;基址增加0x20,相当于增加512byte,比如:ds:bx = 0000:0000 = 00000; ds:bx = 0020:0000 = 0200+0000=0x0200=512byte
mov ds,ax;往高地址挪一个扇区512byte
xor bx,bx;偏移清零,通过段基址挪动
call read_disk;相当于寄存器传参
loop Continue_Read
pop ds
;---------------上面都是把数据从磁盘读到内存,下面开始重定位------------------------------------
;先计算出用户程序code_entry在内存的绝对地址
realloc:
mov ax,[0x06];默认是ds段,此时已是0x1000;code_entry的section.code1.start低2字节
mov dx,[0x08];code_entry的section.code1.start高2字节
call reallocaddress
mov [0x06],ax;把内存中的物理地址写回去,这次得到绝对物理地址了;
;mov [0x06],ds
mov cx,[0x0a];5个段需要重定位
mov bx,0x0c;
;用户程序每个section都计算出内存的绝对地址,然后写回去
reallocLoop:
mov ax,[bx]
mov dx,[bx+2]
call reallocaddress
mov [bx],ax;
add bx,4
loop reallocLoop
jmp far [0x04];内存操作默认以ds基址,这里是0x10000;跳转到用户程序start变量地址
;mov ax, [0x04];得到offset,就是start的偏移地址
;jmp 0x1000:ax
;dx:ax 32位偏移地址,寄存器传参
;输出16位段基址,保存在ax
reallocaddress:
push dx
add ax,[cs:phy_address];注意:目前在cs段,不加从内存读数据默认用ds,此处为用户程序;ax=0x0000+[0x10006]=0x0020;
add dx,[cs:phy_address+0x2];dx=0x0001
shr ax,4;低16位地址的低4位去掉,高4位补零,得到段基址;ax=0x0002
ror dx,4;高16位地址循环右移;dx=0x1000
and dx,0xf000;取出最需要的4bit,其他清零;dx=0x1000
or ax,dx;ax=0x1002
pop dx
ret
;ds:bx 从硬盘读数据到该物理地址
;di, si 是逻辑扇区号:逻辑扇区只用28位,所以di有4位是不用的;si是逻辑扇区低16位
;可以通过int 0x13中断读取,也可以通过磁盘控制器读取;
read_disk:
push ax
push bx
push cx
push dx
push si
push di
;https://www.cnblogs.com/mlzrq/p/10223060.html 详细说明
mov dx,0x1f2;磁盘端口,指定读取或写入的扇区数
mov al,1;每次读一个扇区
out dx,al;往端口写入数据
inc dx;0x1f3 lba地址的低8位,就是0-7位
mov ax,si;
out dx,al;先把低8位写入端口,因为用户程序被写入了磁盘100号扇区,所以调用函数传参数di=100
inc dx;0x1f4 lba地址的中8位,就是8-15位
mov al,ah
out dx,al;
inc dx;0x1f5 lba地址的高8位,就是16-23位
mov ax,di;
out dx,al;
;上面3个已经把前面24位填满,这里填最高4位
inc dx;0x1f6 lba地址的前4位,就是24-27位
mov al,0xe0; 高4位是各种标志位: 0 CHS,1 LBA; 1; 0 从 1 主; 0; 这里是e;
or al,ah
out dx,al
inc dx;0x1f7
mov al,0x20;发送读扇区的请求:0x20
out dx,al
;------------------------------ and al,0x88 逻辑上出错,先屏蔽试试
waits:
in al,dx; 从0x1f7读取磁盘状态,一共有8位;第7位:1表示busy 第3位:1表示准备好读写操作,所以在0xxx1xxx的时候才能读写,其他状态都不行;
and al,0x88;第7位和第3位保持不变,其他清零
cmp al,0x08;
jnz waits;状态不等于0x08,说明没准备好,继续等待
mov dx,0x01f0;数据端口,16位,需要ax接数据;每个扇区512byte,每次读2byte,要读256次
mov cx,256;
;准备好了,开始读磁盘
readw:
in ax,dx;
mov [bx],ax;
add bx,2;每次读2byte
loop readw;
pop di
pop si
pop dx
pop cx
pop bx
pop ax
ret
phy_address dd 0x10000;用户程序拷贝到内存地址
times 510 - ($-$$) db 0;
dw 0xaa552、用户程序
;用户程序
;段的数目并未限制,用户可根据需求自行创建
;-------------------------------------------------------------------------------
SECTION header vstart=0;vstart=0连着写,不能有空格
program_len dd program_end;
code_entry dw start;变量偏移0x4;
dd section.code1.start;code1段基址:变量偏移0x6
reallocate_item dw (header_end-code1Segment)/4 ;每个段偏移都是dd=4byte,变量偏移0xa
;重定位表,记录重要段相对于程序起始位置的偏移
code1Segment dd section.code1.start;变量偏移0xc
data1Segment dd section.data1.start;变量偏移0x10 本section在文件中的真实偏移量(真实地址),或则说相对开始的偏移地址
stack1Segment dd section.stack1.start;变量偏移0x14
use1Segment dd section.use1.start;变量偏移0x18
use1DataSegment dd section.use1Data.start;变量偏移0x1c
header_end: ;有vstart = 0,header_end从vstart = 0开始算偏移
;-------------------------------------------------------------------------------
SECTION use1 align=16 vstart=0;
;-------------------------------------------------------------------------------
SECTION use1Data align=16 vstart=0;
use1Data_end:
;-------------------------------------------------------------------------------
SECTION code1 align=16 vstart=0; ;vstart=0连这些,不能有空格
;直接调用BIOS例程在显示器打印
start:
mov ax,[stack1Segment];初始化堆栈
mov ss,ax
mov ax,stacker_pointer;
mov sp,ax;
xor ah,ah
mov al,0x03
int 0x10;调用bios的0x10号中断清屏
;AL=写模式,BH=页码,BL=颜色,CX=字符串长度,DH=行,DL=列,ES:BP=字符串偏移量
;https://zh.wikipedia.org/wiki/INT_10H 有详细说明
mov ah,0x13
mov al,1
xor bh,bh
mov bl,0x04
mov cx, data1_end - msg;cx保存字符串长度
mov dh,12;显示的行号
mov dl,25;显示的列号
mov bp,msg; es:bp指向需要打印的字符串
push ax
mov ax,[data1Segment]
;mov ax,cs;
mov es,ax;es:bp 为串首地址
pop ax
int 0x10
hlt;程序待机
;-------------------------------------------------------------------------------
SECTION data1 align=16 vstart=0
msg db 'are you ready?', 0
data1_end:
;-------------------------------------------------------------------------------
SECTION stack1 align=16 vstart=0;
resb 256; reserve byte,保留/分配256byte空间
stacker_pointer: ;栈底放在高地址
;-------------------------------------------------------------------------------
SECTION tail align=16; 这个段没有vstart = 0,那就从开头计算偏移,也就是SECTION header开始算;
program_end:说明: (1)SECTION用于定于段,没有数量限制,开发人员可根据需求取舍
(2)vstart=0表示该段内的标识都从0开始计算偏移。如果没有 vstart=0,那么段内标识比如msg、start等都从程序开始处计算偏移;
vstart=0千万要紧挨着,不能有个空格,不能有个空格,不能有个空格,重要的事情说三遍。否则这种声明无效,段内标识的偏移还是会从程序开头处计算,导致后续逻辑出错
(3)段的数量没限制,但是建议把代码段和数据段分开,各种变量尽量在数据段声明;代码段声明的变量因未隔离开,容易被cpu当成代码执行,导致异常或逻辑错乱
(4)MBR为什么要用0xaa55了? 0xaa55=0b 1010 1010 0101 0101,看出来有啥特点了么? 0和1交叉呈现,就像梳子一样;奇偶校验总是为偶数,a和5与是0,或是f;
1、去年逆向x音15.5.0版本时,可以直接用fiddler抓包。后来貌似升级到17版本时fiddler就抓不到包了,看雪有大佬破解了x音防抓包的功能,原理并不复杂:boringssl源码中有个SSL_CTX_set_custom_verify函数,定义如下:
void SSL_CTX_set_custom_verify(
SSL_CTX *ctx, int mode,
enum ssl_verify_result_t (*callback)(SSL *ssl, uint8_t *out_alert)) {
ctx->verify_mode = mode;
ctx->custom_verify_callback = callback;
}(1)第二个mode参数就是验证client的关键参数了,有以下4种取值:
// SSL_VERIFY_NONE, on a client, verifies the server certificate but does not
// make errors fatal. The result may be checked with |SSL_get_verify_result|. On
// a server it does not request a client certificate. This is the default.
#define SSL_VERIFY_NONE 0x00
// SSL_VERIFY_PEER, on a client, makes server certificate errors fatal. On a
// server it requests a client certificate and makes errors fatal. However,
// anonymous clients are still allowed. See
// |SSL_VERIFY_FAIL_IF_NO_PEER_CERT|.
#define SSL_VERIFY_PEER 0x01
// SSL_VERIFY_FAIL_IF_NO_PEER_CERT configures a server to reject connections if
// the client declines to send a certificate. This flag must be used together
// with |SSL_VERIFY_PEER|, otherwise it won't work.
#define SSL_VERIFY_FAIL_IF_NO_PEER_CERT 0x02
// SSL_VERIFY_PEER_IF_NO_OBC configures a server to request a client certificate
// if and only if Channel ID is not negotiated.
#define SSL_VERIFY_PEER_IF_NO_OBC 0x04从注释就能看出:
0x00:client要验证server的证书,但是不会报错;server不会要求client提供证书,这也是默认的参数
0x01:client和server双方都要验证对方的证书,并且会报错
0x02:如果client不提供证书,server可以拒绝连接;这个取值要和SSL_VERIFY_PEER一起配合使用,否则无效
0x04:server向client索要证书
x音默认情况下不能抓包是因为这个参数取值不是0x00,所以直接用frida hook SSL_CTX_set_custom_verify这个函数,把第二个参数改成0x00即可!也可以直接找到libttboringssl.so的源码把第二个参数写死成0x00;
(2)第三个参数callback从名字看就知道是个回调函数,函数返回值ssl_verify_result_t取值如下:
enum ssl_verify_result_t BORINGSSL_ENUM_INT {
ssl_verify_ok,
ssl_verify_invalid,
ssl_verify_retry,
};从名字也能看出来返回值取第一个表示验证通过!直接通过hook把第三个参数改成0即可!如果觉得用frida hook麻烦,也可以在libsscronet.so偏移0x1CCBBE处,把“movs R0,1”改成“moves R0,0”即可!也就是把返回值从1改成0!
2、为了更好的逆向和ssl相关的功能(抓包、加解密等),有必要了解一些ssl的关键函数!
(1) 站在逆向的角度,我个人觉得最最最重要的就是SSL_write函数了,定义如下:从函数名和参数就能看出是从ssl发送buf的数据,发送长度是num!发送的数据存放在buf的,直接hook这个函数打印buf是不是就能看到网络数据了?
/*num字节从缓冲区buf写入指定的ssl连接*/
int SSL_write(SSL *ssl, const void *buf, int num) {
ssl_reset_error_state(ssl);
if (ssl->quic_method != nullptr) {
OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return -1;
}
if (ssl->do_handshake == NULL) {
OPENSSL_PUT_ERROR(SSL, SSL_R_UNINITIALIZED);
return -1;
}
int ret = 0;
bool needs_handshake = false;
do {
// If necessary, complete the handshake implicitly.
if (!ssl_can_write(ssl)) {//如果还不能通过这个ssl写数据
ret = SSL_do_handshake(ssl);//开始握手
if (ret < 0) {
return ret;
}
if (ret == 0) {
OPENSSL_PUT_ERROR(SSL, SSL_R_SSL_HANDSHAKE_FAILURE);
return -1;
}
}
//从这里发数据
ret = ssl->method->write_app_data(ssl, &needs_handshake,
(const uint8_t *)buf, num);
} while (needs_handshake);
return ret;
}(2)既然SSL_write是发数据的,SSL_read岂不就是读数据的?代码如下:
int SSL_read(SSL *ssl, void *buf, int num) {
int ret = SSL_peek(ssl, buf, num);
if (ret <= 0) {
return ret;
}
// TODO(davidben): In DTLS, should the rest of the record be discarded? DTLS
// is not a stream. See https://crbug.com/boringssl/65.
ssl->s3->pending_app_data =
ssl->s3->pending_app_data.subspan(static_cast<size_t>(ret));
if (ssl->s3->pending_app_data.empty()) {
ssl->s3->read_buffer.DiscardConsumed();
}
return ret;
}代码很简单,对于逆向人员来说,hook这两个函数是可以获取发送和接收数据的,也就是绕开了证书校验,对部分app是有用的!详细代码可以参考文章末尾第4个链接!
(3)通信双方最重要的莫过于密钥的协商了,handshake最重要的就是干这个的,整个方法如下;handshake内部最重要的又莫过于change_cipher_spec:为了保证安全,通信双方每隔一段时间就会改变加解密的参数!
int ssl_run_handshake(SSL_HANDSHAKE *hs, bool *out_early_return) {
SSL *const ssl = hs->ssl;
for (;;) {
// Resolve the operation the handshake was waiting on. Each condition may
// halt the handshake by returning, or continue executing if the handshake
// may immediately proceed. Cases which halt the handshake can clear
// |hs->wait| to re-enter the state machine on the next iteration, or leave
// it set to keep the condition sticky.
/*handshake等待时可能有很多种情况:*/
switch (hs->wait) {
case ssl_hs_error://报错提示
ERR_restore_state(hs->error.get());
return -1;
case ssl_hs_flush: {//刷新缓存?
int ret = ssl->method->flush_flight(ssl);
if (ret <= 0) {
return ret;
}
break;
}
case ssl_hs_read_server_hello:
case ssl_hs_read_message:
/*为保证安全,每隔一段时间就需要改变加解密参数*/
case ssl_hs_read_change_cipher_spec: {
if (ssl->quic_method) {//双方用quic协议
// QUIC has no ChangeCipherSpec messages.
//quic本身比较简单,就没有改变加解密参数的说法
assert(hs->wait != ssl_hs_read_change_cipher_spec);
// The caller should call |SSL_provide_quic_data|. Clear |hs->wait| so
// the handshake can check if there is sufficient data next iteration.
ssl->s3->rwstate = SSL_ERROR_WANT_READ;
hs->wait = ssl_hs_ok;
return -1;
}
uint8_t alert = SSL_AD_DECODE_ERROR;
size_t consumed = 0;
ssl_open_record_t ret;
//现在的状态是要改变加解密参数
if (hs->wait == ssl_hs_read_change_cipher_spec) {
//开始和对方协商改变加解密参数
ret = ssl_open_change_cipher_spec(ssl, &consumed, &alert,
ssl->s3->read_buffer.span());
} else {
/*否则重新handshake;其实handshake的本质就是协商加解密协议和参数,
目的和change_cipher_spec没本质区别*/
ret = ssl_open_handshake(ssl, &consumed, &alert,
ssl->s3->read_buffer.span());
}
if (ret == ssl_open_record_error &&
hs->wait == ssl_hs_read_server_hello) {
uint32_t err = ERR_peek_error();
if (ERR_GET_LIB(err) == ERR_LIB_SSL &&
ERR_GET_REASON(err) == SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE) {
// Add a dedicated error code to the queue for a handshake_failure
// alert in response to ClientHello. This matches NSS's client
// behavior and gives a better error on a (probable) failure to
// negotiate initial parameters. Note: this error code comes after
// the original one.
//
// See https://crbug.com/446505.
OPENSSL_PUT_ERROR(SSL, SSL_R_HANDSHAKE_FAILURE_ON_CLIENT_HELLO);
}
}
bool retry;
int bio_ret = ssl_handle_open_record(ssl, &retry, ret, consumed, alert);
if (bio_ret <= 0) {
return bio_ret;
}
if (retry) {
continue;
}
ssl->s3->read_buffer.DiscardConsumed();
break;
}
case ssl_hs_read_end_of_early_data: {
if (ssl->s3->hs->can_early_read) {
// While we are processing early data, the handshake returns early.
*out_early_return = true;
return 1;
}
hs->wait = ssl_hs_ok;
break;
}
case ssl_hs_certificate_selection_pending:
ssl->s3->rwstate = SSL_ERROR_PENDING_CERTIFICATE;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_handoff:
ssl->s3->rwstate = SSL_ERROR_HANDOFF;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_handback: {
int ret = ssl->method->flush_flight(ssl);
if (ret <= 0) {
return ret;
}
ssl->s3->rwstate = SSL_ERROR_HANDBACK;
hs->wait = ssl_hs_handback;
return -1;
}
// The following cases are associated with callback APIs which expect to
// be called each time the state machine runs. Thus they set |hs->wait|
// to |ssl_hs_ok| so that, next time, we re-enter the state machine and
// call the callback again.
case ssl_hs_x509_lookup:
ssl->s3->rwstate = SSL_ERROR_WANT_X509_LOOKUP;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_private_key_operation:
ssl->s3->rwstate = SSL_ERROR_WANT_PRIVATE_KEY_OPERATION;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_pending_session:
ssl->s3->rwstate = SSL_ERROR_PENDING_SESSION;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_pending_ticket:
ssl->s3->rwstate = SSL_ERROR_PENDING_TICKET;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_certificate_verify:
ssl->s3->rwstate = SSL_ERROR_WANT_CERTIFICATE_VERIFY;
hs->wait = ssl_hs_ok;//握手已成功
return -1;
case ssl_hs_early_data_rejected:
assert(ssl->s3->early_data_reason != ssl_early_data_unknown);
assert(!hs->can_early_write);
ssl->s3->rwstate = SSL_ERROR_EARLY_DATA_REJECTED;
return -1;
case ssl_hs_early_return:
if (!ssl->server) {
// On ECH reject, the handshake should never complete.
assert(ssl->s3->ech_status != ssl_ech_rejected);
}
*out_early_return = true;
hs->wait = ssl_hs_ok;
return 1;
case ssl_hs_hints_ready:
ssl->s3->rwstate = SSL_ERROR_HANDSHAKE_HINTS_READY;
return -1;
case ssl_hs_ok:
break;
}
// Run the state machine again.
hs->wait = ssl->do_handshake(hs);
if (hs->wait == ssl_hs_error) {
hs->error.reset(ERR_save_state());
return -1;
}
if (hs->wait == ssl_hs_ok) {
if (!ssl->server) {
// On ECH reject, the handshake should never complete.
assert(ssl->s3->ech_status != ssl_ech_rejected);
}
// The handshake has completed.
*out_early_return = false;
return 1;
}
// Otherwise, loop to the beginning and resolve what was blocking the
// handshake.
}
}(4)还有个很不起眼、容易被忽视的函数:
void SSL_CTX_set_keylog_callback(SSL_CTX *ctx,
void (*cb)(const SSL *ssl, const char *line)) {
ctx->keylog_callback = cb;
}从名字就能看出来是存放key日志的,里面记录的全是ssl协商的密钥!有了这些密钥,是不是就能解密双方通信的数据了?https://www.cnblogs.com/theseventhson/p/14618157.html 这是我之前在PC上用浏览器打开网页时做的操作,记录了ssl协议双方协商的密钥,然后wireshark就能用这些密钥解密数据了!但在android上默认是不记录这些的,需要手动hook来记录,js脚本代码如下:
function startTLSKeyLogger(SSL_CTX_new, SSL_CTX_set_keylog_callback) {
function keyLogger(ssl, line) {
console.log(new NativePointer(line).readCString());
}
const keyLogCallback = new NativeCallback(keyLogger, 'void', ['pointer', 'pointer']);
Interceptor.attach(SSL_CTX_new, {
onLeave: function(retval) {
const ssl = new NativePointer(retval);
const SSL_CTX_set_keylog_callbackFn = new NativeFunction(SSL_CTX_set_keylog_callback, 'void', ['pointer', 'pointer']);
SSL_CTX_set_keylog_callbackFn(ssl, keyLogCallback);
}
});
}
startTLSKeyLogger(
Module.findExportByName('libssl.so', 'SSL_CTX_new'),
Module.findExportByName('libssl.so', 'SSL_CTX_set_keylog_callback')
) 这是我hook x音的结果:
注意:这里抓的是libssl.so的keylog函数,也可以把libssl.so换成libttboringssl.so去获取x音的sslkey!具体操作方式可以参考文章末尾第6个!
参考:
1、https://bbs.pediy.com/thread-267940.htm android抓包整理归纳
2、https://onejane.github.io/2021/05/06/frida%E6%B2%99%E7%AE%B1%E8%87%AA%E5%90%90%E5%AE%9E%E7%8E%B0/#AOSP%E7%BD%91%E7%BB%9C%E5%BA%93%E8%87%AA%E5%90%90 frida沙箱自吐实现
3、https://bbs.pediy.com/thread-268014.htm 绕过非标准http框架和非系统ssl库app的sslpinning
4、https://blog.csdn.net/tzwsoho/article/details/119346275 [frida]拦截SSL_read/SSL_write函数获得HTTPS请求和响应
5、http://buaq.net/go-29171.html 关于抓包碎碎念
6、http://www.zhuoyue360.com/crack/73.html android硬核抓包
dpdk是intel主导开发的网络编程框架, 有这么多的优点,都是怎么实现的了?
1、UIO原理:dpdk绕过了操作系统内核,直接接管网卡,用户程序可以直接在3环读写网卡的数据,这就涉及到两个关键技术点了:
地址映射:3环的程序是怎么定位到网卡数据存放在哪的了?
拦截硬件中断:传统数据处理流程是网卡收到数据后通过硬件中断通知cpu来取数据,3环的程序肯定要拦截这个中断,然后通过轮询方式取数据,这个又是怎么实现的了?
(1)地址映射:3环程序最常使用的就是内存地址了,一共32或64bit;C/C++层面可以通过指针直接读写地址的值;除了内存,还有很多设备也需要和cpu交互数据,比如显示器:要在屏幕显示的内容肯定是需要用户指定的,用户程序可以把显示的内容发送到显示器指定的地方,然后再屏幕打印出来。为了方便用户程序发送数据,硬件层面会把显示器的部分存储空间映射到内存地址,做到了和内存条硬件的寻址方式一样,用户也可以直接通过指针往这里写数据(汇编层面直接通过mov指令操作即可)!网卡也类似:网卡是插在pci插槽的,网卡(或者说pci插槽)的存储空间也会映射到内存地址,应用程序读写这块物理地址就等同于读写网卡的存储空间!实际写代码时,由于要深入驱动,pci网卡预留物理的内存与io空间会保存到uio设备上,相当于将这些物理空间与io空间暴露给uio设备,应用程序访问这些uio设备即可!几个关键的函数如下:
将pci网卡的物理内存空间以及io空间保存在uio设备结构struct uio_info中的mem成员以及port成员中,uio设备就知道了网卡的物理以及io空间。应用层访问这个uio设备的物理空间以及io空间,就相当于访问pci设备的物理以及io空间;本质上就是将pci网卡的空间暴露给uio设备。
int igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
//将pci内存,端口映射给uio设备
struct rte_uio_pci_dev *udev;
err = igbuio_setup_bars(dev, &udev->info);
}
static int igbuio_setup_bars(struct pci_dev *dev, struct uio_info *info)
{
//pci内存,端口映射给uio设备
for (i = 0; i != sizeof(bar_names) / sizeof(bar_names[0]); i++)
{
if (pci_resource_len(dev, i) != 0 && pci_resource_start(dev, i) != 0)
{
flags = pci_resource_flags(dev, i);
if (flags & IORESOURCE_MEM)
{
//暴露pci的内存空间给uio设备
ret = igbuio_pci_setup_iomem(dev, info, iom, i, bar_names[i]);
}
else if (flags & IORESOURCE_IO)
{
//暴露pci的io空间给uio设备
ret = igbuio_pci_setup_ioport(dev, info, iop, i, bar_names[i]);
}
}
}
}(2)拦截硬件中断:为了减掉内核中冗余的数据处理流程,应用程序要hook网卡的中断,从源头开始拦截网卡数据!当硬件中断触发时,才不会一直触发内核去执行中断回调。也就是通过这种方式,才能在应用层实现硬件中断处理过程。注意:这里说的中断仅是控制中断,而不是报文收发的数据中断,数据中断是不会走到这里来的,因为在pmd开启中断时,没有设置收发报文的中断掩码,只注册了网卡状态改变的中断掩码;hook中断的代码如下:
int igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
//填充uio信息
udev->info.name = "igb_uio";
udev->info.version = "0.1";
udev->info.handler = igbuio_pci_irqhandler; //硬件控制中断的入口,劫持原来的硬件中断
udev->info.irqcontrol = igbuio_pci_irqcontrol; //应用层开关中断时被调用,用于是否开始中断
}
static irqreturn_t igbuio_pci_irqhandler(int irq, struct uio_info *info)
{
if (udev->mode == RTE_INTR_MODE_LEGACY && !pci_check_and_mask_intx(udev->pdev))
{
return IRQ_NONE;
}
//返回IRQ_HANDLED时,linux uio框架会唤醒等待uio中断的进程。注册到epoll的uio中断事件就会被调度
/* Message signal mode, no share IRQ and automasked */
return IRQ_HANDLED;
}
static int igbuio_pci_irqcontrol(struct uio_info *info, s32 irq_state)
{
//调用内核的api来开关中断
if (udev->mode == RTE_INTR_MODE_LEGACY)
{
pci_intx(pdev, !!irq_state);
}
else if (udev->mode == RTE_INTR_MODE_MSIX)\
{
list_for_each_entry(desc, &pdev->msi_list, list)
igbuio_msix_mask_irq(desc, irq_state);
}
}2、内存池:传统应用要使用内存时,一般都是调用malloc让操作系统在堆上分配。这样做有两点弊端:
进入内核要切换上下文
操作系统通过buddy&slab算法找合适的空闲内存
所以频繁调用malloc会严重拉低效率!如果不频繁调用malloc,怎么处理频繁收到和需要发送的报文数据了?dpdk采用的是内存池的技术:即在huge page内存中开辟一个连续的大缓冲区当做内存池!同时提供rte_mempool_get从内存池中获取内存空间。也可调用rte_mempool_put将不再使用的内存空间放回到内存池中。从这里就能看出:dpdk自己从huge page处维护了一大块内存供应用程序使用,应用程序不再需要通过系统调用从操作系统申请内存了!
(1)内存池的创建,在rte_mempool_create接口中完成。这个接口主要是在大页内存中开辟一个连续的大缓冲区当做内存池,然后将这个内存池进行分割,头部为struct rte_mempool内存池结构; 紧接着是内存池的私有结构大小,这个由应用层自己设置,每个创建内存池的应用进程都可以指定不同的私有结构; 最后是多个连续的对象元素,这些对象元素都是处于同一个内存池中。每个对象元素又有对象的头部,对象的真实数据区域,对象的尾部组成。这里所说的对象元素,其实就是应用层要开辟的真实数据空间,例如应用层自己定义的结构体变量等;本质上是dpdk自己实现了一套内存的管理办法,其作用和linux的buddy&slab是一样的,没本质区别!整个内存池图示如下:
每创建一个内存池,都会创建一个链表节点,然后插入到链表中,因此这个链表记录着当前系统创建了多少内存池。核心代码如下:
//创建内存池链表节点
te = rte_zmalloc("MEMPOOL_TAILQ_ENTRY", sizeof(*te), 0);
//内存池链表节点插入到内存池链表中
te->data = (void *) mp;
RTE_EAL_TAILQ_INSERT_TAIL(RTE_TAILQ_MEMPOOL, rte_mempool_list, te);
所以说内存池可能不止1个,会有多个!在内存池中,内存被划分成了N多的对象。应用程序要申请内存时,怎么知道哪些对象空闲可以用,哪些对象已经被占用了?当对象元素初始化完成后,会把对象指针放入ring队列,所以说ring队列的所有对象指针都是可以使用的!应用程序要申请内存时,可以调用rte_mempool_get接口从ring队列中获取,也就是出队; 使用完毕后调用rte_mempool_put将内存释放回收时,也是将要回收的内存空间对应的对象指针放到这个ring队列中,也就是入队!
(2)具体分配内存时的步骤:
现代cpu基本都是多核的,多个cpu同时在内存池申请内存时无法避免涉及到互斥,会在一定程度上影响分配的效率,所以每个cpu自己都有自己的“自留地”,会优先在自己的“自留地”申请内存;
如果“自留地”的内存已耗尽,才会继续去内存池申请内存!核心代码如下:
int rte_mempool_get(struct rte_mempool *mp, void **obj_table, unsigned n)
{
#if RTE_MEMPOOL_CACHE_MAX_SIZE > 0
//从当前cpu应用层缓冲区中获取
cache = &mp->local_cache[lcore_id];
cache_objs = cache->objs;
for (index = 0, len = cache->len - 1; index < n; ++index, len--, obj_table++)
{
*obj_table = cache_objs[len];
}
return 0;
#endif
/* get remaining objects from ring */
//直接从ring队列中获取
ret = rte_ring_sc_dequeue_bulk(mp->ring, obj_table, n);
}释放内存的步骤和申请类似:
先查看cpu的“自留地”是否还有空间。如果有,就先把释放的对象指针放在“自留地”;
如果“自留地”没空间了,再把释放的对象指针放在内存池!核心代码如下:
int rte_mempool_put(struct rte_mempool *mp, void **obj_table, unsigned n)
{
#if RTE_MEMPOOL_CACHE_MAX_SIZE > 0
//在当前cpu本地缓存有空间的场景下, 先放回到本地缓存。
cache = &mp->local_cache[lcore_id];
cache_objs = &cache->objs[cache->len];
for (index = 0; index < n; ++index, obj_table++)
{
cache_objs[index] = *obj_table;
}
//缓冲达到阈值,刷到队列中
if (cache->len >= flushthresh)
{
rte_ring_mp_enqueue_bulk(mp->ring, &cache->objs[cache_size], cache->len - cache_size);
cache->len = cache_size;
}
return 0
#endif
//直接放回到ring队列
rte_ring_sp_enqueue_bulk(mp->ring, obj_table, n);
}注意:这里的ring是环形无锁队列!
3、Poll mode driver: 不论何总形式的io,接收方获取数据的方式有两种:
被动接收中断的唤醒:典型如网卡收到数据,通过硬件中断通知操作系统去处理;操作系统收到数据后会唤醒休眠的进程继续处理数据
轮询 poll:写个死循环不停的检查内存地址是否有新数据到了!
在 x86 体系结构中,一次中断处理需要将 CPU 的状态寄存器保存到堆栈,并运行中断handler,最后再将保存的状态寄存器信息从堆栈中恢复,整个过程需要至少 300 个处理器时钟周期!所以dpdk果断抛弃了中断,转而使用轮询方式!整个流程大致是这样的:内核态的UIO Driver hook了网卡发出的中断信号,然后由用户态的 PMD Driver 采用主动轮询的方式。除了链路状态通知仍必须采用中断方式以外(因为网卡发出硬件中断才能触发执行hook代码的嘛,这个容易理解吧?),均使用无中断方式直接操作网卡设备的接收和发送队列。整体流程大致如下:UIO hook了网卡的中断,网卡收到数据后“被迫”执行hook代码!先是通过UIO把网卡的存储地址映射到/dev/uio文件,而后应用程序通过PMD轮询检查文件是否有新数据到来!期间也使用mmap把应用的虚拟地址映射到网卡的物理地址,减少数据的拷贝转移!
总的来说:UIO+PMD,前者旁路了内核,后者主动轮询避免了硬中断,DPDK 从而可以在用户态进行收发包的处理。带来了零拷贝(Zero Copy)、无系统调用(System call)的优化。同时,还避免了软中断的异步处理,也减少了上下文切换带来的 Cache Miss!轮询收报核心代码如下:
/*PMD轮询接收数据包*/
uint16_t
eth_em_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts,
uint16_t nb_pkts)
{
/* volatile防止编译器优化,每次使用必须又一次从memory中取而不是用寄存器的值 */
volatile struct e1000_rx_desc *rx_ring;
volatile struct e1000_rx_desc *rxdp;//指向rx ring中某个e1000_rx_desc描述符
struct em_rx_queue *rxq;//整个接收队列
struct em_rx_entry *sw_ring;//指向描述符队列的头部,根据rx tail来偏移
struct em_rx_entry *rxe;//指向sw ring中具体的entry
struct rte_mbuf *rxm;//entry里的rte mbuf
/*是new mbuf,新申请的mbuf,当rxm从ring中取出后,需要用nmb再挂上去,
更新对应rx ring和sw ring中的值,为下一次收包做准备*/
struct rte_mbuf *nmb;
struct e1000_rx_desc rxd;//具体的非指针描述符
uint64_t dma_addr;
uint16_t pkt_len;
uint16_t rx_id;
uint16_t nb_rx;
uint16_t nb_hold;
uint8_t status;
rxq = rx_queue;
nb_rx = 0;
nb_hold = 0;
//初始化临时变量,要开始遍历队列了
rx_id = rxq->rx_tail;
rx_ring = rxq->rx_ring;
sw_ring = rxq->sw_ring;
/* 一次性收32个报文 */
while (nb_rx < nb_pkts) {
/*
* The order of operations here is important as the DD status
* bit must not be read after any other descriptor fields.
* rx_ring and rxdp are pointing to volatile data so the order
* of accesses cannot be reordered by the compiler. If they were
* not volatile, they could be reordered which could lead to
* using invalid descriptor fields when read from rxd.
*/
/* 当前报文的descriptor */
rxdp = &rx_ring[rx_id];
status = rxdp->status; /* 结束标记,必须首先读取 */
/*检查状态是否为dd, 不是则说明驱动还没有把报文放到接收队列,直接退出*/
if (! (status & E1000_RXD_STAT_DD))
break;
rxd = *rxdp; /* 复制一份 */
/*
* End of packet.
*
* If the E1000_RXD_STAT_EOP flag is not set, the RX packet is
* likely to be invalid and to be dropped by the various
* validation checks performed by the network stack.
*
* Allocate a new mbuf to replenish the RX ring descriptor.
* If the allocation fails:
* - arrange for that RX descriptor to be the first one
* being parsed the next time the receive function is
* invoked [on the same queue].
*
* - Stop parsing the RX ring and return immediately.
*
* This policy do not drop the packet received in the RX
* descriptor for which the allocation of a new mbuf failed.
* Thus, it allows that packet to be later retrieved if
* mbuf have been freed in the mean time.
* As a side effect, holding RX descriptors instead of
* systematically giving them back to the NIC may lead to
* RX ring exhaustion situations.
* However, the NIC can gracefully prevent such situations
* to happen by sending specific "back-pressure" flow control
* frames to its peer(s).
*/
PMD_RX_LOG(DEBUG, "port_id=%u queue_id=%u rx_id=%u "
"status=0x%x pkt_len=%u",
(unsigned) rxq->port_id, (unsigned) rxq->queue_id,
(unsigned) rx_id, (unsigned) status,
(unsigned) rte_le_to_cpu_16(rxd.length));
nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
if (nmb == NULL) {
PMD_RX_LOG(DEBUG, "RX mbuf alloc failed port_id=%u "
"queue_id=%u",
(unsigned) rxq->port_id,
(unsigned) rxq->queue_id);
rte_eth_devices[rxq->port_id].data->rx_mbuf_alloc_failed++;
break;
}
/* 表示当前descriptor被上层软件占用 */
nb_hold++;
/* 当前收到的mbuf */
rxe = &sw_ring[rx_id];
/* 收包位置,假设超过环状数组则回滚 */
rx_id++;
if (rx_id == rxq->nb_rx_desc)
rx_id = 0;
/* mbuf加载cache下次循环使用 */
/* Prefetch next mbuf while processing current one. */
rte_em_prefetch(sw_ring[rx_id].mbuf);
/*
* When next RX descriptor is on a cache-line boundary,
* prefetch the next 4 RX descriptors and the next 8 pointers
* to mbufs.
*/
/* 取下一个descriptor,以及mbuf指针下次循环使用 */
/* 一个cache line是4个descriptor大小(64字节) */
if ((rx_id & 0x3) == 0) {
rte_em_prefetch(&rx_ring[rx_id]);
rte_em_prefetch(&sw_ring[rx_id]);
}
/* Rearm RXD: attach new mbuf and reset status to zero. */
rxm = rxe->mbuf;
rxe->mbuf = nmb;
dma_addr =
rte_cpu_to_le_64(rte_mbuf_data_iova_default(nmb));
rxdp->buffer_addr = dma_addr;
rxdp->status = 0;/* 重置当前descriptor的status */
/*
* Initialize the returned mbuf.
* 1) setup generic mbuf fields:
* - number of segments,
* - next segment,
* - packet length,
* - RX port identifier.
* 2) integrate hardware offload data, if any:
* - RSS flag & hash,
* - IP checksum flag,
* - VLAN TCI, if any,
* - error flags.
*/
pkt_len = (uint16_t) (rte_le_to_cpu_16(rxd.length) -
rxq->crc_len);
rxm->data_off = RTE_PKTMBUF_HEADROOM;
rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off);
rxm->nb_segs = 1;
rxm->next = NULL;
rxm->pkt_len = pkt_len;
rxm->data_len = pkt_len;
rxm->port = rxq->port_id;
rxm->ol_flags = rx_desc_status_to_pkt_flags(status);
rxm->ol_flags = rxm->ol_flags |
rx_desc_error_to_pkt_flags(rxd.errors);
/* Only valid if PKT_RX_VLAN set in pkt_flags */
rxm->vlan_tci = rte_le_to_cpu_16(rxd.special);
/*
* Store the mbuf address into the next entry of the array
* of returned packets.
*/
/* 把收到的mbuf返回给用户 */
rx_pkts[nb_rx++] = rxm;
}
/* 收包位置更新 */
rxq->rx_tail = rx_id;
/*
* If the number of free RX descriptors is greater than the RX free
* threshold of the queue, advance the Receive Descriptor Tail (RDT)
* register.
* Update the RDT with the value of the last processed RX descriptor
* minus 1, to guarantee that the RDT register is never equal to the
* RDH register, which creates a "full" ring situtation from the
* hardware point of view...
*/
nb_hold = (uint16_t) (nb_hold + rxq->nb_rx_hold);
if (nb_hold > rxq->rx_free_thresh) {
PMD_RX_LOG(DEBUG, "port_id=%u queue_id=%u rx_tail=%u "
"nb_hold=%u nb_rx=%u",
(unsigned) rxq->port_id, (unsigned) rxq->queue_id,
(unsigned) rx_id, (unsigned) nb_hold,
(unsigned) nb_rx);
rx_id = (uint16_t) ((rx_id == 0) ?
(rxq->nb_rx_desc - 1) : (rx_id - 1));
E1000_PCI_REG_WRITE(rxq->rdt_reg_addr, rx_id);
nb_hold = 0;
}
rxq->nb_rx_hold = nb_hold;
return nb_rx;
}接收报文的整理流程梳理如下图所示:
DMA控制器控制报文一个个写到rx ring中接收描述符指定的IO虚拟内存中,对应的实际内存应该就是mbuf;
接收函数用rx tail变量控制不停地读取rx ring中的描述符和sw ring中的mbuf,并申请新的mbuf放入sw ring中,更新rx ring中的buffer addr
最后把读取的mbuf返回给应用程序。
4、线程亲和性
一个cpu上可以运行多个线程, 由linux内核来调度各个线程的执行。内核在调度线程时,会进行上下文切换,保存线程的堆栈等信息, 以便这个线程下次再被调度执行时,继续从指定的位置开始执行。然而上下文切换是需要耗费cpu资源的的。多核体系的CPU,物理核上的线程来回切换,会导致L1/L2 cache命中率的下降。同时NUMA架构下,如果操作系统调度线程的时候,跨越了NUMA节点,将会导致大量的L3 cache的丢失。Linux对线程的亲和性是有支持的, 如果将线程和cpu进行绑定的话,线程会一直在指定的cpu上运行,不会被操作系统调度到别的cpu上,线程之间互相独立工作而不会互相扰完,节省了操作系统来回调度的时间。目前DPDK通过把线程绑定到cpu的方法来避免跨核任务中的切换开销。
线程绑定cpu物理核的函数如下:
/* set affinity for current EAL thread */
static int
eal_thread_set_affinity(void)
{
unsigned lcore_id = rte_lcore_id();
/* acquire system unique id */
rte_gettid();
/* update EAL thread core affinity */
return rte_thread_set_affinity(&lcore_config[lcore_id].cpuset);
}继续往下走:
/*
根据前面的rte_cpuset_t ,设置tid的绑定关系
存储thread local socket_id
存储thread local rte_cpuset_t
*/
int
rte_thread_set_affinity(rte_cpuset_t *cpusetp)
{
int s;
unsigned lcore_id;
pthread_t tid;
tid = pthread_self();//得到当前线程id
//绑定cpu和线程
s = pthread_setaffinity_np(tid, sizeof(rte_cpuset_t), cpusetp);
if (s != 0) {
RTE_LOG(ERR, EAL, "pthread_setaffinity_np failed\n");
return -1;
}
/* store socket_id in TLS for quick access */
//socketid存放到线程本地空间,便于快速读取
RTE_PER_LCORE(_socket_id) =
eal_cpuset_socket_id(cpusetp);
/* store cpuset in TLS for quick access */
//cpu信息存放到cpu本地空间,便于快速读取
memmove(&RTE_PER_LCORE(_cpuset), cpusetp,
sizeof(rte_cpuset_t));
lcore_id = rte_lcore_id();//获取线程绑定的CPU
if (lcore_id != (unsigned)LCORE_ID_ANY) {//如果不相等,就更新lcore配置
/* EAL thread will update lcore_config */
lcore_config[lcore_id].socket_id = RTE_PER_LCORE(_socket_id);
memmove(&lcore_config[lcore_id].cpuset, cpusetp,
sizeof(rte_cpuset_t));
}
return 0;
}继续往下走:
int
pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
const rte_cpuset_t *cpuset)
{
if (override) {
/* we only allow affinity with a single CPU */
if (CPU_COUNT(cpuset) != 1)
return POSIX_ERRNO(EINVAL);
/* we only allow the current thread to sets its own affinity */
struct lthread *lt = (struct lthread *)thread;
if (lthread_current() != lt)
return POSIX_ERRNO(EINVAL);
/* determine the CPU being requested */
int i;
for (i = 0; i < LTHREAD_MAX_LCORES; i++) {
if (!CPU_ISSET(i, cpuset))
continue;
break;
}
/* check requested core is allowed */
if (i == LTHREAD_MAX_LCORES)
return POSIX_ERRNO(EINVAL);
/* finally we can set affinity to the requested lcore
前面做了大量的检查和容错,这里终于开始绑定cpu了
*/
lthread_set_affinity(i);
return 0;
}
return _sys_pthread_funcs.f_pthread_setaffinity_np(thread, cpusetsize,
cpuset);
}绑定cpu的方法也简单:本质就是个上下文切换
/*
* migrate the current thread to another scheduler running
* on the specified lcore.
*/
int lthread_set_affinity(unsigned lcoreid)
{
struct lthread *lt = THIS_LTHREAD;
struct lthread_sched *dest_sched;
if (unlikely(lcoreid >= LTHREAD_MAX_LCORES))
return POSIX_ERRNO(EINVAL);
DIAG_EVENT(lt, LT_DIAG_LTHREAD_AFFINITY, lcoreid, 0);
dest_sched = schedcore[lcoreid];
if (unlikely(dest_sched == NULL))
return POSIX_ERRNO(EINVAL);
if (likely(dest_sched != THIS_SCHED)) {
lt->sched = dest_sched;
lt->pending_wr_queue = dest_sched->pready;
//真正切换线程到指定cpu运行的代码
_affinitize();
return 0;
}
return 0;
}
tatic __rte_always_inline void
_affinitize(void);
static inline void
_affinitize(void)
{
struct lthread *lt = THIS_LTHREAD;
DIAG_EVENT(lt, LT_DIAG_LTHREAD_SUSPENDED, 0, 0);
ctx_switch(&(THIS_SCHED)->ctx, <->ctx);
}
void
ctx_switch(struct ctx *new_ctx __rte_unused, struct ctx *curr_ctx __rte_unused)
{
/* SAVE CURRENT CONTEXT */
asm volatile (
/* Save SP */
"mov x3, sp\n"
"str x3, [x1, #0]\n"
/* Save FP and LR */
"stp x29, x30, [x1, #8]\n"
/* Save Callee Saved Regs x19 - x28 */
"stp x19, x20, [x1, #24]\n"
"stp x21, x22, [x1, #40]\n"
"stp x23, x24, [x1, #56]\n"
"stp x25, x26, [x1, #72]\n"
"stp x27, x28, [x1, #88]\n"
/*
* Save bottom 64-bits of Callee Saved
* SIMD Regs v8 - v15
*/
"stp d8, d9, [x1, #104]\n"
"stp d10, d11, [x1, #120]\n"
"stp d12, d13, [x1, #136]\n"
"stp d14, d15, [x1, #152]\n"
);
/* RESTORE NEW CONTEXT */
asm volatile (
/* Restore SP */
"ldr x3, [x0, #0]\n"
"mov sp, x3\n"
/* Restore FP and LR */
"ldp x29, x30, [x0, #8]\n"
/* Restore Callee Saved Regs x19 - x28 */
"ldp x19, x20, [x0, #24]\n"
"ldp x21, x22, [x0, #40]\n"
"ldp x23, x24, [x0, #56]\n"
"ldp x25, x26, [x0, #72]\n"
"ldp x27, x28, [x0, #88]\n"
/*
* Restore bottom 64-bits of Callee Saved
* SIMD Regs v8 - v15
*/
"ldp d8, d9, [x0, #104]\n"
"ldp d10, d11, [x0, #120]\n"
"ldp d12, d13, [x0, #136]\n"
"ldp d14, d15, [x0, #152]\n"
);
}
参考:
1、https://blog.csdn.net/ApeLife/article/details/100751359 uio驱动实现
2、https://blog.csdn.net/ApeLife/article/details/100006695 内存池的实现
3、https://blog.51cto.com/u_15076236/4624576 PMD优化
4、https://blog.csdn.net/jeawayfox/article/details/105189788 dpdk接收报文
5、https://blog.csdn.net/u012630961/article/details/80918682 dpdk线程亲和性
6、https://zhuanlan.zhihu.com/p/366155783 dpdk多线程模型