Skip to content

Latest commit

 

History

History
346 lines (223 loc) · 14.1 KB

csapp_note.md

File metadata and controls

346 lines (223 loc) · 14.1 KB

有符号数算术右移位(左端补 1),无符号数必须是逻辑右移位(左端补 1)

Java 中 >> 是算术右移(SAR),>>> 是逻辑右移(SHR)。xor swap

联想到 Rust 的除法和取模有个 Euclidean 版本

return strlen(a)-strlen(b)>0 这个代码 bug 的原因是发生了隐式的无符号整数转有符号整数,当不等式左边的值在有符号数的补码范围内的时候再隐式转换结果会导致误判

乘法优化成加法和移位

例如 x*14 = x * (21 + 22 + 2**3) = (x<<3) + (x<<2) + (x<<1)

IEEE 浮点数表示

符号位是最后一位,指数部分有 127 的偏移量来帮助表示负数因此指数 -1 表示成 126

规格化公式为 sign * 2**exp * (1+f) 尾数部分 1+f 会控制在 1-2 之间

如果指数部分全为零,则是非规格化公式 sign * 2**exp * f

!> 切记尾数部分是 1 表示十进制是 1/2**23 而不是表示十进制 1

所以 -1.0/1.0 的 fract 部分都是 0, sign 为 1 则表示负数

出现无限循环小数就截断

难怪分数部分是 2 的幂的话计算机很简单的精准表达

1.25f32 = 0b1_11111101_000000000000000000000

但如果分数部分是 0.1 这样无法精确表达只能用无限循环小数近似

1.1f32 = 0b1_11111100_011001100110011001101

一些特殊的浮点数

  • 0 很特殊,指数/尾数全为零
  • exp 部分全为 1 的话,如果尾数部分全为零则表示无穷大无穷小,否则表示 Nan

为什么 幂部分 不用补码表示负数

  1. exp 部分全为 1 表示无穷大,但是补码全 1 表示 -1
  2. 需要使用额外的算法来处理指数的加法和减法运算,因为补码的加法和减法运算并不能直接对应到实际的幂运算上。
  3. 补码表示法在表示负数时并不直观,因为负数的补码并不能直接反映对应的负数幂。

内存地址空间碎片整理

sleep/ready 状态的进程如果不是绝对地址的,可以调整下位置减少内存碎片

如果内存不够用,sleep/ready 的进程可以从内存搬去外存中(swap)

x86 汇编

x86 %rip 寄存器就是 riscv 的 pc

前四个入参 rdi,rsi,rdx,rcx 返回值 rax

movb/movw/movl/movq 的单位: u8/u16/u32/u64, movabsq 叫传送绝对的四字用于 mov 一个 64 位立即数到寄存器

movb (%rdi,%rcx),%al 是一条x86-64汇编指令。它的作用是将一个字节(8位)从内存中的某个地址通过寄存器传送到 AL 寄存器中。

这条指令的含义是:

(%rdi,%rcx) 表示一个内存地址,它由两个寄存器的组合构成,%rdi 是基址寄存器,%rcx 是偏移寄存器。它们的值会相加得到最后的内存地址。
movb 表示传送一个字节大小的数据。
%al 是一个8位寄存器,也称为累加器寄存器。它是 AX 寄存器的低8位。
因此,这条指令的作用是从内存地址 (%rdi + %rcx) 中读取一个字节的数据,并将其存储在 AL 寄存器中。

cltq

将AX寄存器的内容符号扩展到EAX寄存器中。指令的效果是将AX的最高位(符号位)的值复制到高位,扩展成一个32位的有符号数。

这个指令的格式是:cltq,它不需要任何操作数。

例如,如果AX寄存器的值为0xFFFF(-1的补码表示),那么执行cltq指令后,EAX寄存器的值将变为0xFFFFFFFF(-1的补码表示)。

