趣话计算机底层技术
第一章 中央处理器CPU
1.1.1 逻辑门
1.1.2 加法器
1.1.3 算术逻辑单元ALU
- 既可以算术运算,也可以逻辑运算
- 本质都是通过输入控制的开关
- 乘法转换成加法
- 除法转换成减法
- 减法转换成加法
1.2.1 指令集
- 指令机器码 CPU支持的所有指令的编码(比特流)
- CPU的指令集 所有的指令
- RISC 一条指令只完成一个基本操作的精简指令集,指令长度基本固定
- CISC 一个指令可以完成一个复杂功能的复杂指令集,指令长度基本不固定
1.2.2 寄存器
- 原因 存储在内存不安全,读写效率慢
- 寄存器 CPU内部保存数据的存储电路
- 有指令操作寄存器
1.2.3 汇编语言
- 汇编语言 用助记符来编程的语言
1.2.4 高级语言
- 高级语言 接近人类的自然语言来编程的语言
- 编译器
- 编译 高级语言转换成机器指令
1.2.5 指令执行过程
- 读取指令
- 指令译码
- 指令执行
- 数据会写
1.3.1 指令流水集
- 改进执行指令的流程,提升性能
1.3.2 流水线的级数
- 将指令执行过程拆分更细
- 进一步减少CPU电路资源的浪费
- 但是级数过多会增加额外的电路设备,产生额外的功耗,造成额外的时间开销
1.3.3 流水线里的冒险
- 原因
- 访问同一电路(内存、输出)导致流水线停顿
- 指令所需数据来自前一指令
- 结构冒险 流水线中出现硬件资源竞争
- 数据冒险 流水线中后面的指令需要等待前面指令完成数据的读写
- 控制冒险 流水线需要根据前面的指令的执行结果来决定下一步去哪里执行
1.4 缓存
- 缓存 保存从内存的数据和指令
- 缓存行 管理内存的单元
- 取模映射 内存中的数据只能存在缓存中的固定位置,方便存储和访问
- 二路组相联 每个cache分成两组,即两个缓存行
- 降低冲突——允许多个具有相同索引但不同标记的数据同时被缓存
- 提高命中率——每个组有两个缓存行,它可以存储更多的数据
- 指令缓存 存储指令
- 数据缓存 存储数据
- 三级缓存技术
- 一级缓存
- 二级缓存
- 三级缓存(CPU多核共用)
1.5 多核缓存不一致问题
- 原子操作 不可切分的动作
- 片内总线 多核之间进行信息沟通
- 缓存一致性协议(缓存行的四种状态)
- 已修改(Modified, M)
- 独占(Exclusive, E)
- 共享(Shared, S)
- 无效(Invalid, I)
- 规定一个内存被多个核缓存,不允许多个核同时修改缓存❎
1.6 指令乱序执行
- 数据冒险与流水线停顿
- 数据冒险 流水线中后面的指令需要等待前面指令完成数据的读写
- 乱序执行 先执行不需要依赖前面数据的指令,打乱指令执行的顺序
- 保留站 一个缓冲区登记指令是否有数据依赖、具体依赖什么数据、需要用到的执行部件有哪些、当前是否繁忙、以及需要读写的寄存器
1.7 控制冒险与跳转指令
- 静态预测 不管前面结果,直接假定分支不会跳转继续把后续的指令载入流水线处理
- 分支预测 记录最近跳转的次数,根据最近多次跳转的结果再来预测
1.8 一条指令同时处理多个数据
- MMO〜MM7 64位寄存器,同时存储两个32位的整数和8个8位的整数(借用用浮点数运算单元FPU的寄存器)
- XMMO〜XMM7 128位寄存器
- 单指令多数据流SIMD 在一条指令中同时处理多个数据的技术
1.9 一个核同时执行两个线程
- 资源闲置 比如整数运算时浮点数运算电路会闲置
- 超线程 单线程改多线程,效率不能翻倍但有提升
1.10 管理内存
- 分段式存储管理 寄存器16位,最大64KB的段
- 访问内存 段地址+段内偏移地址
- OS划分时间片
- 虚拟内存
- 分页交换 把暂时不用的页面放到硬盘,使用页错误中断换回页面
1.11 地址翻译
- 内存管理单元MMU
- 页目录索引
- 页表索引
- 页内偏移
- 地址翻译 从CR3寄存器中取出页目录地址,根据页目录索引找到页表,再根据页表索引找到物理内存页面,最后根据页内偏移,完成寻址
- 地址翻译缓存 把翻译的虚拟地址和物理地址的映射关系放入缓存(局部性原理)
- 地址转换后援缓冲器(快表)TLB
1.12 GPU
- 多计算电路,少逻辑控制电路
- SIMT并行计算 (批量计算算法固定)
- 多ALU and 多执行上下文 (充分利用计算资源,不让ALU闲置)
第二章 存储设备
2.1 缓存为什么快
- 内存 动态随机存储器DRAM(1个DRAM单元——一个MOS晶体管和一个电容,可以存储1比特的信息)
- 缓存 静态随机存储器SRAM
- 缓存因为为没有依靠电容充放电,全都是晶体管的导通与断开,比起内存的DRAM速度要快得多
- 缺点 成本高、占用空间大
2.2 内存条
- 存储芯片 黑乎乎的东西8~16个
- PCB电路板
- 金手指 连接主板插槽的接触点
- RAM 随机存储器
- 动态数据刷新 DRAM“漏电”电容的电荷消失,需要周期性充电刷新来维持数据的稳定
- 内存控制器
- 指定芯片、分片、格子的行地址和列地址来访问比特位
- 读写单元 一字节8个比特位
2.3 多个CPU共同访问内存
- 非一致性内存访问NUMA(Non Uniform Memory Access)
- 16核拆分成两个CPU,组成两个NUMA节点Node
- 每个节点直接连接一部分内存
- 内连接(inter-connect)通道 两个CPU之间的通道
- 本地访问
- 远程访问
- 远程访问效率高于页面置换,内存不够时优先远程访问
2.4 硬盘
- 金属磁粒 机械硬盘盘面存储数据的东西
- 读磁头 通过电磁检测磁粒的极性分辨0和1
- 写磁头 通过磁场改变单元格中金属磁粒的极性,设定成0或1
2.5 磁盘管理
- 块block 读写的基本单位,把连续的扇区当成一个整体
- 块位图 记录哪些块是空闲的,哪些块是被占用的(记录在第一个块)
- inode 记录每一个文件的大小、位置、权限、时间等,每一个都是128字节,并且每一个都有一个专属号码在inode表
- 目录
- 根目录
- 描述符 记录inode表、块位图、inode位图的位置信息
- 超级块 记录硬盘总共用了多少块,还剩多少块
- 启动扇区DBR 装载引导程序
- 主引导扇区MBR 记录所有的分区信息,位于硬盘的第一个扇区
第三章 数据的输入与输出
3.1 总线
- 总线 包含传输数据的数据总线、传输地址的地址总线和进行控制管理的控制总线
- 总线控制器 统一管理总线
- 北桥芯片 集成内存控制器、总线控制器、图像控制器(访问内存和显卡)高速设备
- 南桥芯片 集成各种IO外部设备的控制器(低速设备)
- 一条总线 → 多个层级总线组成的总线系统
- NOW CPU集成内存控制器和图像控制器 重新变回一条总线
3.2 中断
- eflags寄存器 存储当前CPU是否可以被中断的值
- 不可屏蔽的中断NMI
- 可编程中断控制器PIC
- 中断向量
- 中断描述符表IDT 记录处理中断对应的函数地址
- idtr寄存器 指向中断描述符表IDT的内存地址
- 异常处理 也是这个表,但异常需要立即处理
- 异常是同步的,中断是异步的
- 高级可编程中断控制器APIC
- IO APIC
- Local APIC
- 处理器间中断Inter-Processor Interrupt IPI
- 中断亲和性
- 亲和寄存器
3.3 计算机启动
- 自检工作 所有寄存器全部重置,如有错误记录到EAX寄存器
- 引导处理器BSP
- 主引导记录MBR 512字节,检测最后两个合法字节是0x55和0xAA
3.4 数据搬运
- 可编程输入输出模式PIO 执行in和out两条指令对外部设备读写数据
- 直接存储器访问DMA 设置寄存器传输哪里的数据,从哪里到哪里,长度是多少
- DMA控制器DMAC
3.5 零拷贝技术
- 零拷贝技术 把从硬盘读取的数据缓冲区地址和长度给网络socket描述符
3.6 网卡
- 集线器Hub 收到的信号做一个增强处理后发送给所有端口
- CSMA/CD 载波侦听多路访问/冲突检测
- 以太网帧长度不能低于64字节,这样就算在最远两端发生的碰撞冲突都能及时传递回去被检测到
- ARP 地址解析协议 发送目的地址是FF:FF:FE:FF:FF:FF的广播,匹配的发送回去
- ARP欺骗
- 网卡的混杂模式 把总线的全部数据帧抓取交给CPU处理
- 交换机 记录对应的MAC地址和端口号,精准发送
- 全双工通信
- 冲突域
- 数据帧校验 对帧检验序列FCS进行循环冗余码校验CRC
- RX FIFO队列 网卡内部的接收队列缓冲区
- DMA控制器 传输网卡的数据
- 硬中断 硬中断需要快速完成
- 软中断 硬中断无法快速完成的调用软中断处理函数进行处理
- NAPI 数据包过多采用中断+轮询
- 第一部分 硬中断通知,关中断,但不处理数据包
- 第二部分 软中断轮询处理,不需要关中断
- Netfilter Linux内核的一个子系统,允许实现各种与网络相关的操作
- 协议栈
3.7 直接收发数据包
- 每次中断都要保存上下文,从用户态切换到内核态
- 线程亲和性 网络监控软件的工作线程独占CPU核心,解决缓存失效问题
- DPDK
- 通过操作系统的用户态模式驱动UIO,在用户态通过轮询的方式读取网卡的数据包
- 直接在用户态读取,不用把数据包在内核态空间和用户态空间搬来搬去
- 读取后直接分析,不用走系统协议栈和netfilter
- 大页内存技术 支持2MB和1GB管理内存页面
- Interrupt DPDK
- 没有数据包处理时就进入睡眠,改为中断通知
- 共享CPU核,不独占CPU
- DPDK线程有更高的调度优先级
- 数据包多后变轮询模式,灵活切换
第四章 操作系统
4.1 控制程序
- 多道程序处理
- 时间分片
- 时钟中断
- 任务状态
- 创建
- 就绪
- 执行
- 阻塞
- 终止
- 优先级
- 抢占 高优先级程序出现时,低优先级的程序未完成被剥夺执行机会
- 多核时代 → 操作系统
4.2 进程
- 1号进程init
- PID 进程的身份证
- 访问越界
- 内核地址空间 操作系统内核运行的空间
- 进程调度
- 一个进程里可以同时存在多个执行流,也就是多个线程,每一个线程都有自己的执行上下文和堆栈,互不影响
- 线程 操作系统调度执行的单位
- 并发执行 运行多个不同的线程
4.3 线程
- 完全公平调度算法CFS 红黑树管理线程
- 只要进程的其中一个线程挂掉,所有线程都会被结束掉
- 权重 权重越高的线程越优先被运行
- 等待队列
4.4 系统调用
- 内核地址空间 操作系统内核所在的内存区域
- 用户地址空间 应用程序访问的内存区域
- 系统调用表 sys_call_table
- 系统调用 操作系统将管理文件、内存、进程、线程、网络、硬件等的在内核地址空间操作封装成函数接口方便应用程序调用
- 内核栈 线程有两个栈,一个在用户态地址空间,一个在内核态地址空间,内核栈会小很多
- syscall sysret 一对的
4.5 异常处理
- 中断描述符IDT 记录所有中断和异常需要处理的地方
- 异常 CPU在执行线程的代码指令时出现了错误
- idtr寄存器记录中断描述符IDT
- 信号投递
- 触发异常时,CPU自动保存的现场(返回地址和一些其他关键寄存器的值)
- iret(interrept return) 专门用于被中断或异常打断的线程处理完毕后返回用户态地址空间
4.6 信号处理
- 可靠信号和不可靠信号
- 进程的描述符task_struct
- 进程不能直接调用这些信号处理函数
- 调用sigprocmask函数屏蔽信号
- SIGKILL和SIGSTOP无法屏蔽
- 一个进程实际就是一个线程组
- task struct中原来的信号等待队列只存放各个线程自己的信号,另外再单独设置一个队列来存放进程的信号,让所有的线程共享
- 给线程投递信号(group=0 )
- 给进程投递信号(group=1 )
- 处理信号处理函数的表格是整个进程共享的
4.7 锁
- 原子操作
- 自旋锁 获取锁的时候线程会一直循环检查状态
- 互斥锁 获取锁的时候线程进入锁的等待队列,交出CPU执行权限进入睡眠,等待唤醒
- 条件变量 等待条件变量的线程平时阻塞着,别的线程发现条件满足之后,就将条件变量激活
- 信号量 升级版互斥锁,有计数器指定最多允许多少个线程同时获得它
4.8 Linux的权限管理
- 文件打开的过程
- 常规DAC检查 genenric_permission函数
- 文件的归属用户id保存在文件索引inode
- 进程的用户id保存在进程的task_struct(task_struct->cred的fsuid)
- Linux操作系统为所有文件针对所属的用户、所属的用户组和其他用户分别设置了访问权限
- 读(Read )、写(Write )、可执行(Execute )三种权限
- UGO权限管理方式 权限信息和文件的归属信息记录在索引信息inode
- ACL访问控制列表 单独记录一些细粒度的权限信息(校验完进程所属用户和文件所属用户后,就会进入ACL的检查)
- Cgroup检查 是否有权访问对应的设备
4.9 Docker
- 轻量级的虚拟容器 只提供一个运行环境,不用运行一个操作系统,所有容器的系统内核与宿主机共用
- chroot和pivot_root 可以将进程看到的根目录修改为一个新位置(“伪造一个文件系统欺骗容器的进程”)
- 操作系统镜像文件和进程依赖的目录和文件通过联合挂载的方式,挂载到容器进程的根目录下,变成容器的rootfs,和真实系统目录一模一样
- 命名空间namespace 划定一个个的命名空间,把进程划分到这些命名空间中,每个命名空间都是独立存在的,命名空间里的进程都无法看到空间之外的进程、用户、网络等信息
- Cgroup 划定一个个分组,然后限制每个分组能够使用的资源,比如内存的上限值、CPU的使用率、硬盘空间总量等(系统内核会自动检查和限制这些分组中的进程资源使用量)
第五章 系统编程
5.1 进程
- Linux 一切皆文件
- fork函数 创建进程
- 一次调用会返回两次
- 父进程返回进程号
- 子进程返回0
- 子进程和父进程共享内存空间
- 写时拷贝(COW)机制 允许只读,写入会触发异常分配新页面
- fork创建子进程只拷贝当前线程,不拷贝其他线程
5.2 线程的栈
- 后进先出LIFO
- push和pop指令 压入数据和弹出数据
- call指令 调用函数(把call指令后面的那条指令地址保存到栈里面,等调用完函数后才能回来继续往后执行)
- ret指令 函数执行完后用来返回到调用它的地方的指令,CPU在执行ret指令的时候,就会把之前保存在这里的地址取出来,跳转过去
- 使用寄存器传参比使用线程栈传参快很多
ulimit -a
查看线程栈的大小- 自动增长 缺页异常处理时发现是线程栈会分配新内存页面
- 但是如果超出上限的话会杀死进程
- 内核栈 页面小(4KB~8KB)小心使用递归调用
- 栈溢出攻击 将栈里保存的返回地址覆盖为恶意代码的地址
5.3 进程通信
- 信号 Linux的一种软中断通信机制,总共64种信号,只能通知不能传输数据
kill -l
查看所有信号- socket套接字
- 127.0.0.1 本地回环地址,数据在协议栈转发,可以在虚拟的回环网卡lo抓取数字
- 匿名管道 内核的一段缓冲区,提供读写两个端口,单向
- 消息队列 内核的一个消息链表,可以指定类型
- 命名管道 有名字就不限制进程通道,只要使用名字都可以打开管道通信
- 共享内存
5.4 IO多路复用
- select函数 遍历所有文件描述符,挂入相关联设备的等待队列后进入阻塞,如果设备有消息提供回调函数通知Web服务进程
- select使用位图数组,最多同时处理1024个文件描述符
- poll模型 使用链表存储,可容纳更多文件描述符
- epoll模型
- 采用红黑树管理监听的文件描述符方便查找
- 采用双向链表管理队列,不需要遍历所有的文件描述符
5.5 读写文件
- 数据结构 缓存文件数据块信息存到内存
- fsync函数 进行同步将缓存写入硬盘
- 内存映射文件 将文件的数据缓存页映射到进程的用户台地址空间
- 把整个文件或者文件的一部分直接映射到应用程序的地址空间
5.6 协程
- 线程可以在执行函数遇到阻塞后,保存执行的上下文,转而执行别处的代码。待阻塞的请求完成后,再回去继续执行
- 协程 在一个线程中,可以抽象出多个执行流协程,由线程来统一调度管理
- 操作系统可以通过时钟中断和系统调用(抢占式调度)进入内核来剥夺线程的执行权
- Golang 设计为支持协程,封装好系统调用方便协程调度器管理
5.7 调试器GDB
- ptrace函数
1 | long ptrace( |
break
设置断点- 把被调试进程中那个位置的指令修改为0xCC
- CPU执行这条特殊的指令会陷入内核态,然后取出中断描述符表IDT的3号表项中的处理函数来执行
- 系统内核拿到CPU的执行权,会发送一个SIGTRAP信号给被调试的进程
- GDB截获信号,检查是不是设置的断点,显示断点触发
- 在没有下一步指示之前,被调试的进程都不会进入就绪队列被调度执行
- 单步执行
- eflags标志寄存器 包含了程序运行的一些状态和一些工作模式的设定
- TF标记 告诉CPU进入单步执行模式
- 内存断点
watch
监视被调试进程中某个内存地址的数据变化- 调试寄存器 DR0到DR7总共8个,在DRO〜DR3中设置要监控的内存地址,在DR7中设置要监控的模式(读或写)
5.8 可执行文件ELF
- do_execve_commo函数 启动可执行文件的函数
- prepare_binprm函数 读取文件头部的128字节数据,放入内存缓冲区
- search_binary_handler函数 在可执行文件处理节点的链表中寻找处理可执行文件的模块
- load_script函数 脚本类型程序的加载函数
- 程序头表 结构数组,每一个结构都记录一个段(segment)的信息
- 段 进程地址空间中的一块区域,由一个节(section )或者多个节构成
- 节 存放ELF文件的数据,比如静态数据、代码指令、调试信息
- 加载过程
- load_elf_binary函数
- 引用动态链接库需要解释器加载
- 静态编译则不需要
- 检查可执行栈
- 加上随机偏移 难以推算数据和函数的内存地址
第六章 计算机攻击与安全
6.1 TCP序列号
- 中间人攻击 监听网络通信拿到通信的序列号和确认号,伪造进行通信
- 初始序列号INS 计数器,每4ms加1(不能直接发送,会被别人算出新的ISN)
- ISN = M + F(localhost, localport, remotehost, remoteport)
- M 计数器
- F MD5算法
- F参数 通信双方的IP和端口
- ISN = M + F(localhost, localport, remotehost, remoteport, secretkey)
- 同名计数器 因为CPU为8核防止多个线程之间竞争
- TCP定时器
- TCP计数器 DelayedACKLost
6.2 TCP SYN Flood
- SYN洪水攻击 收到SYN数据包后,需要准备一个数据块来存储客户端的信息,发送大量SYN数据包需要分配大量数据块直到空间耗尽
- SYN Cookie 第二次发给客户端的序列号是一个哈希值,第三次握手时计算哈
希值+1=ACK
为正常数据包,错误数据包直接丢弃
6.3 HHTPS
- 对称加密
- 非对称加密
- 非对称与对称加密结合 使用非对称加密算法传输加密内容的密钥
- 密钥计算
- 中间人攻击 冒充服务器和客户端通信,冒充客户端和服务器通信
- 数字证书
- 根证书 验证最终的签发者是否在根证书列表中
6.4 漏洞攻击
- 栈金丝雀Stack Canary 抵御栈溢出攻击
- 虚函数攻击 覆盖虚函数表指针,指向一个假的虚函数表,表格写入恶意代码地址
- 虚函数表指针一般都是在对象的头部(最前面8个字节)
- KPTI内核页表隔离 线程运行在用户态和内核态时使用不同的页表
- 侧信道 通过判断内存的访问速度来获知是否有被缓存
6.5 SGX
- 安全访问级别 Ring0-Ring3
- Enclave安全空间
- 创建 通过执行ECREATE指令创建一个安全空间
- 初始化 通过执行EINIT指令对安全空间进行初始化
- 进入&退出 通过执行EENTER/EEXIT指令进入和退出安全空间
- 中断&异常 通过执行AEX指令退出,将在安全空间执行的上下文保存起来
- 系统调用 通过执行AEX指令退出,执行完系统调用再进来
- 函数调用 安全空间和外部可以互相调用函数,普通空间调用安全空间函数叫ECALL,安全空间调用外部空间函数叫OCALL
- 销毁 通过执行EREMOVE指令销毁一个安全空间
- 内存加密 内存加密引擎MEE(memory encryption engine)电路,对安全空间的数据进行透明的加解密,数据写入内存时加密,读取CPU内部时解密
6.6 挖矿病毒
- top命令 常用的实时系统监控工具,提供了一个动态的、交互式的实时视图,显示系统的整体性能信息以及正在运行的进程的相关信息
- ps命令 显示当前进程的状态
- netstat命令 打印所有的网络连接信息
- unhide 网络取证工具,能够发现那些借助 rootkit、LKM 及其它技术隐藏的进程和TCP/UDP 端口
- Redis持久化存储
1 | CONFIG SET dir /root/.ssh # 指定保存地址 |
- 原因 Redis默认没有密码,可以使用命令行或直接修改redis.conf文件设置密码
6.7 整数+1引发的内核攻击
- IDT表项的结构图
- 无符号整数与有符号整数的切换
- 精心设计一个config值,从应用层传入内核空间的perf_swevent_init函数
- 利用内核漏洞,把一个64位无符号数赋值给一个int型变量,导致变量溢出为一个负数
- 禾ij用溢出的event_id越界访问perf_swevent_enabled,指向IDT的表项、将第四项中断处理函数的高32位进行+1
- 修改后的中断处理函数指向了用户空间,提前在此安排恶意代码。
- 应用层执行int 4汇编指令,触发4号中断,线程将进入内核空间,以内检权限执行提前安排的恶意代码
6.8 从虚拟机逃脱
- 虚拟化技术
- 硬件辅助虚拟化 操作系统和程序的指令都是在真实的CPU上执行的,不再用软件来解释模拟
- 虚拟机监控程序HyperVisor
- 虚拟机逃逸技术 虚拟机会和外面的真实计算机通信,抓住通信过程中的漏洞,把指令代码掺杂在通信数据中可以逃逸出去
- 漏洞编号CVE Common Vulnerabilities and Exposures 显示年份和具体漏洞编号