(一)Git使用(代码版本工具)
*版本穿梭*
git版本穿梭指的是可以在提交了多个版本的文件中自由的切换。
基本语法
git reset --hard 版本号
*代码演示*
#使用命令切换到第一个提交的版本文件中去。
Bear@Xans MINGW64 /d/demo (master)
$ git reflog
c430c84 (HEAD -> master) HEAD@{0}: commit: second commit # 第二次提交的版本
a0f668a HEAD@{1}: commit (initial): my first commit # 第一次提交的文件版本
Bear@Xans MINGW64 /d/demo (master)
$ git reset --hard a0f668a # 穿梭回到第一次提交的版本
HEAD is now at a0f668a my first commit
Bear@Xans MINGW64 /d/demo (master)
$ git reflog
a0f668a (HEAD -> master) HEAD@{0}: reset: moving to a0f668a
c430c84 HEAD@{1}: commit: second commit
a0f668a (HEAD -> master) HEAD@{2}: commit (initial): my first commit # 指针已经指向了第一次提交的版本
Bear@Xans MINGW64 /d/demo (master)
$ cat 1.txt # 此时查看文件内容,发现已经切换到第一个版本文件添加的内容中去了。
Hello China
Hello China
Hello China
Git 切换版本,底层其实是移动的 HEAD 指针,具体原理如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mTTa8Y6I-1691918091377)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps28.jpg)]
(二)CPU的缓存(Cache)机制
*1. 时间局部性:*
如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
----被引用过一次的存储器位置在未来会被多次引用(通常在循环中)。
*2. 空间局部性:*
一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。
----如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。
Cache 的工作原理:利用空间局部性和时间局部性原理,通过自有的存储空间,缓存一部分内存中的指令和数据,*减少CPU访问内存的次数*,从而提高系统的整体性能
1.为什么有些处理器没有Cache?
①这些处理器低功耗低成本,增加Cache会增加芯片面积和发热量
②工作频率不用高
③Cache无法保证实时性
(三)CPU访问内存或外部设备
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VDkAqHhJ-1691918091379)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps29.jpg)]
(四)地址的本质
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1IPBLh6B-1691918091379)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps30.jpg)]
地址的本质就是由CPU管脚发出的一组地址控制信号
(五)一些定义:
1.MMU
Memory Management Unit的缩写,中文名是内存管理单元,有时称作****分页内存管理单元*(英语:*paged memory management unit*,缩写为*PMMU****)。它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件
2.译码器
译码器(decoder)是一类多输入多输出组合逻辑电路器件,其可以分为:变量译码和显示译码两类。 变量译码器一般是一种较少输入变为较多输出的器件,常见的有n线-2^n线译码和8421BCD码译码两类;显示译码器用来将二进制数转换成对应的七段码,一般其可分为驱动LED和驱动LCD两类。
3.总线
总线是各种数字信号的集合,包含地址信号、数据信号、控制信号等。
不同的总线之间通过桥连接。
(1)两种编址方式:** **统一编址:内存RAM和外部设备共享CPU的可寻址空间
独立编址:内存RAM和外部设备分别占用CPU不同的可寻址空间
4.桥
桥一般是芯片组电路,用来将总线的电子信号翻译成另一种总线的电子信号
5.指令集
指令集是个虚的东西,是标准规范,就像红绿灯规则
CPU支持的有限个指令的集合
指令集最终的实现就是微架构,就是CPU内部的各种译码和执行电路
指令集作为CPU和编译器的设计规范和参考标准,主要用来定义指令的格式、操作数的类型、寄存器的分配、地址的格式等
几大指令集:x86、ARM、RISC-V
6.微架构
指令集在CPU处理器内部的具体硬件电路的实现,称为微架构
一套相同的指令集,可由不同形式的电路实现,有不同的微架构
微架构一般称为CPU内核
7.EABI
EABI中的E,表示“Embedded”,即嵌入式应用二进制接口,是一种新的ABI。EABI支持软件浮点和硬件实现浮点功能混用,系统调用的效率更高,和今后的工具更兼容,软件浮点的情况下,EABI的软件浮点的效率要比OABI高很多。
(六)ARM 汇编
1.GNU ARM 汇编参考文档
2.ARM汇编指令
ARM汇编指令集汇总_arm汇编语言指令大全_Saint-000的博客-CSDN博客
汇编指令-跳转指令B BEQ BNE BCC_汇编beq_瑞欧莱的博客-CSDN博客
[arm-汇编stmdb、ldmia、stmfd、ldmfd] - 简书 (jianshu.com)
arm汇编指令探究之 ldmia_半瓶醋的历史的博客-CSDN博客
Blo
ARM汇编(2)_arm blo_bleeoom的博客-CSDN博客
Movw和movt
ARM base instruction – movw and movt-CSDN博客
3.ARM 寄存器
寄存器属于计算机的中央处理器(CPU)部分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CjqjDiIT-1691918091380)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps31.jpg)]
4.objcopy命令
将一个文件的内容复制到另一个目标文件中,对目标文件实行格式转换
①可执行文件内部结构
.symtab 符号表
.debug 用来保存可执行文件中每一条二进制指令对应的源码位置信息(根据这些信息,GDB调试器才可执行源码级调试)
BSS段不占用空间
.init 代码来自C运行库的一些汇编代码。用来初始化C运行所依赖的环境,如堆栈初始化
.text(代码段) .rodata(只读数据段) .bss(放置未初始化的全局变量和静态变量) .data(数据段)
字符串常量放置 .rodata
BSS段设计的初衷是为了减少文件体积,节省磁盘空间。
BSS段和数据段的唯一差异:在可执行文件内不给bss段分配存储空间,在程序运行内存时再分配存储空间和地址
5.编译流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1mzPUNeN-1691918091381)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps34.jpg)]
(1)强、弱符号
强:函数名、初始化的全局变量
弱:未初始化的全局变量
各个文件定义的全局变量变量名相同,发生符号冲突,可执行文件到底执行哪一个?
决议规则
一山不容二虎
强弱可以共存 (强弱共存,强覆盖弱)
体积大者胜出
(七)程序的运行
1.操作系统下程序运行
通过加载器将程序的指令加载到内存中,然后CPU到内存中取指令运行
2.一些概念、函数
①Fork
Linux系统——fork()函数详解(看这一篇就够了!!!)_fork()用于_代码拌饭饭更香的博客-CSDN博客
用于创建一个进程,所创建的进程复制父进程的代码段/数据段/BSS段/堆/栈等所有用户空间信息;在内核中操作系统重新为其申请了一个PCB,并使用父进程的PCB进行初始化;(Process Control Block,进程控制块)
②物理地址、虚拟地址
虚拟地址关系到进程的用户空间和内核空间,而物理地址则用来寻址实际可用的内存
虚拟地址到物理地址的转换通常需要以下几个步骤:
\1. 获取页表项:操作系统会通过虚拟地址中的页号去查找相应的页表项。页表项中存储了该虚拟地址对应的物理页号以及其他的信息。
\2. 计算物理地址:通过得到的页表项中的物理页号和虚拟地址中的页内偏移量,就可以计算出实际的物理地址,也就是内存中的实际硬件地址。
\3. 访问物理地址:使用计算出的物理地址可以直接访问内存中的数据,完成读取或者写入操作。
通过上述三个步骤,虚拟地址就被成功转换成了对应的物理地址。在实际情况中,还需要考虑多级页表、虚拟内存的分页、地址空间的映射等复杂情况。而在硬件的实现中,还有诸如TLB等优化设计可以提高虚实地址转换的效率。
相同的虚拟地址经过MMU硬件转换后,会分别映射到物理内存的不同区域
③进程管理、调度和运行原理
对于每一个运行的进程,Linux内核会使用一个task_struct结构体来表示,多个结构体通过指针构成链表。操作系统基于链表对进程进行管理、调度和运行。
不同进程的代码段和数据段分别存在物理内存不同的物理页上。
④NAND/NOR分区
NAND分区是指将储存空间划分为块,每个块由多个页组成。这种分区方式适用于存储大量数据和文件的闪存设备中,如固态硬盘和USB闪存驱动器。
NOR分区是将储存空间按字节地址划分为块,每个块包含多个字节。这种分区方式适用于需要频繁更改的应用程序,如嵌入式系统或移动设备。它还可以用于存储引导代码或操作系统映像等特殊用途。
3.裸机环境下的程序运行
裸机平台下,系统商店后,没有程序运行的环境,需要借助第三方工具将程序加载到内存
如(JTAG)
4.程序入口main()函数分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awiJeBqd-1691918091382)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps35.jpg)]
(八)链接(自制库)
1.链接静态库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZ6a9PhI-1691918091383)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps36.jpg)]
Linux ar命令 | 菜鸟教程 (runoob.com)
链接静态库会出现一个问题,当我们在源文件定义了100个函数,却只用了一个,这样会使可执行文件体积大大增加
解决方法:一个源文件定义一个函数
但是,当很多程序调用函数(如printf),会导致大量重复的printf代码指令,浪费内存资源。
这时候,动态链接登场了!
2.动态链接库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-STweGszW-1691918091385)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps38.jpg)]
除了默认路径,还可用户指定路径去查找库
用户可以在/etc/ld.so.conf中添加自己的共享库路径,修改后可以使用ldconfig命令生成缓存/etc/ld.so.chche以提高查找效率。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XzHJOHrZ-1691918091386)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps39.jpg)]
动态链接器本身也是一个动态库,即/lib/ld-linux.so文件
它会先给自己重定位,然后在运行。(自举)
将动态库设计成与地址无关的代码,可实现多进程之间对库的共享
与地址无关,利用相对寻址来实现
每个应用程序将引用的动态库(绝对地址)符号收集起来,保存在GOT表,记录各符号的地址。GOT表(全局偏移表)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9uhPt9m-1691918091387)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps41.jpg)]
在使用延迟绑定时,程序不会在程序启动时加载所有的链接库,而是等到需要时才加载链接库。这样可以减少程序启动时的加载时间和程序的内存占用,提高程序的响应速度和执行效率。同时,如果在程序运行时未使用某些链接库,则可以避免浪费资源。
显示加载引用动态库
Makefile
[ Makefile中.PHONY伪目标的作用(完整)_伪目标 .phony_tilblackout的博客-CSDN博客](#:~:text=Makefile中.PHONY伪目标的作用 (完整) 1 第一种情况 :避免命令与目录下的文件名重复,2 第二种情况 :make的并行和递归执行过程中 3 第三种情况 :生成多个应用程序)
避免命令与当前目录文件重名
(九)Linux内核模块
内核模块的运行不同于应用程序,不依赖C标准库,动态链接和重定位过程需要内核自己来完成。运行于内核空间;模块加载由系统调用init_module完成。
1.内核模块的加载流程
2.U-boot重定位分析
(1)一些概念、问题
CPU的SVC模式:
SVC (Supervisor Call) 模式是 ARM 处理器的一个特权级别,可以让操作系统内核执行一些特定的操作,例如进程切换、I/O 操作等。在 SVC 模式下,CPU 可以访问一些特殊的寄存器,包括 SPSR (Saved Program Status Register) 和 LR (Link Register),用于保存当前的程序状态以及调用 SVC 的返回地址。操作系统通常会通过一个特定的指令 (SWI) 将 CPU 切换到 SVC 模式。
SDRAM
SDRAM是同步动态随机存取存储器,是计算机主板上常用的一种内存类型。它是一种具有高速度、大容量、低功耗、高可靠性的存储器,能够提高计算机系统的工作效率和应用性能。与其它存储器相比,SDRAM采用同步技术,即输出数据的时钟与输入数据时钟同步,因此其读写速度更快。同时,它还具有自刷新功能,可以自动刷新存储器芯片中的存储信息,保证数据不会丢失。目前主流的DDR SDRAM已经普及,其速度和容量已经大幅提升。
系统上电复位为什么要关闭Cache和MMU?
在系统上电复位时,部分内存中的数据可能会变得不可用或无效,导致CPU Cache和MMU中缓存的数据与当前内存中的实际数据不匹配。如果不关闭Cache和MMU,CPU将继续访问缓存的数据,而不是主存中的实际数据,从而可能导致不可预期的错误行为,甚至系统崩溃。
系统上电复位程序主要执行下列操作
● 设置CPU为SVC模式。
● 关闭Cache,关闭MMU。
● 设置看门狗、屏蔽中断、设置时钟、初始化SDRAM
因此,在系统上电复位时,需要关闭Cache和MMU,以确保CPU只能访问主存中的实际数据。稍后在系统初始化过程中,可以重新启用Cache和MMU。
零长度数组
int array[0];
零长度数组虽然不占据任何内存空间,但仍然具有一些特定的用途。例如,它可以用于作为函数的返回值,表示返回一个空的集合或序列;或者作为一个占位符,占据某个位置,但不需要提交任何元素。
需要注意的是,访问零长度数组的第0个元素是未定义的行为,因为数组中没有任何元素。因此,如果需要表示一个非空的集合或序列,应当使用数组长度大于0的数组。
(2)LDS文件语法
链接脚本主要用于规定如何把输入文件内的section放入输出文件内, 并控制输出文件内各部分在程序地址空间内的布局. 但你也可以用连接命令做一些其他事情
(3)boot是如何启动的呢?谁又来引导U-boot运行的呢?
ARM SoC一般会在芯片内部集成一块ROM,在ROM上会固化一段启动代码,如图4-37所示,系统上电后,会首先运行固化在芯片内部的ROMCODE代码。这部分代码的主要工作就是初始化存储接口、建立存储映射,它会根据CPU外部管脚或eFuse值来判断系统的启动方式。
多种启动方式:
NOR Flash、NAND Flash或者从SD卡启动
如果我们设置系统从NOR Flash启动,那么这段代码就会将NOR Flash映射到零地址,然后系统复位,CPU跳到U-boot中断向量表中的第一行代码,即NOR Flash中的第一行代码去执
行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-olugx0et-1691918091389)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps44.jpg)]
除了SDRAM和NOR Flash支持随机读写,可以直接运行代码,其他Flash存储器是不支持直接运行代码的,只能将代码复制到内存中执行
系统一般会先将NAND Flash 或SD卡中的一部分代码(前4KB)复制到芯片内部的SRAM中去执行,映射SRAM到零地址,然后在这4KB代码中进行各种初始化、代码复制、重定位等工作,最后PC指针才跳到SDRAM内存中去执行代码。
(十)内存堆栈管理
1.栈的管理
先进后出
满栈SP指向栈顶元素,空栈指向栈顶元素上方的可用空间
C语言函数中的局部变量、传递的实参、返回的结果、编译器生成的临时变量都是保存在栈。
系统上电开始运行的都是汇编代码,在执行第一个C语言函数之前,都要初始化栈空间
计算机运行的程序分操作系统和应用程序。
每个应用程序进程都有4GB(32位)的虚拟地址空间,其中0-3GB用户空间分给应用程序使用,3GB-4GB内核空间分给操作系统使用。
应用程序无权限访问内核,只能通过中断或系统调用访问内核
通过页表和MMU转换虚拟为物理地址,每个进程可独享3GB用户空间
(1)栈的初始化
本质就是栈指针SP的初始化,在系统启动过程中,内存初始化后,将栈指针指向内存中的一段空间,就完成了栈的初始化
ARM处理器使用R13(SP)和R11(FP)来管理堆栈。
新版本Linux栈的起始地址是随机的,防黑客栈溢出攻击(高地址往低地址增长)
栈空间查看、设置
ulimit -s //查看栈大小 单位KB
ulimit -s 4096 //设置栈空间大小 4MB 即4*(2的20次方)
(2)栈溢出
栈内存空间满溢出
编写程序避免栈溢出原则:
①尽量不用大数组,需大内存用堆内存malloc
②函数不要嵌套太深
③递归层数不宜深
(3)FILO栈式操作
FILO是"First In Last Out"的缩写,表示先进后出。栈是一种能够实现FILO操作的数据结构。栈有两种基本操作,一种是push操作,将元素放入栈中,另一种是pop操作,将栈顶的元素弹出。
对于FILO栈式操作,可以使用栈来实现。可以理解为一个放在桌子上的桶或者箱子,我们在桶或者箱子的顶部放入一个个物品,但是当我们需要拿出其中一个物品的时候,只能从顶部开始逐一取出,直到取到目标物品。
在编程语言中,实现FILO栈式操作的相关函数包括push和pop。push函数将元素压入栈中,pop函数将栈顶元素弹出。FILO栈式操作常常用在算法题中,可以帮助我们更快速的解决一些复杂问题。
(4)内存池
内存池是一种数据结构,用于管理分配和释放内存的方法。它是一块预先分配好的内存区域,被分割成固定大小的内存块(或对象),这些内存块可以被动态地分配和释放,同时可以重复利用已经释放的内存块,避免了频繁的内存分配和释放操作,减少了内存碎片的产生,提高了内存使用效率。内存池被广泛地应用于各种高性能计算机系统、网络服务器和游戏引擎等场景。
(5)嵌入式系统堆内存管理框架
(6)mmap映射区
①缓存页减少系统调用(非mmap)
当我们运行一个程序时,需要从磁盘上将该可执行文件加载到内存。将文件加载到内存有两种常用的操作方法,一种是通过常规的文件I/O操作,如read/write等系统调用接口;一种是使用mmap系统调用将文件映射到进程的虚拟空间,然后直接对这片映射区域读写即可。
为了提高读写效率,减少I/O读盘次数以保护磁盘,Linux内核基于程序的局部原理提供了一种磁盘缓冲机制
I/O缓冲区通过减少系统调用的次数来降低系统调用的开销,但也 增加了数据在不同缓冲区复制的次数:一次读写流程要完成两次数据的复制操作。
②mmap
通过mmap系统调用将文件直接映射到进程的虚拟地址空间中,地址与文件数据一一对应,对这片内存映射区域进行读写操作 相当于对磁盘上的文件进行读写操作。这种映射方式减少了内存复制和系统调用的次数,可以进一步提高系统性能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EWQodfQG-1691918091391)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps47.jpg)]
通过mmap()函数虽然完成了文件和进程虚拟空间的映射,但是需 要注意的是,现在文件还在磁盘上。当用户程序开始读写进程虚拟空 间中的这片映射区域时,发现这片映射区域还没有分配物理内存,就 会产生一个请页异常,Linux内存管理子系统就会给该片映射内存分配 物理内存,将要读写的文件内容读取到这片内存,最后将虚拟地址和 物理地址之间的映射关系写入该进程的页表。文件映射的这片空间分 配物理内存成功后,我们再去读写文件时就不用使用文件的I/O接口函数了,直接对进程空间中的这片映射区域读写即可。
(7)mmap应用
硬盘大文件读写
Framebuffer 屏幕显示
多进程共享动态库
(十一)内存泄漏
当用户使用malloc()申请内存时,内 存分配器ptmalloc将这两个内存块节点从空闲链表中摘除,并把内存 块的地址返给用户使用。如果用户使用后忘了归还,那么空闲链表中就没有了这两个内存块的信息,这两块内存也就无法继续使用了,在内存中就产生了两个“漏洞”,即发生了内存泄漏
1.预防内存泄漏
内存申请后及时地释放,两者要配对使用,
内存释放后要及时将指针设置为NULL
使用内存指针前要进行非空判断。
当函数的调用关系变得复杂时,就很容易产生内存泄漏。为了预防这种错误的发生,在编程时,如果我们在 一个函数内申请了内存,则要在申请处添加注释,说明这块内存应该在哪里释放
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UnyJLsHb-1691918091392)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps49.jpg)]
2.内存泄漏检测:MTrace
MTrace是Linux系统自带的一个工具
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9qKVaO2G-1691918091393)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps50.jpg)]
3.常见的内存错误
内存越界、内存踩踏、多次释放、非法指针
段错误:非法访问内存(访问了权限未许可的内存空间)
(1)内存越界
①访问内存禁区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9KQztRyn-1691918091395)(C:/Users/86156/AppData/Local/Temp/ksohtml13568/wps53.jpg)]
②非法指针
在我们调试链表时,通常通过指针来操作每一个节点。如果指针在遍历链表时已经指向链表的末尾或头部,指针已经指向NULL了,此时再通过该指针去访问节点的成员,就相当于访问零地址了,也会发生一个段错误,这个指针也就变成了非法指针。
③大容量数组或局部变量
在Linux环境下,每一个用户进程默认有8MB大小的栈空间,如果你在函数内定义大容量的数组或局部变量,就可能造成栈溢出,也会引发一个段错误。内核中的线程也是如此,每一个内核线程只有8KB的内核栈,在实际使用中也要非常小心,防止堆栈溢出。
④多次free
我们使用malloc()申请的堆内存,如果不小心多次使用free()进行释放,通常也会触发一个段错误。
⑤数组越界
在访问数组时,如果超越数组的边界继续访问,也会发生一个段错误。
⑥使用core dump调试段错误
嵌入式C P362
(2)内存践踏
①内存踩踏监测:mprotect
P366
②内存检测神器:Valgrind
P368
不仅可以检测内存泄漏,还可以对程序进行各种性能分析、代码覆盖测试、堆栈分析及CPU的Cache命中率、丢失率分析等