Skip to content

MongXinChan/csapp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

csapp

CMU15213 CSAPP course files stored in Linux (WSL)

CS:APP Docker and Materials

CS:APP is an excellent material for learning computer systems and systems programming. However, it is inconvenient to use a virtual machine for self-learners. In this repo, I build a Docker image with most pre-requistes installed and attached all lab materials in it. You can directly use this Docker image xieguochao/csapp.

The original materials of CS:APP is from CMU: CS:APP labs.

The code server is based on https://github.com/cdr/code-server.

How to Use It?

Install Docker or Podman

Clone Repository

  1. git clone --branch 1.8 https://github.com/XieGuochao/csapp.git.
  2. cd csapp.

Build Image (Optional)

  1. cd csapp-docker
  2. docker build -t csapp ..

Run

Under the root directory.

docker run -p 7777:7777 -v "$PWD/labs:/home/csapp/project" csapp

Then you can access your labs via browser http://localhost:7777/ with password csapp. You can find all files in labs under /home/csapp/project.

前言

在我看来csapp更像是一个c语言的基础教程,里面涉及了许多更底层的原理,比如为什么强制类型转化只是把解释类型的作用更换了,而其比特位不变?为什么3<4<3最后返回结果是0?负数的反码+1为什么等于反码?

这些都是CSAPP所涉及到的内容,不过由于时间关系我会省去一些控制流相关的章节,毕竟我们知道if-else,switch等都是需要逻辑完备的东西。底层细节的布尔代数就不再赘述。

chapter2

小端,大端存储

int val = 0x887654321;
byte_pointer valp=(byte_pointer) &val;
show_bytes(valp,1);
show_bytes(valp,2);
show_bytes(valp,3);
A:小端法21          大端法:87
B:小端法21 43       大端法:87 65
C:小端法21 43 65    大端法:87 65 43

逻辑运算

表达式 结果
!0x41 0x00
!0x00 0x01
!!0x41 0x01
0x69&&0x55 0x01
0x69||0x55 0x01

只使用位级和逻辑运算,编写一个C表达式,它等价于x==y。换句话说,x和y相等时它讲返回1,否则就返回0

return !(x^y);

位移运算

C 语言还提供了一组移位运算,向左或者向右移动位模式。对于一个位表示为 $[x_{w−1},…,x_0]$,x0的操作数x,C表达式 x≪k会生成一个值,其位表示为$[x_{w−k−1},x_{w−k−2},…,x_0,0,…,0]$。也就是说,x向左移动k位,丢弃最高的k位,并在右端补k个0。移位量应该是一个 0∼w−1之间的值。移位运算是从左至右可结合的,所以$x \leq j\leq k$等价于$x\leq j \leq k$。

有一个相应的右移运算 $x\geq k$,但是它的行为有点微妙。一般而言,机器支持两种形式的右移:逻辑右移和算术右移。逻辑右移在左端补 k个0,得到的结果是$ [0,…,0,x_{w−1},x_{w−2},…,x_k]$。算术右移是在左端补 k个最高有效位的值,得到的结果是[$x_{w−1},…,x_{w−1},x_{w−1},x_{w−2},…,x_{k}$]。这种做法看上去可能有点奇怪,但是我们会发现它对有符号整数数据的运算非常有用。

让我们来看一个例子,下面的表给出了一个8位参数 x 的两个不同的值做不同的移位操作得到的结果:

操作
参数 x [01100011] [10010101]
$x &lt;&lt; 4$ [00110000] [01010000]
$x &gt;&gt; 4$(逻辑右移) [00000110] [00001001]
$x &gt;&gt; 4$(算术右移) [00000110] [11111001]

斜体的数字表示的是最右端 (左移) 或最左端 (右移) 填充的值。可以看到除了一个条目之外,其他的都包含填充 0。唯一的例外是算术右移 [10010101]的情况。因为操作数的最高位是1,填充的值就是 1。

C 语言标准并没有明确定义对于有符号数应该使用哪种类型的右移——算术右移或者逻辑右移都可以。不幸地,这就意味着任何假设一种或者另一种右移形式的代码都可能会遇到可移植性问题。然而,实际上,几乎所有的编译器/机器组合都对有符号数使用算术右移。

与C相比,Java对于如何进行右移有明确的定义。表达是x>>k会将x算数右移k个位置,而x>>>k会对x做逻辑右移操作。

算数右移会保留符号位,因此可以直接将无符号数转换为有符号数,而逻辑右移会将符号位补0,因此需要先左移24位,再右移24位,

