Skip to main content

11-14-11-26学习双周记

· 17 min read
ayanami

11.14-11.26 学习双周记:

最近事务稍多,且更多时间花在了写代码上且略摆,故学习的知识型内容较少

完成了 pa1 和 081 的 lab1


小知识 get:

搜索引擎的小技巧:英文符号

搜索引擎对符号的支持是很差的,甚至会被识别成通配符之类

正确的搜索符号的姿势是使用英文代替符号

e.g.(google)可以搜搜对比一下

python __ 和 python bundle 的对比

* in bash 和 asterisk in bash 的对比

常用特殊符号英文:

? question mark ! exclamation mask

` backtick $ dollar - hyphen # sharp or hash ~ tilde

& ampersand * asterisk _ underscore ^ caret , comma

. dot / forward slash \ backslash < > angle bracket [] bracket () brace

"" quotation mark

正则表达式错用让 cpu 占用率提升 N 倍(雾)

$(\d+)*^ : 一个 * 是如何让算法复杂度从 O(N)变成 O(2^N^)的,匹配 1111111111111111111111111111a 直接爆炸

$(1+1+)+0^ : 同理 匹配 11111111111111111111111111111 就寄了

原理:参考这个,写得真不错

画个图就懂了


配置 wsl 的代理

使用 CFW,打开系统代理、允许局域网链接,使用此脚本

# 转载自 https://www.cnblogs.com/jaycethanks/p/17360464.html
# 获取wsl虚拟机的ip, 并代理至windows 7890 端口
echo "设定wsl网络代理到7890外网访问端口......"
host_ip=$(cat /etc/resolv.conf |grep "nameserver" |cut -f 2 -d " ")
export ALL_PROXY="http://$host_ip:7890"
# curl 命令检查,并仅输出状态码
echo "尝试通过curl命令检查 google 是否可以访问......返回状态码为:"
curl -s -o /dev/null -w "%{http_code}\n" https://www.google.com

放到~/.bashrc

source ~/.bashrc即可

碎碎念:本来配置 wsl 代理主要就是为了解决代码补全在 wsl 之中(即 vsc 开启 remote 模式下)不可用的问题,结果配完之后看到这样的 issue

yxw820603 commented on Oct 7 • edited

经过简单测试,确定插件无法在 Remote-SSH 的情况下工作。

不过 codeium 倒是折腾半天能用了,虽然也不知道为什么,wsl 下的效果比起正常本机差不少......延迟很大

写码的时候还碰到一个很搞的事情

grep "-D"会把"-D"解释成参数-D,""解释成字面量

想到的规避方法是敲个空格 grep "[space]-D"


浅入理解断点和调试器

主要参考这两篇 1,2

在写知识之前,不如先问自己几个问题:

  • debugger 的实现原理是什么?
  • 断点(breakpoint)和监视点(watchpoint)的区别?
  • 断点有哪些实现方法?具体到 gdb 之中,它是怎么实现的?

debugger 的最基本原理,就是这样的代码

int main(int argc, char** argv)
{
pid_t child_pid;

if (argc < 2) {
fprintf(stderr, "Expected a program name as argument\n");
return -1;
}

child_pid = fork();
if (child_pid == 0)
run_target(argv[1]);
else if (child_pid > 0)
run_debugger(child_pid);
else {
perror("fork");
return -1;
}

return 0;
}

debugger 作为父进程,使用 fork 调出了需要 debug 的子进程,然后通过某种方法和子进程交互(操控子进程)

那么就引出了这样的问题:

  1. 怎么操控的(允许操控的机制)
  2. 操控之后的控制流
  3. 已经控制了,debug 信息从哪来(如果不做处理,高级语言编译之后是没有原来的代码行数、变量符号等信息的)

又可以进一步总结出以下几点,

debug 需要信息:

  1. 在高级代码--->汇编的过程之中,我们需要代码的一一对应关系,比如第几行的高级代码对应第几行(到第几行)的汇编代码

  2. 在汇编代码--->可执行文件的过程之中,我们需要将有用的信息保存在可执行文件内

debug 需要断点:

  1. 需要有一种方法在 gdb 或者在可执行文件之中达成中断
  2. 进一步地,需要让 gdb(父进程)能够控制、监视、改变子进程(被调试的进程)

解决了这几个问题,理论上就能产生 debugger

问题 1,很遗憾,由于现代编译器的优化和现代编程语言的复杂性,源程序语言和编译完成的汇编,又或者说最后执行的机器指令很难完美对应,编译器开启对应的编译指令之后也只能做到近似(不能完全相信 back trace!)但大体上还是对的。(也可以强制让编译器不优化)

下图转载: gdb br的失败例子

image-20231123221703870

image-20231123221731588

问题 2,这就是为什么需要 gcc -g 的理由,也是为什么可以不止于对内存地址设断点,还能对一个函数,一行源代码设断点的原因

现代编译器在将高级代码转换为高级代码方面做得很好,其缩进和嵌套的控制结构以及任意类型的变量可以很好地转换为一大堆称为机器代码的位,其唯一目的是在目标 CPU 上尽可能快地运行。大多数 C 行被转换为多个机器代码指令。变量被推到各处 - 进入堆栈、寄存器或完全优化。结构和对象甚至不存在于生成的代码中 - 它们只是一个抽象,被转换为硬编码的偏移量到内存缓冲区中。

那么,当您要求调试器在某个函数的入口处中断时,调试器如何知道在哪里停止呢?当你向它询问变量的值时,它是如何找到要显示的内容的?答案是 - 调试信息

调试信息由编译器与机器代码一起生成。它是可执行程序和原始源代码之间关系的表示。这些信息被编码为预定义的格式,并与机器代码一起存储。多年来,为不同的平台和可执行文件发明了许多这样的格式。由于本文的目的不是调查这些格式的历史,而是展示它们的工作原理,因此我们必须确定一些事情。这将是 DWARF,它今天几乎无处不在地用作 Linux 和其他 Unix-y 平台上 ELF 可执行文件的调试信息格式。

对应 elf 文件中的.debug_**段

image-20231123223152372

如何进一步阅读?

objdump --dwarf=info

info 可以换成别的

image-20231123223310051

image-20231123223328905

问题 3、4

debug 需要断点,需要某种可恢复的中断,怎么做?

  • 软件支持
  • 硬件支持

先讲硬件支持是怎么实现的。如果设备有实现硬件 debug,它会在内存之中占据一段特殊的位置,使得这个硬件支持对 cpu 是可见的

(cpu 也实现了 debug 的控制寄存器和控制单元)。而 gdb 在 debug 时,会先确定本机的架构和硬件信息,之后根据硬件信息去寻找相关的 debug 是否有硬件支持。

而这个硬件支持表现在能够硬件上单步执行,通过比较器设置断点,etc

Gateway between re-purposed JTAG bit protocol and debug logic

Debug hardware often visible in a special memory address space

E.g. (gdb) stop requires writing 0x1 (Halt Request) to address 0x090 (Debugger Run Control Register) of the CPU debug unit.

  • Shift 4 bits into IR

  • Shift 34 bits into DR

  • Shift 4 bits into IR

  • Shift 34 bits into DR

  • Shift 34 bits into DR

另一种就是软件支持,

首先是中断,当程序运行到断点的时候,它应该向 gdb 发出一个中断信号(比如 SIGTRAP),之后 gdb 程序(父进程)接收到中断信号后,辨别出这个是断点产生的中断还是程序正常运行的中断,并加以处理

image-20231127222802586

然后是 ptrace 系统调用,这个系统调用允许一个进程去得到另一个进程的控制权,包括监视、改变、发送命令等

void run_target(const char* programname)
{
procmsg("target started. will run '%s'\n", programname);

/* Allow tracing of this process */
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
perror("ptrace");
return;
}

/* Replace this process's image with the given program */
execl(programname, programname, 0);
}

而有了中断之后,一种简单的实现方法可能是这样的:

在指定的地址上设一个监视点 w

首先,每次运行一条指令之后我都切换到 debugger,比对一下 pc 和监视地址是否相同,如果相同,那么我就停止

这种切换的机制来源于:

  1. wait() 不止在子进程 exit 时才会退出,在子进程触发中断时也会返回,并保存一些中断信息
  2. ptrace 调用定义了一个特殊的 request PTRACE_SINGLESTEP,会告诉 OS 启动被监控进程,但是一条指令后停止(pc+4),并产生中断通知父进程
void run_debugger(pid_t child_pid)
{
int wait_status;
unsigned icounter = 0;
procmsg("debugger started\n");

/* Wait for child to stop on its first instruction */
wait(&wait_status);

while (WIFSTOPPED(wait_status)) {
icounter++;
/* Make the child execute another instruction */
if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {
perror("ptrace");
return;
}

/* Wait for child to stop on its next instruction */
wait(&wait_status);
}

procmsg("the child executed %u instructions\n", icounter);
}

我们当然可以用这种单步执行比对 pc 的“监视点”方法实现断点,但这个方法的问题是,每执行一条指令都要不断切换进程,效率太低了

jyy 介绍, gdb 用了一种很神奇的方法: int 3(注意这个 int 不是 integer 的 int)偷龙转凤

int 3 是一个单字节 x86 指令,作用就是简单地发出一个中断 SIGTRAP

单字节使得它能够替换到任何一条指令的开头,并且不会覆盖两条及以上的指令

在设置断点的时候,gdb 可以将断点处的指令保存起来,之后替换它的首个字节为 int 3

Instruction at the given address is read, saved and replaced with a breakpoint:

​ - either a special instruction, // SIGTRAP, int 3

​ - or an undefined encoding. // SIGILL

之后程序正常运行,执行到 int 3 时产生中断(SIGTRAP,int 3 就是第三号 TRAP,x86 的调试器中断),而 gdb 程序作为父进程收到这个信号,达成中断(通过 ptrace 设置了中断的 handler),之后也可以将原来的指令替换回来继续执行。

继续执行的细节:

实际上在有了 ptrace 的调用之后,每当被监控程序执行 exec 的时候,就会发出一个中断

Indicates that this process is to be traced by its parent. Any signal (except SIGKILL) delivered to this process will cause it to stop and its parent to be notified via wait(). Also, all subsequent calls to exec() by this process will cause a SIGTRAP to be sent to it, giving the parent a chance to gain control before the new program begins execution. A process probably shouldn't make this request if its parent isn't expecting to trace it. (pid, addr, and data are ignored.) 指示此过程将由其父级跟踪。传递给此进程的任何信号(SIGKILL 除外)都会导致它停止,并通过 wait() 通知其父级。此外,此进程对 exec() 的所有后续调用都将导致向它发送 SIGTRAP,从而使父级有机会在新程序开始执行之前获得控制权。如果进程的父进程不希望跟踪它,则进程可能不应发出此请求。(PID、ADDR 和 DATA 将被忽略。

小的自问自答环节:

Q:为什么 gdb 不是特权指令?它读寄存器值、内存值等是怎么实现的?

A:但 install 是特权指令(笑),Linux 之中,二进制文件是自动具备可执行的默认权限的(file mode),而不是像 bash 脚本那样需要 sudo chmod +x 给予权限。

并且咨询 gpt 还得到一个有趣的事情:ptrace 调用确实可以修改其他的进程,所以 ptrace 调用是需要 root 权限,但是,用户权限下,可以使用 setuid 或者 setcap 机制,是 gdb 能对特定的文件,具有特定系统调用的权限

读寄存器值是使用了特定的中断,读内存是读的虚拟内存。

对页表的进一步探讨

在写知识之前,不如先问自己几个问题:

  • 为什么要引入多级页表?多级页表的原理和具体实现是什么?什么是页表目录(page table dir)?
  • 页表的基址寄存器,在 xv6 中是什么?在页表还没有建立之前和建立页表,设置基址之时,内核代码是怎么运行的?
  • 计算机的 cache 具体是在什么位置?它接受的和返回的是物理地址还是虚拟地址?
  • 多进程下,内核栈在实际内存的哪里?有几个?又是如何保证多个内核栈不重叠的?

xv6 的文件系统和系统调用

在写知识之前,不如先问自己几个问题:

  • 使用系统调用写一个 ls 的大体思路?

  • 文件重命名,软硬链接究竟是什么?文件夹又是什么?

  • 文件描述符和管道是怎么实现的?

  • 系统调用是如何实现的

  • 多进程下,内核栈在实际内存的哪里?有几个?又是如何保证多个内核栈不重叠的?

  • 宏内核和微内核的利弊?

文件系统部分:

Linux 文件的核心标识是 inode

名字之于 inode 号好比域名之于 ip 地址

是由文件所在的文件夹管理这一个 <filename, inode number>的 map

这就又引出了文件夹是什么,实际上就是——什么也不是,它只是一个 记录了底下文件的<filename, inode number>的 map 数据

KISS 法则,risc 和 cisc

Loading Comments...