cltq指令在需要将16位有符号数扩展为32位有符号数时很有用,以便在执行更大范围的计算时保持数据的正确性和一致性。

See also movsbq/movzbq

leaq

leaq src,dst 将 src 计算出的地址存入到 dst 甚至可以当乘法去用,参考 csapp 3.5.1 的例子

leaq (%rdx,%rdx,4),rax 表示 rdx+4*rdx=5rdx 寻址, rax=5rdx

练习 3.7 汇编代码翻译成 C

回答正确

scale2:
leaq (%rdi,%rdi,4), %rax // rax=5x
leaq (%rax,%rsi,2), %rax // rax+=2*y=5x+2y
leaq (%rax,%rdx,8), %rax // rax+=8*z=5x+2y+8z
ret

setz

cmp eax, ebx ; 比较eax和ebx的值
setz cl      ; 如果eax等于ebx,将cl设置为1,否则设置为0

cmp指令比较了eax和ebx的值,接着setz指令根据比较结果将cl寄存器设置为相应的值。

setz指令的功能是当前一条指令的结果为零(zero)时,将目标寄存器设置为1,否则将其设置为0

需要注意的是,setz指令只能用于设置8位寄存器或者指定的内存位置,不能直接用于通用寄存器。如果需要将结果存储到通用寄存器中,可以使用条件移动指令(如cmovz)来实现。

rep

rep指令通常与movs、stos、cmps、scas等指令结合使用,形成rep movs、rep stos、rep cmps、rep scas等指令。这些指令都涉及对数据块的重复操作

mov ecx, 10  ; 重复次数为10
mov edi, dest ; 目标地址
mov esi, src  ; 源地址
rep movs      ; 重复执行movs指令,从源地址复制数据到目标地址

cmovge(条件传送)

let ret = if xxx { } else { } 这样的代码比纯 if else 性能更好,处理器会预先两个分支都执行赋值,然后根据运行时的 xxx 选择其中一个赋值,也就是 cmovge 指令

假设分支预测成功率不到一半,那么传统 if else 分支预测的执行可能还不如两个分支都执行无分支预测的 cmovge 快

但条件传送指令要求所有分支的代码都无副作用

练习 3.33

回答正确

*u += a;
*v += b;
return sizeof(a) + sizeof(b);

movslq %edi, %rdi
addq %rdi, (%rdx)
addb %sil, (%rcx)
movl $6, %eax
ret

确定 abuv 四个入参的顺序和大小类型。提示有两种正确答案

movslq 让 edx 自身扩展从 32 位扩展到 64 位 rdx

首先 sizeof 是个 const fn 返回值 eax 是 6 说明 a/b 其中一个是 u16 一个是 u32

addq %rdi, (%rdx) 说明参数三 rdx 解引用 加上 入参一的数值,假设对应代码 *u += a 这句,

这样的话入参顺序是 a(rdi: u32),b(rsi: u16),u(rdx: &u64),v(rcx: &u8) 应该能自圆其说了

下一句是 参数四(rcx)解引用 加上 sli(rax 低 8 位), 第一句汇编是 参数一复制到 rax

gdb

stepi 4 执行 4 条指令
finish 运行到当前函数返回(step out?)
delete 1 删除断点 1
delete 删除所有断点
disas 反汇编当前函数
x/5i $rip-20 查看 pc 前五个指令

在x86-64架构中,一个指令的长度通常是变长的,可以是16位、32位或64位 所以 $rip-20 不一定表示向前偏移五个指令

缓冲区溢出应对机制

ASLR 地址空间随机化

进程地址空间和栈的位置每次运行都会不一样随机化

canary/guard value

金丝雀值每次运行随机生成保存在只读 section 中,在恢复寄存器和函数返回之前检查金丝雀值是否被修改,被修改则进程异常中止

虚拟内存

stack frame

栈帧(Stack Frame)是计算机程序在执行函数调用和返回时所使用的一种数据结构。它用于在运行时维护函数调用过程中的局部变量、函数参数、返回地址和其他与函数调用相关的信息。