#include <stdio.h>

int fun1(unsigned word){
    return (int)((word<<24)>>24);
}
//算数右移会保留符号位,因此可以直接将无符号数转换为有符号数。
//而逻辑右移会将符号位补0,因此需要先左移24位,再右移24位,

int fun2(unsigned word){
    return ((int)(word<<24)>>24);
}
//将无符号数转换为有符号数后,再进行算数右移。
//注意:在C语言中,右移操作符的行为取决于操作数的类型。

int main(){
    unsigned int a[]={0x00000076,0x87654321,0x000000c9,0xEDCBA987};
    for(int i=0; i<4; i++){
        printf("fun1(0x%08x) = %d\n", a[i], fun1(a[i]));
        printf("fun2(0x%08x) = %d\n", a[i], fun2(a[i]));
    }
    return 0;
}

image-20250601162747062

操作符优先级问题

1<<2+3<<4,在程序看起来则是1<<(2+3)<<4

image-20250601163220034

原码,补码,反码

符号 类型 含义
B2Tw 函数 二进制转补码
B2Uw 函数 二进制转无符号数
U2Bw 函数 无符号数转二进制
U2Tw 函数 无符号数转补码
T2Bw 函数 补码转二进制
T2Uw 函数 补码转无符号数
TMinw 常数 最小补码值
TMaxw 常数 最大补码值
UMaxw 常数 最大无符号数
+tw 操作 补码加法
+uw 操作 无符号数加法
*tw 操作 补码乘法
*uw 操作 无符号数乘法
-tw 操作 补码取反
-uw 操作 无符号数取反

$$ TMax_w=\sum^{w-2}_{i=0} 2^i=2^{w-1}-1\\ TMin_w=-2^{w-1}$$

Note

反码的计算如下:

  1. 对于正数,反码与原码相同
  2. 对于负数,符号位保持不变,其余按位取反。

e.g:-3的原码是1011,反码为1100

无符号数编码定义

对向量$\vec{x}=[x_{w-1},x_{w-2},...,w_0]:$ $$ B2U_w(\vec{x})=\sum^{w-1}_{i=0} x_i2^i $$

补码编码

