# 计算机系统
# 第一章 综述
计算机系统层次
程序从源码到运行的基本过程
补码
- 正数不变
- 负数忽略第一个数字,剩下取反加一
- 加法:直接加;减法:转化成加法。
- 若得到结果为负数,则一样忽略第一个数字,剩下位次取反加一
u%和d%的区别前者不读取符号,即把补码的第一位当作数字;后者读取符号。
在n位系统中,补码表示的范围为
# 第二章 数据的机器级表示与处理
# 数据的表示
定点数
带符号整数(signed interger)
用补码表示和计算,最高位MSB表示符号位。最高位为符号位,正数比负数多一个。
无符号整数(unsigned integer)
数量为
。C语言中,无符号数优先级更高,有符号和无符号进行运算时,优先按无符号处理。若溢出,无符号数结果为对
模运算。
浮点数
由三部分组成:
符号部分1位指数部分8位尾数部分23位。①先进行规格化②正数符号位为0,负数符号位为1③指数部分先加127偏移量再用相应进制表示④尾数部分只取小数点之后部分,多余的地方补0。
# 数据存储
存储单位
- 位
- 字节(8位)
- 字:存储器访问的基本单位,16位或32位
存储方式
- 大端模式:符合人类习惯。小号地址存储高字节,大号地址存储低字节。
- 小端模式:与大端模式相反,小号地址存储低字节,大号地址存储高字节。是x86架构的默认存储方式。
对齐存储
数据地址大小必须是数据大小的整数倍。优点:①提高CPU访问速度②减少跨存储单元造成的性能开销。
# 数据运算
浮点运算
见上。NaN表示非法计算结果。Infinity表示超出表示范围的数值。
整数运算(int32位,char8位,short16位,long随系统,longlong64位)
- 逻辑左移
<<:低位补0 - 逻辑右移
>>:高位补0 - 算数右移:高位补符号位
- 逻辑左移
# 第三章(一) 程序的转换与机器级表示
# 程序转换的基本过程
预处理Preprocessing
展开宏、处理条件编译、插入头文件
编译Compilation
将高级语言代码转化为汇编代码
.s文件汇编Assembly
将汇编代码转化为机器代码
.o文件链接Linking
合并多个
.o文件和库文件,生成可执行文件。处理符号解析和地址重定位
# 汇编语言与机器指令的对应关系
汇编语言是机器语言的符号表示,便于人类阅读。
数据传送指令
mov 源,目标将数据从源位置传送到目标位置。举例:
movl $5,%eax将立即数5加载到寄存器%eax算数运算指令
addl 源,目标将源添加到目标,并将结果存储在目标位置。举例:
addl %ebx,%eax将%ebx的值加到%eax上subl 源,目标
控制流指令
jmp 地址无条件跳转到指定地址。cmpl 源,目标比较两个操作数,设置条件标志je 地址如果相等,跳转到地址
# 过程调用的机器级表示
函数调用和返回在机器级通过栈实现。
栈的使用
栈用于保存函数调用中的临时数据,如参数和返回地址。
push:将数据压入栈pop:从栈顶取出数据
函数调用指令
call 函数名:将返回地址压入栈,并跳转到函数入口。ret:从栈中取出返回地址,并跳转回调用者。
栈帧(Stack Frame)
每个函数都有自己的栈帧,用于保存①参数②局部变量③返回地址
# 第三章(二) 复杂数据类型的分配与访问
# 数组的分配与访问
数组的存储
数组的存储是连续的。地址以字节计算,则元素地址计算公式:
基 址 元 素 大 小 ( 字 节 ) 数组的访问
根据地址计算公式
,假设arr基址存储在%eax(累加器),i存储在%ecx(计数器),则其汇编语言如下:基 址 类 型 movl arr,%eax #将地址存到累加器中 movl %ecx,%edx #存储计数器值到数据寄存器中 imull $4,%edx #计算偏移量i*4 addl %edx,%eax #将偏移量加到基址上 movl %eax,%ebx #用基址寄存器存储地址
# 结构体的分配与访问
结构体的对其存储
- 每个成员的起始地址是其大小的整数倍
- 结构体总大小要满足最大成员大小的整数倍
结构体成员访问
每个成员的位置相对于结构体基址是固定的。
# 联合体的分配与访问
联合体是共享存储空间的多成员结构,所有成员起始地址相同。
特点:①只有一个成员占用存储空间②联合体大小等于最大成员大小。
# 第三章(三) 越界访问与缓冲区溢出
# 定义
越界访问:①超出数组地址范围②指针指向地址不合法
缓冲区溢出:程序向固定大小的内存空间中写入超出其容量的数据,覆盖其相邻内存。(导致未定义行为)
危害:①程序崩溃②安全漏洞③数据篡改
# 缓冲区溢出的工作机制
常见的攻击机制为覆盖栈上的返回地址,使其返回时跳转到攻击者控制的地址。
+------------------+
| 返回地址 | <-- 被覆盖的目标
+------------------+
| 参数 |
+------------------+
| 局部变量 | <-- 溢出的源
+------------------+
# 保护机制
编写安全代码:
- 避免直接操作裸指针。
- 在数组操作时检查边界条件。
使用安全函数:
- 替代
strcpy的函数:strncpy。 - 替代
gets的函数:fgets。
启用编译器保护:
- 堆栈保护:通过在栈帧中插入
canary值,防止返回地址被修改。 - 地址空间布局随机化(ASLR):随机化栈、堆和代码段的地址,增加攻击难度。
使用现代语言和工具:
- 选择内存安全的编程语言(如 Python、Rust)。
- 使用工具检测潜在的内存问题(如
Valgrind)。
# 第四章 (一)程序的链接
# 链接基本概念
- 静态链接:在编译时将所有依赖的目标文件和库文件合并到一个可执行文件中
- 优点:速度快
- 缺点:体积大
- 动态链接:程序运行时加载共享库(
.so或.dll文件)- 优点:体积小,库复用
- 缺点:需要额外的运行开销
# 目标文件格式
对于linux标准文件格式ELF格式,包括 .text 代码段 .data 已初始化数据段 .bss 未初始化数据段
- 可重定位目标文件(relocatable object file)
.o或.obj- 包含代码和数据,地址未确定(需要通过链接器确定)
- 每个
.o文件由一个源文件生成
- 可执行目标文件(executable object file)
.exe或无- 包含代码和数据,地址已确定
- 可直接加载到内存执行
- 共享目标文件(shared object file)
.so或.dll- 特殊的可重定位文件,运行时加载
# 符号解析与重定位
- 符号解析:将符号的引用(调用符号的位置)与定义(符号的位置,即全局变量地址)关联起来的过程。
- 重定位:将程序从虚拟地址空间映射到实际内存地址的过程。
- 重定位表:记录需要修改地址的地方
- 重定位过程:①合并多个目标的代码段和数据段②修正符号的引用地址为实际地址
# 动态链接的机制
延迟绑定:动态链接的函数在首次调用时才绑定地址。
实现:
- 在调用动态库函数时,跳转到一个中间层PLT。
- PLT调用动态链接器(动态加载函数地址)
优点
- 减少可执行文件体积:程序不包含库代码
- 提高代码复用率
# 第四章 (二)深入链接操作与ELF文件结构
# 链接的具体操作
预处理-编译-汇编-链接
gcc -E main.c -o main.i 输出预处理文件,展开所有宏和include
gcc -S main.i -o main.s 输出汇编代码,人类可读的代码main.s
gcc -c main.s -o main.o 输出目标文件main.o,包含机器指令和数据
gcc -c lib.c -o lib.o
gcc main.o lib.o -o program 输出可执行文件
最后一步的链接两大核心过程:
- 符号解析:链接器在标准库中查找printf的定义,并与main相关联。
- 重定位:将main.o和lib.o中的
.text和.data段合并,计算实际内存地址,并更新引用地址。
# ELF文件结构
主要段如下:
.text:存放代码,只读.data:存放已初始化的全局变量和静态变量.bss:存放未初始化的全局变量和静态变量;运行初始化为0,不占用文件大小- 符号表
.symtab:列出所有符号信息,包括定义和引用 - 重定位表
.rel.text和.rel.data:列出需要重定位的地址信息
# 动态链接库的加载
ELF动态链接文件结构
动态链接库是ELF文件的特殊形式,包含三张表
.dynsym动态符号表,列出库中可导出的符号.plt(Procedure Linkage Table)延迟绑定时的跳转表.got(Global Offset Table)存储全局变量和函数指针
动态链接库加载过程
- 程序运行时,操作系统加载ELF文件
- 动态链接器查找并加载所有依赖的共享库
- 使用PLT和GOT实现函数调用的延迟绑定