一生一芯

一生一芯做到了正式阶段,当然,预学习阶段的东西也忘得差不多了.

看PA2视频

我直接进行一个视频的嗯看,虽然动辄一个小时的视频,确实很难顶 可以结合ddca(digital design and computer archteture)的课一起看?

再次学习copilot的使用``

’tab’,自动用灰色文本建议, ‘alt’+’]’/’[‘切换建议

使用ctrl+enter在新选项卡中

点击accept solutions–>即可

使用copilot chat

ctrl+shift+p–>copilot chat

了解git的原理

每次都卡死在这里,真的是无无又奈奈啊

数字逻辑电路: 模拟器

#define REGS_FOREACH(_)  _(X) _(Y)
#define RUN_LOGIC        X1 = !X && Y; \
                         Y1 = !X && !Y;
#define DEFINE(X)        static int X, X##1;
#define UPDATE(X)        X = X##1;
#define PRINT(X)         printf(#X " = %d; ", X);

int main() {
  REGS_FOREACH(DEFINE);
  while (1) { // clock
    RUN_LOGIC;
    REGS_FOREACH(PRINT);
    REGS_FOREACH(UPDATE);
    putchar('\n'); sleep(1);
  }
}

X-macro:好处和坏处? X-macro是C语言中的一种编程技巧,它使用预处理器宏来生成重复的代码。X-macro的名字来源于它的典型用法,即定义一个宏,这个宏接受一个参数(通常被命名为X),然后在一系列的代码行中重复使用这个参数。

例如,你可能会看到像这样的X-macro定义:

#define LIST_OF_VARIABLES(X) \
    X(Variable1) \
    X(Variable2) \
    X(Variable3)

然后,你可以使用这个X-macro来生成一系列的代码行,例如:

#define DECLARE_VARIABLE(V) int V;
LIST_OF_VARIABLES(DECLARE_VARIABLE)

这将会展开为:

int Variable1;
int Variable2;
int Variable3;

X-macro的优点是它可以减少重复的代码,并且使得添加、删除或修改列表中的元素变得非常简单。只需要在LIST_OF_VARIABLES宏中添加、删除或修改元素,然后所有使用这个列表的代码都会自动更新。

然而,X-macro的缺点是它可能会使代码变得难以理解和维护,特别是对于不熟悉这种技巧的开发者来说。此外,由于它依赖于预处理器,因此它可能会导致一些难以调试的问题,例如宏展开错误或者名称冲突。

Vim中执行外部命令

在 Vim 中,你可以使用 :! 命令来执行外部命令。在你的例子中,你想要编译当前的 C 文件并运行它。以下是如何做到这一点:

  1. 首先,确保你已经保存了你的文件。你可以按 :w 来保存。

  2. 然后,输入 :!gcc % && ./a.out。这个命令做了两件事:

    • gcc %:这将使用 gcc 编译器来编译当前文件(% 在 Vim 中表示当前文件)。如果编译成功,gcc 将生成一个名为 a.out 的可执行文件。

    • ./a.out:这将运行刚刚编译的程序。注意,&& 是一个 shell 命令,它表示“如果前一个命令成功执行,那么执行后一个命令”。

  3. 按回车键执行命令。

gcc -E a.c -o a.i,会看到预处理后的文件

int main() {
  static int X, X1; static int Y, Y1;;
  while (1) {
    X1 = !X && Y; Y1 = !X && !Y;;
    printf("X" " = %d; ", X); printf("Y" " = %d; ", Y);;
    X = X1; Y = Y1;;
    putchar('\n'); sleep(1);
  }
}

更完整的实现: 数码管显示

import fileinput
 
TEMPLATE = '''
\033[2J\033[1;1f
     AAAAAAAAA
    FF       BB
    FF       BB
    FF       BB
    FF       BB
    GGGGGGGGGG
    EE       CC
    EE       CC
    EE       CC
    EE       CC
      DDDDDDDDD
''' 
BLOCK = {
    0: '\033[37m░\033[0m', # STFW: ANSI Escape Code
    1: '\033[31m█\033[0m',
}
VARS = 'ABCDEFG'

for v in VARS:
    globals()[v] = 0
stdin = fileinput.input()

while True:
    exec(stdin.readline())
    pic = TEMPLATE
    for v in VARS:
        pic = pic.replace(v, BLOCK[globals()[v]]) # 'A' -> BLOCK[A], ...
    print(pic)

需要搜索一下ANSI Escape Code(ANSI转义码)(chatgpt写博客也很爽)

你还体验了 UNIX 哲学 Make each program do one thing well Expect the output of every program to become the input to another

Vim给多行添加空格

  1. Ctrl + v 进入可视块模式。
  2. 使用 jk 键选择你想要添加空格的行。
  3. Shift + i 进入插入模式。
  4. 输入空格。
  5. Esc 键退出插入模式。 以上是错误的,这是chatgpt在中文互联网上抓取的知识(不知道为什么用英文问chatgpt它的答案也是错的,大概是因为英文上抓取的也是错误,因为我看到浏览器首页的答案就是错误的那个) 正确的是:通过stackchange上的答案
  6. Ctrl + v 进入可视块模式。
  7. 使用 jk 键选择你想要添加空格的行。
  8. 输入:’<’> normal I 注意space后要添加空格

jyy的小技巧

在 Linux 系统中,/tmp 目录是用于存储临时文件的地方。这个目录通常被系统管理员配置为可以在每次系统启动时清空,以确保不会在其中积累太多的临时文件。以下是有关 /tmp 目录的一些重要信息:

  1. 用途: /tmp 目录是用来存储临时文件和临时目录的地方。这些文件和目录通常只在当前会话中有用,不会保留在系统上。

  2. 定期清理: 大多数 Linux 发行版会在每次系统启动时清空 /tmp 目录。这是通过在启动过程中运行脚本来实现的,该脚本负责删除 /tmp 中的所有内容。

  3. 权限: /tmp 目录通常对所有用户开放写权限,以便任何用户都可以在其中创建临时文件。这是通过设置目录权限为 1777(drwxrwxrwt)来实现的,其中 “t” 标志表示粘滞位(sticky bit)。

  4. 临时文件的生命周期: 由于 /tmp 目录在系统启动时被清空,因此不应该在其中存储需要持久保存的文件。如果需要长期存储的文件,请使用其他目录。

  5. 其他位置: 除了 /tmp 目录外,有些系统还可能有 /var/tmp 目录,用于存储较为持久的临时文件,不会在系统启动时被清空。

总的来说,/tmp 目录是一个用于存储短暂和临时文件的标准位置,但请注意,不要在其中存储需要持久保存的数据。jyy每次讲课都是在这里面写它仅作展示的文件

C程序的语义

C程序的状态机模型

状态机的一个视角

  • 状态 = 堆 + 栈
  • 初始状态 = main的第一条语句
  • 迁移 = 执行一条简单语句

递归的汉诺塔

void hanoi(int n, char from, char to, char via) {
  if (n == 1) printf("%c -> %c\n", from, to);
  else {
    hanoi(n - 1, from, via, to);
    hanoi(1,     from, to,  via);
    hanoi(n - 1, via,  to,  from);
  }
  return;

}

非递归汉诺塔

typedef struct {
  int pc, n;
  char from, to, via;
} Frame;

#define call(...) ({ *(++top) = (Frame) { .pc = 0, __VA_ARGS__ }; })
#define ret()     ({ top--; })
#define goto(loc) ({ f->pc = (loc) - 1; })

void hanoi(int n, char from, char to, char via) {
  Frame stk[64], *top = stk - 1;
  call(n, from, to, via);
  for (Frame *f; (f = top) >= stk; f->pc++) {
    switch (f->pc) {
      case 0: if (f->n == 1) { printf("%c -> %c\n", f->from, f->to); goto(4); } break;
      case 1: call(f->n - 1, f->from, f->via, f->to);   break;
      case 2: call(       1, f->from, f->to,  f->via);  break;
      case 3: call(f->n - 1, f->via,  f->to,  f->from); break;
      case 4: ret();                                    break;
      default: assert(0);
    }
  }
}

这样的思路可以将任何递归程序转化成非递归,但我们先把这份代码弄懂先 在C语言中,__VA_ARGS__是一个预处理器宏,它用于表示可变参数的列表。这个宏通常在定义接受可变参数的宏时使用。(还有哪些预处理器宏? FILE LINE func)例如__LINE__ 是 C 和 C++ 中的一个预处理宏,它会在编译时被替换为源代码中的当前行号。这个宏可以用于记录或输出代码中的行号信息,通常用于调试、错误报告等目的。

以下是一个简单的示例:

#include <stdio.h>

int main() {
    printf("This message is from line %d.\n", __LINE__);
    return 0;
}

在上述代码中,__LINE__ 会被替换为该行的行号。当程序运行时,输出将是类似于 “This message is from line 5.” 的内容,其中 5 是 printf 语句所在的行号。

__LINE__ 是在预处理阶段执行的,因此它不会在运行时保留行号信息。这使得它在生成错误消息、调试输出等情况下非常有用,可以帮助开发者追踪和定位代码中的问题。

例如,你可以定义一个名为PRINTF的宏,它接受一个格式字符串和任意数量的额外参数,然后将这些参数传递给printf函数:

#define PRINTF(format, ...) printf(format, __VA_ARGS__)

然后,你可以像这样使用这个宏:

PRINTF("Hello, %s!\n", "world");

这将展开为:

printf("Hello, %s!\n", "world");

在你的代码中,__VA_ARGS__被用于call宏的定义中,表示call宏可以接受任意数量的参数,并将这些参数用于初始化Frame结构体的实例。(variable arguments可变参数列表)

call指令的实现

#define call(...) ({ *(++top) = (Frame) { .pc = 0, __VA_ARGS__ }; })

先top指针自增再把Frame结构体的实例赋值给top指针指向的内存

状态机的一个视角

状态 = 内存M + 寄存器 R

gdb调试的适合可以使用layout asm来查看汇编代码 可以使用layout src来查看C代码 也可以使用layout split来查看汇编代码和C代码

既可以是C代码视角也可以是机器指令的视角

操作系统上的程序

  • 所有的指令都只能计算
  • deterministic: mov,add,sub,call ,…
  • non-deterministic: jmp,ret,syscall,…
  • 但这些指令甚至都无法使程序停下来(NEMU: 加条trap指令)

rdrand $rax是一条x86汇编指令,用于生成一个随机数并将其存储在rax寄存器中。

rdrand指令是Intel在其处理器中实现的一种硬件随机数生成器。它可以生成高质量的随机数,适用于加密和安全相关的应用。

这条指令的使用需要硬件支持。你可以通过检查CPU的特性标志来确定是否支持rdrand指令。在Linux系统中,你可以通过查看/proc/cpuinfo文件来检查这个特性标志。

请注意,rdrand指令生成的随机数并不适合所有的应用。在某些情况下,你可能需要使用其他的随机数生成方法,例如使用软件的伪随机数生成器,或者使用其他的硬件随机数生成设备。

在GDB中,watchpoint是一种特殊的断点,用于监视表达式的值的变化。当表达式的值发生变化时,程序会暂停执行。

你可以使用watch命令来设置一个watchpoint。例如,如果你想要监视变量x的值,你可以使用以下命令:

watch x

这样,每当x的值发生变化时,程序就会暂停执行,你就可以查看x的新值以及导致x值变化的代码。

你也可以使用rwatch命令来设置一个read watchpoint,当程序读取指定表达式的值时,程序会暂停执行。

还可以使用awatch命令来设置一个access watchpoint,当程序读取或写入指定表达式的值时,程序会暂停执行。

要查看当前设置的所有watchpoint,你可以使用info watchpoints命令。

要删除一个watchpoint,你可以使用delete命令,后面跟上watchpoint的编号。例如,delete 1会删除编号为1的watchpoint。

一条特殊的指令


调用操作系统syscall

  • 把(M,R)完全交给操作系统,任其修改
    • 一个有趣的问题: 如果程序不打算完全信任操作系统,怎么办?
  • 实现与操作系统中的其他对象交互
    • 读写文件/操作系统状态(例如把文件内容写入M)
    • 改变进程的状态,例如创建进程/销毁自己 程序 = 计算 + syscall
    • 问题: 怎么构造一个最小的Hello world?

gcc --verbose hello.cgcc -static hello.c

  • –verbose 可以查看编译选项(可真不少)
  • printf变成了puts@plt
  • -static会复制libc

直接硬来

强行编译+链接: gcc -c+ld +