在这个定义中,将字的最高有效位解释为负权(negative weight)。我们用函数$B2T_{w}$(Binary to Two's-complement,长度为$w$)来表示: A

**原理:**补码编码的定义

对向量$\vec{x}=[x_{w-1},x_{w-2},...,w_0]:$ $$ B2T_w(\vec{x})=-x_{w-1}2^{w-1}+\sum^{w-2}{i=0} x_i2^i $$ 最高有效位$x{w-1}$也成为符号位,它的“权重”为$-2^{w-1}$,是无符号表示中权重的负数。 $$ B2T_4([0001])=-02^3+02^2+02^1+12^0=0+0+0+1=1\ B2T_4([0101])=-02^3+12^2+02^1+12^0=0+4+0+1=5\ B2T_4([1011])=-12^3+02^2+12^1+12^0=-8+0+2+1=-5\ B2T_4([1111])=-12^3+12^2+12^1+12^0=-8+4+2+1=-1 $$ **原理:**补码(Two's complement)编码的唯一性

函数$BWT_w$是一个双射。

我们定义函数$T2B_w$(即"补码到二进制")作为$BWT_w$的反函数,其长度为$w$位模式到$TMin_w$和$TMax_w$之间数字的映射,写作$B2T_{w}:{0,1}^w\rightarrow{TMin_w,...,TMax_w}$

image-20250601170349861

补码的范围是不对称的:$|TMin|=|TMax|+1$,也就是说,TMin没有与之对应的正数。

有符号数的表达方式

反码(Ones' Complement):除了最高有效位的权是$-(2^{w-1}-1)$而不是$-2^{w-1}$,它和补码是一样的: $$ B2O_w(\vec{x})=-x_{w-1}(2^{w-1}-1)+\sum^{w-2}{i=0}x_i2^i $$ 原码(Sign-Manitude):最高有效位是符号位,用来确定剩下的位应该取负权还是正权: $$ B2S_w(\vec{x})=(-1)^{x{w-1}}\sum^{w-2}_{i=0}x_i2^i $$

    short x = 12345;
    short mx = -x;

    printf("Bytes for x (12345):\n");
    show_bytes((byte_pointer) &x, sizeof(short));

    printf("Bytes for mx (-12345):\n");
    show_bytes((byte_pointer) &mx, sizeof(short));

在大端法机器上运行时,这段代码的输出为30 39cf c7,指明x的十六进制表示为0x3039,而mx的十六进制表示为0xCFC7

image-20250601172013661

(在WSL中是小端存储)

image-20250810151026271

有符号数和无符号数之间的转换(P49)

E.g:

short int v =-12345;
unsigned short uv =(unsigned short)v;
printf("v = %d,uv =%ud",u,uv);

原理:补码转换为无符号数 对满足$TMin_w \leq x \leq TMax_w$的x有: $$ T2U_w(x)= \begin{cases} x + 2^w & \text{if } x < 0 \ x & \text{if } x \geq 0 \end{cases} $$

推导:补码转换为为无符号数 $$ B2U_w(T2B_w(x))=T2U_w=x+x_{w-1}2^w $$

原理:无符号数转换为补码,对满足$0\leq u \leq UMax_w$的u有 $$ U2T_w(u)= \begin{cases} u, &\text{if } u\leq TMax_w \ u-2^{w},&\text{if } u> TMax_w \end{cases} $$

image-20250810152411093

扩展一个数字的位表示

常用的运算是在不同字长的整数之间转换,同时又保持数值不变。当然,当目标数据类型太小以至于不能表示想要的值得时候,这根本就是不可能的。然而,当一个较小的数据类型转换到一个较大的数据类型,应该是总能可能的。

要将一个无符号数转换为一个更大的数据类型,我们只需要简单地在表示的开头添加0。这种运算被称为零扩展 (zero extension),表示原理如下:

原理: 无符号数的零扩展

定义宽度为 $w$ 的位向量 $\vec{u}=[u_w−1,u_w−2,cdots,u_0]$ 和宽度为 $w′$ 的位向量 $\vec{u^′}=[0,cdots,0,u_w−1,u_w−2,cdots,u_0]$,其中 $w′&gt;w$。则 $$ B2U_w(\vec{u})=B2U_w′(\vec{u^′}) $$ 要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展 (sign extension),在表示中添加最高有效位的值,表示方式为如下原理。我们用蓝色标出符号位以突出它在符号扩展中的角色。

原理: 补码数的符号扩展

定义宽度为 w 的位向量$\vec{x}=[x_w−1,x_w−2,cdots,x_0]$和宽度为 w′ 的位向量 $\vec{x^′}=[x_w−1,cdots,x_w−1,x_w−1,x_w−2,cdots,x_0]$,其中 $w′&gt;w$。则 $$ B2T_w(\vec{x})=B2T_w′(\vec{x^′}) $$ 例如,考虑下面的代码:

short sx = -12345;  /* -12345 */
unsigned short usx = sx; /* 53191 */
int x = sx;   /* -12345 */
unsigned ux = usx;  /* 53191 */

printf("sx = %d:\t", sx);
show_bytes((byte_pointer) &sx, sizeof(short));
printf("usx = %u:\t", usx);
show_bytes((byte_pointer) &usx, sizeof(unsigned short));
printf("x = %d:\t", x);
show_bytes((byte_pointer) &x, sizeof(int));
printf("ux = %u:\t", ux);
show_bytes((byte_pointer) &ux, sizeof(unsigned));

在采用补码表示的 32 位大端法机器上运行这段代码时,打印出如下输出:

sx = -12345: cf c7
usx = 53191: cf c7
x = -12345: ff ff cf c7
ux = 53191: 00 00 cf c7

我们看到,**尽管 -12345 的补码表示和 53191 的无符号表示在 16 位字长时是相同的,但是在 32 位字长时却是不相同的。**特别地,-12345 的十六进制表示为 $0xFFFFCFC7$,而 53191 的十六进制表示为 $0x0000CFC7$。==前者使用的是符号扩展——最开头加了 16 位,都是最高有效位 1,表示为十六进制就是 $0xFFFF$。后者开头使用 16 个 0 来扩展,表示为十六进制就是 $0x0000$==。

Tip

这里的意思是说:

  • 符号扩展是针对有符号数的,它通过复制符号位来保持数值在扩展后不变。

  • 零扩展是针对无符号数的,它通过在前面填充 0 来保持数值在扩展后不变。

虽然在 16 位时,-12345 的补码和 53191 的无符号表示在二进制上是相同的,但由于它们是不同类型的数(一个有符号,一个无符号),当它们被扩展到 32 位时,会遵循不同的扩展规则,从而得到不同的 32 位表示。

image-20250810154152831

从字长 $w=3$$w=4$ 的符号扩展的结果。位向量 $[101]$ 表示值 $−4+1=−3$

对它应用符号扩展,得到位向量 [1101],表示的值 −8+4+1=−3。我们可以看到,对于 w=4,最高两位位的组合值是 −8+4=−4,与 w=3 时符号位的值相同。类似地,位向量 [111] 和 [1111] 都表示值 −1。

有了这个直觉,我们现在可以展示保持补码值的符号扩展。

推导:补码数值的符号扩展

令 w′=w+k,我们想要证明的是 $$ B2T_{w+k}([x_{w-1}, \cdots, x_{w-1}, x_{w-2}, \cdots, x_0]) = B2T_w([x_{w-1}, x_{w-2}, \cdots, x_0]) $$ 下面的证明是对 k 进行归纳。也就是说,如果我们能够证明符号扩展一位保持了数值不变,那么符号扩展任意位都能保持这种属性。因此,证明的任务就变成了: $$ B2T_{w+1}([x_{w−1},x_{w−1},x_{w−2},⋯,x_0])=B2Tw([x_{w−1},x_{w−1},⋯,x_0]) $$

用等式 (2.3) 展开左边的表达式,得到: $$ \begin{align*}

B2T_{w+1}([x_{w-1}, x_{w-1}, x_{w-2}, \cdots, x_0]) &= -x_{w-1}2^w + \sum_{i=0}^{w-1} x_i 2^i \

&= -x_{w-1}2^w + x_{w-1}2^{w-1} + \sum_{i=0}^{w-2} x_i 2^i \

&= -x_{w-1}(2^w - 2^{w-1}) + \sum_{i=0}^{w-2} x_i 2^i \

&= -x_{w-1}2^{w-1} + \sum_{i=0}^{w-2} x_i 2^i \

&= B2T_w([x_{w-1}, x_{w-2}, \cdots, x_0])

\end{align*} $$ 我们使用的关键属性是 $2^w−2^{w−1}=2^{w−1}$。因此,加上一个权值为 $-2^w$ 的位,和一个权值为 $−2^{w−1}$ 的位转换为一个权值为 $2^{w−1}$ 的位,这两项运算的综合效果就会保持原始的数值。

截断数字

当一个 w 位的数 $x=[x_{w−1},x_{w−2},⋯,x0]$ 截断为一个 $k$ 位数字时,我们会丢弃高 $w−k$ 位,得到一个位向量 $x′=[x_{k−1},x_{k−2},⋯,x_{0}]$。截断一个数字可能会改变它的值——溢出的一种形式。对于一个无符号数,我们可以很容易得出其数值结果。

原理:截断无符号数

令等于 w 位的向量$ x=[x_{w−1},x_{w−2},⋯,x0]$,而 x′ 是将其截断为 k 位的结构:$x′=[x_{k−1},x_{k−2},⋯,x_{0}]$。令 $x=B2Uw(x)$。则 $ x′= x\mod2^k$。

该原理背后的直觉是所有被截去的位在其权重形式都为 $2^i$,其中 i≥k,因此,每一个权在取模操作下结果都为零。可用如下推导表示:

推导:截断无符号数

通过对等式 (2.1) 应用取模运算就可以看到: $$ \begin{align*} B2U_w([x_{w-1}, x_{w-2}, \cdots, x_0]) \bmod 2^k &=\left[\sum_{i=0}^{w-1} x_i 2^i\right] \bmod 2^k \ &= \sum_{i=0}^{w-1} (x_i 2^i \bmod 2^k) \ &= \sum_{i=0}^{k-1} x_i 2^i \ &= B2U_k([x_{k-1}, x_{k-2}, \cdots, x_0]) \end{align*} $$ 在这些推导中,我们利用了属性:对于任何 $i≥k,2^imod2^k=0$

Note

实际上这些截断也只是把目标位数的赋值变为0。

可以从下面代码中遇见:

int u =32768;
short a = (short)u;
unsigned short ua = (unsigned short)a;
printf("u=%d, a= %d \n",u,a);
printf("a = %d, uv = %u\n", a, ua);

image-20250810161319752

  • short a = (short)u; 发生了截断。int 通常是 32 位,short 是 16 位。32768 在 32 位表示中是一个正数,但它在 16 位补码中的最高位是 1,因此会被解释为负数。$32768bmod2^{16}=32768$,而 $U2T_{16}(32768)=32768−65536=−32768$。所以 a 的值是 -32768。
  • unsigned short ua = (unsigned short)a;a 的位模式(补码)重新解释为无符号数,因此 ua 的值是 32768。

补码截断也具有相似的属性,只不过要将最高位转换为符号位

原理:截断补码数

令等于 w 位的向量 $x=[x_{w−1},x_{w−2},⋯,x_0]$,而 x′ 是将其截断为 k 位的结构:$x′=[x_{k−1},x_{k−2},⋯,x_{0}]$。令$x=B2Tw(x)$。则 $x′=U2T_k(x \mod2^k)$

在这个公式中,$x \mod2^k$ 将是 0 到 $2^k−1$ 之间的一个数。对其应用函数 $U2T_k$ 产生的效果是把最高有效位 $x_{k−1}$ 的权重从 $2^{k−1}$ 转换为 $−2^{k−1}$。举例来看,将数值 x=53191 从 int 转换为 short。由于 $2^{16}=65536≥x$,我们有 $x\mod2^{16}=x$。但是,当我们把这个数转换为 16 位的补码时,我们得到 x′=53191−65536=−12345。

推导:截断补码数

使用与无符号数截断相同的参数,则有 $$ B2Uw([x_{w−1},x_{w−2},⋯,x0])mod2^k=B2U_k([x_{k−1},x_{k−2},⋯,x_0]) $$

也就是说,$x\mod2^k$ 能够被一个位级表示为 $[x_{k−1},x_{k−2},⋯,x_0]$ 的无符号数表示。将其转换为补码数则有 $x′=U2T_k(x\mod2^k)$

总而言之,无符号数的截断结果是: $$ B2U_k([x_{k-1}, x_{k-2}, \cdots, x_0]) = B2U_w([x_{w-1}, x_{w-2}, \cdots, x_0]) \bmod 2^k \quad (2.9) $$ 而补码数字的截断结果是: $$ B2Tk([x_{k−1},x_{k−2},⋯,x_{0}])=U2Tk(B2Uw([x_{w−1},x_{w−2},⋯,x0])mod2^k) \quad(2.10) $$

Note

换而言之

image-20250810161842990

数据的溢出

由于数据的加减是通过补码进行运算的。我们通常会将数据从原码转换到补码,即取反码+1

image-20250810162830094

乘法

无符号数乘法

对于满足$0\leq x,y\leq UMax_w$的$x$和$y$有: $$ x\times^{u}_{w}y = (x\times y) mod \ 2^w \quad\quad\quad(2.16) $$

补码乘法

对满足$TMin_w \leq x,y\leq TMax_w$的x和y有: $$ x\times ^{t}_wy=U2T_w(x\times y)mod2^w) \quad \quad \quad (2.17) $$

Tip

对于双精度浮点数,有负零和正零之分,但是他们取值是相同的:

While -0.0 and 0.0 have the same mathematical value, they are represented differently in a computer's memory according to the IEEE 754 standard for floating-point numbers. They have the same value because the standard defines them as numerically equal, but they exist as distinct bit patterns to preserve directional information from certain calculations.

he Role of Signs in Floating-Point Numbers

Floating-point numbers are stored in three parts: a sign bit, an exponent, and a mantissa (or significand).

  • Sign Bit: This single bit determines if the number is positive (0) or negative (1). For both 0.0 and -0.0, the exponent and mantissa bits are all zero. The only difference is the sign bit. For 0.0, the sign bit is 0, while for -0.0, the sign bit is 1.

  • Numerical Equality: The IEEE 754 standard specifies that when a comparison is made (e.g., using ==), the sign bit is ignored for zero values. This means the result of -0.0 == 0.0 will always be true. This is why they have the same value in a logical or mathematical sense, even though they have different bit representations.

    Why the Distinction is Useful

    The distinction between 0.0 and -0.0 is critical for preserving sign information, particularly in calculations involving division or functions like atan2.

For example, consider the function 1.0 / x.

  • If x approaches zero from the positive side (e.g., 0.0001, 0.00001, etc.), the result approaches positive infinity. This is represented in the floating-point standard as +INF.
  • If x approaches zero from the negative side (e.g., -0.0001, -0.00001, etc.), the result approaches negative infinity. This is represented as -INF.

If a calculation results in a value that is mathematically zero but came from a negative direction, the use of -0.0 preserves this directional information. This is useful for numerical stability and correctness in specific algorithms, especially those that deal with complex numbers or trigonometry.

About

CMU15213 CSAPP course files stored in Linux (WSL)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published