当一个函数被调用时,计算机系统会为该函数创建一个新的栈帧。栈帧通常包括以下几个重要的组成部分:

局部变量:函数中定义的局部变量在栈帧中进行分配和管理。每个栈帧有自己的一块内存空间用于存储局部变量的值。

函数参数:传递给函数的参数值也存储在栈帧中。参数可以通过栈帧中的相对偏移进行访问。

返回地址:当函数调用完成后,程序需要返回到调用它的位置继续执行。为此,返回地址通常存储在栈帧中,以便在函数返回时正确地返回到调用点。

保存的寄存器:在函数调用前,某些寄存器中的值可能需要被保存下来,以便在函数调用结束后恢复原始值。这些寄存器的值通常也存储在栈帧中。

栈帧的创建和销毁是由程序的执行环境(如操作系统或编译器)负责的。它们通过栈数据结构来管理,每次函数调用和返回时,栈帧都会被推入或弹出栈中。栈帧的使用使得函数调用和返回能够高效地进行,并且在函数之间正确地传递数据和控制流。

xmm 寄存器/SIMD

快速扫了一眼并行计算的指令,略

ch2&ch3 缺了好多练习我的思考过程要看的书太多了时间关系我要控制学习速度效率只能跳过练习以后再补


ch4 流水线

参考 x86 设计我们的 Y86 指令集再通过硬件描述语言"造出处理器"去仿真模拟测试

每条指令的执行分成五个阶段(stage),每个时钟周期只有一个新指令进入处理器,所以处理器可以同时执行五个不同阶段的指令

  1. 取指(fetch)
  2. decode
  3. execute
  4. memory
  5. write_back (update regs)

流水线优点是增加吞吐量一个时钟周期同时干五个指令,缺点是单个指令的延迟更高

Y86 设计文档

x86 的子集,只支持少数寻址方法和 8-bit 整数,没有浮点数

  • registers: rax,rcx,rdx,rbx,rsp,rbp,rsi,rdi,r8-r14, rip
  • 条件码(riscv 没有): ZF(zero flag), SF(sign), OF(overflow) 保存上一条算术或逻辑指令造成的影响
  • stat: 是否出现异常

wget http://csapp.cs.cmu.edu/3e/sim.tar

make 报错无奈无法跑 y86 模拟器

冒险冲突问题(hazard)

一条指令依赖其他仍在流水线中的指令

HCL 硬件描述语言造处理器和流水线详细表格就快速扫过

通过暂停解决冲突

发现指令之间有冲突,流水线先停止新的输入指令,等运行中的指令跑完再说

通过转发解决冲突

需要额外的硬件设计

ch5 编译器优化

优化方法: 循环展开、常量折叠、数据库 pushdown

Compiler Performance Efficiency

用于编译器量化优化效果的指标

ch6 存储器

利用局部性原理(程序最近的数据访问只会集中在某部分数据) 提高缓存命中率/缓存的置换

ch7 linking

COMMON 段只有 relocatable 文件才有用于分配全局的弱符号 类似于 .bss

例如全局的 int *bufp1; 会分配到 .o 文件的 COMMON 段

练习题 7.2

(回答正确)

  • 规则 1 不允许多个同名的强符号

  • 规则 2 如有多个弱符号和一个强符号,选择强符号

  • 规则 3 如有多个弱符号则任意选一个(注意规则 3 跟程序员的自我修养书中不一样)

  • A.a: main.1 (抄答案的,学下格式)

  • A.b: main.1

  • B.a: 错误

  • B.b: 错误

  • C.a: x.2

  • C.b: x.2

练习题 7.3

!> 如果 gcc 参数中符号定义(库)在引用符号的目标文件之前,则符号引用无法解析会链接失败,所以大伙约定上会把库放在 gcc 入参的最后

  • A. p.o libx.a
  • B. p.o liby.a libx.a
  • C. p.o liby.a libx.a

回答错误: B. 是 p.o libx.a liby.a; C p.o libx.a liby.a libx.a

lazy binding 的好处是像 libc 这样有数千个函数的库,运行时请求哪个再解析避免启动时开销太大

library interposistioning

听上去像 LD_PRELOAD 那样的动态库劫持,不过还有编译时 gcc -DCOMPILETIME 和 链接时 --wrap 打桩

ch8 异常&进程

interrupt,trap,fault(例如缺页异常),abort(例如除以零、DRAM 硬件奇偶校验错误)

练习题 8.1

AB 并发, AC 不是, BC 并发

进程组

默认下子进程和父进程同属一个进程组,kill -1234 会给进程组 1234 的所有进程发信号

session 是一个包含一个或多个进程组的过程

注意信号处理函数内要保存/恢复 errno

用 volatile+atomic 修饰信号回调中要修改的全局变量

强迫每次读全局变量前都要从内存缓存重新读一次

sigsuspend

setjmp/longjmp

longjmp函数接受一个jmp_buf类型的参数和一个整数值。它会将之前保存的跳转点上下文信息恢复,并将跳转点的返回值设置为val

ch9 虚拟内存

L1-L3 cache is sram, memory is dram

多级页表项的好处——节约内存

假设 PTE(页表项) 占用 4byte(权限位+页表号+偏移), 一个一级 PTE 页表有 1024 个一级 PTE 可以映射 1024*1024 个二级 PTE

第一,如果一级页表的第一个 PTE 为空,则对应的 1024 个二级 PTE 都不会存在,对进程来说 4G 的虚拟内存绝大部分都是未分配的

第二,只有一级页表项才需要常驻内存和经常使用的二级页表才会在内存中

练习题 9.4 怎么计算地址这块还是有点不理解(书中 9.6 内容 https://hansimov.gitbook.io/csapp/part2/ch09-virtual-memory/9.6-address-translation)

内部碎片/外部碎片

内部碎片: 为了内存对齐额外申请的空间

外部碎片: 两段连续内存分配之间的小间隙无法使用

GC: Mark&Sweep 算法

最早出现在 Lisp 垃圾回收的时候,从堆内存开始遍历,发现有的内存分配节点是 unreachable 的说明这是垃圾内存没有引用可以回收

ch10 文件

这章好水,所有内容我之前读 《beginning linux programming》都学过了

v-node 表,所有进程共享的打开的文件

练习题 10.1

fd2=3

ch11 网络编程

TCP/UDP 扩展的 IP 协议,使得数据包可以在进程间传播而不是主机间传送的粒度

cat /etc/services | grep postgres

getaddrinfo类似于DNS解析,输入域名输出sockaddr。getnameinfo 类似反向DNS解析,输入sockaddr输出域名

ch12 并发编程

建议 pthread_join 回收线程资源,如果不打算 join 等线程结束则用 pthread_detach(pthread_self())

Rust 的读写锁(写优先),当两个获取写锁的线程来回传递,这样读锁是不是会饥饿

srand 所有线程共用一个随机数 seed 建议用线程安全的 rand_r

RDTSC指令是用来读取时间戳计数器(TSC)的值,TSC是一个递增的计数器,它记录了处理器从上电以来经过的时钟周期数。虽然可以将其用作随机种子,但它并不是真正的随机数生成器。

RDRAND指令是Intel Ivy Bridge微架构引入的一个指令,用于从硬件随机数生成器(Hardware Random Number Generator,HRNG)中读取随机数。HRNG的实现依赖于物理上的随机事件,如热噪声或放射性衰变。这使得RDRAND生成的数看起来是随机的,但并不是真正的随机数。

要生成真正的随机数,通常需要依赖外部的随机源,如操作系统提供的随机数生成器或专用的硬件随机数生成器