当前位置: 首页 > news >正文

PKU_Compiler

130480524_p0

from pixiv

资源

  • NJU Compiler 课程
  • 中科大 Compiler 课程
  • LLVM IR Github book教程
  • Koopa IR 框架
  • PKU 讲义本体
  • Github仓库

Lv0 环境配置

Docker

获取编译实践的镜像:

sudo docker pull maxxing/compiler-dev
  • docker安装

  • 配置docker镜像

    • vim /etc/docker/daemon.json

    • {"registry-mirrors": ["xxx","xxx"]
      }
      
    • docker 存活的镜像

Docker 基本使用方法

  • 官方文档
// 启动容器
docker run maxxing/compiler-dev// 查看目前 Docker 中所有的容器:
docker ps -a// 删除 容器
docker rm CONTAINER ID
// -it 参数, 这个参数会开启容器的 stdin 以便我们输入 (-i), 同时 Docker 会为容器分配一个终端 (-t).
docker run -it maxxing/compiler-dev bash

在许多情况下, 我们需要让 Docker 容器访问宿主系统中的文件. 比如你的编译器存放在宿主机的 /home/max/compiler 目录下, 你希望 Docker 容器也能访问到这个目录里的内容, 这样你就可以使用容器中的测试脚本测试你的编译器了. 你可以执行:

docker run -it --rm -v /home/max/compiler:/root/compiler maxxing/compiler-dev bash
  • -v /home/max/compiler:/root/compiler 选项, 这个选项代表: 我希望把宿主机的 /home/max/compiler 目录, 挂载 (mount) 到容器的 /root/compiler 目录. 这样, 在进入容器之后, 我们就可以通过访问 /root/compiler 来访问宿主机的 /home/max/compiler 目录了.
  • --rm: Docker 会在退出后删除刚刚的容器
  • 因为Docker只是测试我们在宿主机上实现的代码,所以测完直接可以丢掉了

maxxing/compiler-dev 实验环境

实验环境中已经配置了如下工具:

  • 必要的工具: git, flex, bison, python3.
  • 构建工具: make, cmake.
  • 运行工具: qemu-user-static.
  • 编译工具链: Rust 工具链, LLVM 工具链.
  • Koopa IR 相关工具: libkoopa (Koopa 的 C/C++ 库), koopac (Koopa IR 到 LLVM IR 转换器).
  • 测试脚本: autotest.

我们要做的事情就是在宿主机上编写我们的compiler代码,然后将我们代码目录挂载到docker的某个目录下,在docker maxxing/compiler-dev的实验环境中运行测试我们的程序

具体查看

项目模板

Github

Lv1 main函数

编译原理基础知识

语法分析树

二义性

所谓的二义性可以理解为对于一个文法,可以构造出多个语法分析树

因为语法分析树是对语义的解释,则说对于一个文法我们有多个语义

案例:

  • 因最短/最长匹配导致的二义性

    image-20250911161426980

    如上图中的文法和语句,我们有多个方式解释if a then if b then c else d 这个语句,即在执行时,其有多个语义:

    • if (a) then { if b then c else d}
    • if (a) then { if b then c } else { d }

    我们可以通过更改文法/规定匹配来避免二义性,例如这里我们规定else匹配距离其最近的if,则最终我们只会得到if (a) then { if b then c else d}, 其语法树:

    image-20250911161858841

  • 因运算符的结合性/优先级导致的二义性

    例如运算符优先级导致的二义性,我们可以通过规定运算符优先级(更常见)或者更改文法避免。如a - b * c语义可以为:

    • a - (b * c)
    • (a - b) * c

    image-20250911162142542

    我们通过如上方式避免(ANTLR 4 中规定越在上的优先级越高)

抽象语法树

抽象语法树的前身--语法分析树

image-20250905110831735

上图中例如"(" 和 ")"这些信息是不重要的,同时3,4这两个叶子结点的父节点也是没有必要的

对于表达式而言,编译只需要知道运算符和运算数

优先级、结合性等已经在语法分析部分处理掉了

对于语句、函数等语言其他构造而言也一样

例如,编译器不关心赋值符号是=还是:=或其它

抽象语法树

为了节省内存,压缩信息,得到更加紧凑的表示,我们对语法分析树进行浓缩:

image-20250905111318880

  • 具体语法是语法分析器使用的语法
    • 必须适合于语法分析,如各种分隔符、消除左递归、提取左公因子,等等
  • 抽象语法是用来表达语法结构的内部表示
    • 现代编译器一般都采用抽象语法作为前端(词法语法分析)和后端(代码生成)的接口

在编译器中,为了定义抽象语法树,需要使用实现语言(C++/C/java...)来定义一组数据结构

例如在课中以简单的实例说明的:

image-20250905152616108

其给每个终结符和非终结符定义了结构体,并定义构造函数在生成抽象语法树时使用:

image-20250905152742215


image-20250905152914595

抽象语法树的自动生成

在语法动作中,加入生成语法树的代码片段。

  • 片段一般是语法树的“构造函数’

在产生式归约的时候,会自底向上构造整棵树

  • 从叶子到根

上述内容说的就是Bison中的写法,例如:

FuncDef: FuncType IDENT '(' ')' Block {auto ast = new ast::FuncDefAST();ast->func_type = unique_ptr<ast::BaseAST>($1);ast->ident = *unique_ptr<std::string>($2);ast->block = unique_ptr<ast::BaseAST>($5);$$ = ast;}

若想要表示位置信息,则可以在数据结构中完善更多的信息:

image-20250905152955546

中间代码生成

语义分析

  • 语义分析也称为类型检查、上下文相关分析
  • 负责检查程序(抽象语法树)的上下文相关的属性:
    • 这是具体语言相关的,典型的情况包括:
      • 变量在使用前先进行声明
      • 每个表达式都有合适的类型
      • 函数调用和函数的定义一致
      • ...

在Flex/Bison中已经做到了一部分语义分析,例如类型检查

例如我们在Bison中通过%union%token定义终结符和非终结符语义的类型:

%union {std::string *str_val;int int_val;ast::BaseAST *ast_val;
}// 终结符的类型定义
%token INT RETURN
%token <str_val> IDENT
%token <int_val> INT_CONST// 非终结符的类型定义
%type <ast_val> FuncDef FuncType Block Stmt 
%type <int_val> Number

符号表

若要我们自己实现语义分析中的类型检查,变量声明检查等内容,符号表不可或缺

P -> D E
D -> T id;|
T -> int| bool
E -> n| id| true| false| E + E| E && E

我们以上述简单的语法为例:

  • P : program

  • D : declare

  • T : type

  • E : express

  • id: 终结符

  • n: 终结符

我们对语句:

int x;
bool y;
4 + x;

进行语法检查和语义检查:

抽象语法树:

image-20250907142822744

类型检查算法

image-20250907142936910

  • table_enter为插入符号表的操作
    • 当我们检查声明时,若未在符号表找到相关符号的声明,则插入新元素[key, value],其中key为符号的name, 如x, value为符号的类型,如int

image-20250907143157139

  • 在检查表达式时,若表达式中的符号未在符号表中查找到,则说明使用了未声明的符号

符号表的作用域与scope

作用域

int x;int foo() {int x;int y;return x + y;
}

上述代码中在不同作用域下有两个不同的x,符号表需要能够区分:

  • 方法#1:一张表的方法
    • 进入作用域时,插入元素
    • 退出作用域时,删除元素
  • 方法#2:采用符号表构成的栈
    • 进入作用域时,插入新的符号表
    • 退出作用域时,删除栈顶符号表

Scope

struct list{int x;struct list *list;
} *list;void walk(struct list *list) {list:if (list == list->list)goto list;
}

上述代码中list出现了多次,分别为:

  • 变量名 (*list

  • 命名 (struct list)

  • 标号 (goto list)

它们被称为不同的scope

每个scope用一个表来处理

中间代码生成

中间代码其实也可以理解为是某种语言,例如LLVM IR,其与汇编语言已经非常相近了,同时也可以作为一门语言进行书写

这里我们以更加简单的例子进行讲解:

image-20250907151913886

我们从之间的语法 --> IR

R_t Gen E(E e) {
switch(e) {case n: r=fresh();emit("movn n,r");return r;case id: r=fresh();emit ("mov id,r');return r;case true: r=fresh();emit("movn 1,r");return r;case false:r=fresh();emit("movn 0,r");return r;case e1 + e2: r1 = Gen_E(e1);r2 = Gen_E(e2);r = fresh();emit("add r1, r2, r3");return r;case e1 && e2: r1 = Gen_E(e1);r2 = Gen_E(e2);r = fresh();emit("and r1, r2, r3");return r;
  • R_t表示一个寄存器类型
  • fresh()表示生成一个新的寄存器
  • emit表示生成对应的IR指令

上述方法也很像我们实现抽象语法树时做的行为


中间代码的形式有很多:

  • 树和有向无环图(DAG)
    • 高层表示,适用于程序源代码
  • 三地址码(3-address code)
    • 低层表示,靠近目标机器
  • 控制流图(CFG)
    • 更精细的三地址码,程序的图状表示适合做程序分析、程序分析等
  • 静态单赋值形式(SSA)
    • 更精细的控制流图
    • 同时编码控制流信息和数据流信息
  • 连续传递风格(CPS)
    • 更一般的SSA

抽象语法树变体 --DAG 有向无环图

“语法树的变体是有向无环图” 的意思是:编译器里用于表示程序结构的“树状”数据结构可以允许不同父节点指向同一个子节点(即共享子结构),形成一个有向图,而且这个图通常不会包含环(acyclic),因此称为 DAG(Directed Acyclic Graph)。

  • :每个节点有且只有一个父节点(除了根),不同分支间不会共享子树。

  • 有向图:节点间的引用有方向(例如父 → 子)。

  • DAG:有向且无环的图;允许多个父节点共享同一个子节点,但不会有回到自己/循环的路径。

树是最直观的表示法,但在实际编译器中多个位置可能出现相同的子表达式或相同的“值”。为了节省内存、方便优化与比较,常采用共享子节点的做法——这会把结构从树变成 DAG。

例如对于表达式a+a*(b-c)+(b-c)*d, 其抽象语法树为:

image-20250910163230070

树是最直观的表示法,但在实际编译器中多个位置可能出现相同的子表达式或相同的“值”。为了节省内存、方便优化与比较,常采用共享子节点的做法——这会把结构从树变成 DAG。

对于重复子表达式, 边从上层节点指向这个共享节点。

于是上述抽象语法树变成如下:

image-20250910163340675

DAG --> 三地址中间表示

语法制导翻译 -- 依赖图

  • 依赖图(dependency graph) 是把“某一棵语法树(或某次产生式展开)中每个属性的具体出现”作为节点,属性之间的“谁依赖谁”作为有向边构成的图。
  • 求属性的合法计算顺序就是对这个图做 拓扑排序(topological order)。
  • 如果图中有环(cycle),说明这些属性不能用单次有向无环评估得到(需要重写规则或使用迭代/固定点技巧)。
  • 对于只含合成属性(S-属性)的文法,依赖图天然是自底向上的,直接用后序遍历(reduce 时计算)即可。
  • 对于 L-属性(L-attributed),可以在一次自顶向下/左到右的遍历或在解析时用语义动作按顺序求到属性值(能在线计算)。

  1. 依赖图的正式定义(简洁)

给定:一棵 解析树(parse tree) T 和一个 SDD(syntax-directed definition,给出每条产生式的属性计算规则)。

  • 节点集合 V = “T 中所有属性的出现”(每个语法树结点对于每个属性出现一个节点,例:对某个语法树结点 A,如果 A 有属性 A.inhA.s,就有两个依赖图节点)。
  • 有向边 (u → v) 当且仅当:v 的计算公式使用了 u 的值
    举例:如果规则写 A.s = B.s + C.s,那么在语法树的对应位置就会有边 B.s → A.sC.s → A.s

目标:找到一个节点序列,使得每条边 u→v 都满足 u 在 v 之前被计算——这就是拓扑序。


  1. 怎么构造依赖图(步骤)
  1. 在解析树的每个树结点(一个非终结符或终结符)上列出它的所有属性实例(合成/继承/其他)。这些属性实例就是依赖图的节点。
  2. 对照你为每条产生式写的语义规则(例如 A.s = B.s + C.s, C.inh = A.inh 等),为每个语义等式加入有向边:从被使用的属性指向被定义的属性。
    • 例如在产生式 A -> B C 中,规则 C.inh = f(B.inh, B.s) 会产生 B.inh → C.inhB.s → C.inh
  3. 做完语法树上所有出现的产生式和对应规则,得到完整的依赖图。
  4. 对该有向图做拓扑排序(比如 Kahn 算法或 DFS)。若能得到排序,则按此顺序依次计算属性;若不能(存在环),则说明不能单次评估。

命令

启动docker环境

docker run -it --rm -v /home/cilinmengye/Github/PKU_Compiler:/root/compiler maxxing/compiler-dev bash

Cmake编译命令

cd 项目目录
cmake -DCMAKE_BUILD_TYPE=Debug -B build
cmake --build build

执行命令

build/compiler -koopa debug/hello.c -o debug/hello.koopa

Flex and Bison 初见

  • vscode 下载 Yash插件,提供flex 和 bison 高亮

Flex注释问题

Flex 推荐用 C 风格注释

/* 这是注释 */

Flex 把 .l 文件分为三段:

  1. %{ ... %}:直接拷贝到生成的 .c
  2. %% ... %%:规则区(正则表达式 + 动作)
  3. %% ... 之后:C 代码区

Flex 确实允许 C 风格注释,但是要注意:

  • 注释只能写在 C 代码区动作 {...} 里面
  • 直接把一大段 /* ... */ 放在规则区开头,有些版本的 Flex 会误判,把它当成“正则模式”

Lv2 初试目标代码生成

目的:

  • AST --> in-memory Koopa IR
  • in-memory Koopa IR --> Text form Koopa IR

建立内存形式的 Koopa IR

我想要通过如下方法实现:

  1. 遍历 AST, 直接建立 (某种) 内存形式的 Koopa IR, 再将其转换为文本形式输出.

  2. 遍历 AST, 输出文本形式的 Koopa IR 程序,再使用 libkoopa 中的接口将文本形式 Koopa IR 转换为 raw program


好吧1方法实在是太有难度了,我还是选择方法2进行实现吧。

In-memory Koopa IR --> Riscv assembly code

在 SysY 程序中, 我们定义了一个 main 函数, 这个函数什么也没做, 只是返回了一个整数, 之后就退出了. RISC-V 程序所做的事情与之一致:

  1. 定义了 main 函数.
  2. 将作为返回值的整数加载到了存放返回值的寄存器中.
  3. 执行返回指令.

命令

// 测试
build/compiler -riscv debug/hello.c -o debug/hello.S
clang debug/hello.S -c -o debug/hello.o -target riscv32-unknown-linux-elf -march=rv32im -mabi=ilp32
ld.lld debug/hello.o -L$CDE_LIBRARY_PATH/riscv32 -lsysy -o debug/hello
qemu-riscv32-static debug/helloecho $?

附录 参考资料

  • LLVM IR
  • Koopa IR 接口
  • Koopa IR 文档
  • Koopa IR 相关博客
    • Koopa IR 人话版

Lv3 表达式

编译原理基础知识

三地址码

我们的Koopa IR也是一种三地址码:

  • 我们需要给每个中间变量和计算结果命名
  • 只有最基本的控制流(call,jump等),没有各种控制结构(if, while, for等)

控制流图

控制流图也是一种中间表示

我们将三地址码划分为基本块,基本块与基本块之间的跳转我们用->表示

image-20250911150612268

基于控制流图的数据流分析:

  • 例如永远到达不了的基本块节点,我们可以将这个基本块删除(死基本块删除)
  • 例如变量的值随控制流图传播发现值不会改变,则可以将变量直接变成常量(常量传播)

image-20250911151143369

image-20250911151201398

语法制导 实现抽象语法树-->IR

生成临时变量

image-20250911200221154

  • E.code: 综合属性,表示中间代码
    • 综合属性则说明从语法分析树自下而上,步步传递给父节点的综合属性
  • E.addr: 综合属性,表示变量名(临时变量/常量)
  • top: 表示当前scope下的符号表
    • top.get(id.lexeme) 表示取出 当前scope下符号表最上一个变量名
  • gen(xxx): 表示生成代码xxx
  • new Temp():表示生成临时变量
    • 如右边的t1, t2等为生成的临时变量

S → id = E ;

  • S.code = E.code || gen(top.get(id.lexeme) '=' E.addr)
    含义:
    1. 先生成 E 的代码
    2. 再生成一条赋值语句,把 E 的值存入 id 对应的地址
    3. top.get(id.lexeme) 就是符号表里取出这个 id 的位置

IR --> RISCV

IR --> RISCV 这个过程例如 LLVM IR反而是最顶层的IR,我们在将LLVM IR --> RISCV时需要转化为多次更底层的IR,IR --> 更底层IR 这个过程被称为 pass

每次pass的过程都在依据IR进行优化

image-20250912183423557

image-20250912183454723

  • LLVM IR --> DAG,DAG也是一种中间表示,即IR
  • 在DAG的基础上进行指令选择,指令调度,寄存器分配等过程变为更底层的LLVM MIR,这个时候LLVM MIR已经是与架构相关的了
  • 最后成为Machine Code

总结

  • Flex: 我们在其中编写的是词法分析
  • Bison: 我们在其中编写的是语法分析(产生式规则),同时我们以 {}动作 编写了遍历语法树的过程
  • 我们还需要编写 语义分析(语义规则),这点需要我们独自创建.hpp.cpp进行编写了,在我的实现中ast.hpp做的正是这点

Lv3.1. 一元表达式

命令

build/compiler -koopa debug/unaryop.c -o debug/unaryop.koopa

Koopa IR

Koopa IR 程序的in-memory raw Koopa IR结构:

  • 最上层是 koopa_raw_program_t, 也就是 Program.

    // https://github.com/pku-minic/koopa/blob/master/crates/libkoopa/include/koopa.htypedef const void *koopa_program_t;typedef struct {const void **buffer; // Buffer of slice items.uint32_t len; // Length of slice.koopa_raw_slice_item_kind_t kind; // 其为enum类型: Unknown. Type. Function. Basic block. Value.
    } koopa_raw_slice_t;typedef struct {koopa_raw_slice_t values; // Global values (global allocations only).koopa_raw_slice_t funcs; // Function definitions.
    } koopa_raw_program_t;
    
  • 之下是全局变量定义列表和函数定义列表.

    • 在 raw program 中, 列表的类型是 koopa_raw_slice_t.
    • 本质上这是一个指针数组, 其中的 buffer 字段记录了指针数组的地址 (类型是 const void **), len 字段记录了指针数组的长度, kind 字段记录了数组元素是何种类型的指针
    • 在访问时, 你可以通过 slice.buffer[i] 拿到列表元素的指针, 然后通过判断 kind 来决定把这个指针转换成什么类型.
  • koopa_raw_function_t 代表函数, 其中是基本块列表.

    typedef struct {koopa_raw_type_t ty;	// Type of function.const char *name;	// Name of function.koopa_raw_slice_t params;	// Parameters.koopa_raw_slice_t bbs; // Basic blocks, empty if is a function declaration.
    } koopa_raw_function_data_t;
    typedef const koopa_raw_function_data_t *koopa_raw_function_t;
    
  • koopa_raw_basic_block_t 代表基本块, 其中是指令列表.

    typedef struct {const char *name; // Name of basic block, null if no name.koopa_raw_slice_t params; // Parameters.koopa_raw_slice_t used_by; // Values that this basic block is used by.koopa_raw_slice_t insts; // Instructions in this basic block.
    } koopa_raw_basic_block_data_t;
    typedef const koopa_raw_basic_block_data_t *koopa_raw_basic_block_t;
    
  • koopa_raw_value_t 代表全局变量, 或者基本块中的指令.

    struct koopa_raw_value_data {koopa_raw_type_t ty; // Type of value.const char *name; // Name of value, null if no name.koopa_raw_slice_t used_by; // Values that this value is used by.koopa_raw_value_kind_t kind; // Kind of value.
    };
    typedef struct koopa_raw_value_data koopa_raw_value_data_t;
    typedef const koopa_raw_value_data_t *koopa_raw_value_t;
    

相关类型定义:

///
/// Tag of raw Koopa type.
///
typedef enum {/// 32-bit integer.KOOPA_RTT_INT32,/// Unit (void).KOOPA_RTT_UNIT,/// Array (with base type and length).KOOPA_RTT_ARRAY,/// Pointer (with base type).KOOPA_RTT_POINTER,/// Function (with parameter types and return type).KOOPA_RTT_FUNCTION,
} koopa_raw_type_tag_t;///
/// Tag of raw Koopa value.
///
typedef enum {/// Integer constant.KOOPA_RVT_INTEGER,/// Zero initializer.KOOPA_RVT_ZERO_INIT,/// Undefined value.KOOPA_RVT_UNDEF,/// Aggregate constant.KOOPA_RVT_AGGREGATE,/// Function argument reference.KOOPA_RVT_FUNC_ARG_REF,/// Basic block argument reference.KOOPA_RVT_BLOCK_ARG_REF,/// Local memory allocation.KOOPA_RVT_ALLOC,/// Global memory allocation.KOOPA_RVT_GLOBAL_ALLOC,/// Memory load.KOOPA_RVT_LOAD,/// Memory store.KOOPA_RVT_STORE,/// Pointer calculation.KOOPA_RVT_GET_PTR,/// Element pointer calculation.KOOPA_RVT_GET_ELEM_PTR,/// Binary operation.KOOPA_RVT_BINARY,/// Conditional branch.KOOPA_RVT_BRANCH,/// Unconditional jump.KOOPA_RVT_JUMP,/// Function call.KOOPA_RVT_CALL,/// Function return.KOOPA_RVT_RETURN,
} koopa_raw_value_tag_t;///
/// Kind of raw Koopa value.
///
typedef struct {koopa_raw_value_tag_t tag;union {koopa_raw_integer_t integer;koopa_raw_aggregate_t aggregate;koopa_raw_func_arg_ref_t func_arg_ref;koopa_raw_block_arg_ref_t block_arg_ref;koopa_raw_global_alloc_t global_alloc;koopa_raw_load_t load;koopa_raw_store_t store;koopa_raw_get_ptr_t get_ptr;koopa_raw_get_elem_ptr_t get_elem_ptr;koopa_raw_binary_t binary;koopa_raw_branch_t branch;koopa_raw_jump_t jump;koopa_raw_call_t call;koopa_raw_return_t ret;} data;
} koopa_raw_value_kind_t;

后续

我认为后续实验的内容都是整体核心思想不变,全部都是工程量,所以接下来我也就没有兴趣继续写下去了

不过作者的Koopa IR框架值得学习

CMake

Outline

  • 视频
  • 教程
  • 案例

基础

Message

message(STATUS "...")

在 CMake 脚本里,message() 用来打印信息。

  • STATUS 表示这是普通提示信息(在配置时会显示)。
  • 常见级别有:
    • STATUS:提示信息
    • WARNING:警告
    • FATAL_ERROR:致命错误(立刻终止 CMake)

例如:

message(STATUS "Hello, CMake")

运行 cmake .. 时会看到:

-- Hello, CMake

(前面的 -- 是 CMake 自动加的。)

"Include directory: ${INC_DIR}"

这里 ${INC_DIR} 表示变量替换

  • 在 CMake 里,变量用 ${...} 来取值。
  • 所以这行代码的效果是:打印字符串 + INC_DIR 的值。

例如:

set(INC_DIR /usr/include/mylib)
message(STATUS "Include directory: ${INC_DIR}")

输出:

-- Include directory: /usr/include/mylib

Include directory

“加头文件目录”的写法:

写法 影响范围 推荐度
include_directories() 全局(当前目录及子目录所有 target) ❌ 老式,不推荐
target_include_directories() 只影响指定 target,且能控制传递性(PUBLIC/PRIVATE/INTERFACE) ✅ 现代 CMake 推荐

C++

C++ 命名规范

  • 类/类型:大驼峰 KoopaParser
  • 函数:小驼峰 parseFunction()
  • 变量:小驼峰 lineNumber,成员变量加 _m_
  • 常量/宏:全大写 MAX_BUFFER_SIZE
  • 命名空间:小写 koopa_ir
  • 文件:小写下划线 koopa_parser.cpp

智能指针

语法 作用
auto 自动推导类型
make_unique<T>() 创建智能指针,避免手动 new
unique_ptr<T> 独占所有权智能指针,自动释放内存
move() 移动语义,转移所有权,避免拷贝

例如:

std::string* name = new std::string("hello");std::unique_ptr<std::string> func_name = std::unique_ptr<std::string>(name); 
  • 注意: std::unique_ptr<T>()中需要传入的是T的原始指针T*, 而不能是T这个对象
  • 当我们要输出name的值时需要使用*name, 因为func_nane是个指针

make_uniqueC++14 引入的一个 工厂函数,作用就是简化上面的写法:

std::unique_ptr<std::string> func_name = std::make_ptr<std::string>("hello");

move:

move() 是 C++11 的 移动语义

  • unique_ptr 不能被拷贝(拷贝构造被删除),只能 移动
std::unique_ptr<std::string> func_Name = move(func_name);

移动后,func_name 为空(指针被置为 nullptr),func_Name 成为新的所有者。

运算

强制类型转换运算符

reinterpret_cast<新类型>(指针)
  • reinterpret_cast 是 C++ 里最“原始”的转换,它会直接把一块内存的比特位重新解释成另一种类型
  • 它不会检查类型是否兼容,只要编译器认为语法合法,它就会“硬转”。
int x = 0x12345678;
char* p = reinterpret_cast<char*>(&x);
// p 现在指向 int 的内存,但把它解释成了 char*

类的创建

好问题 👍 我们一步步来。

在 C++ 里,创建类实例有两种主要方式:栈上创建堆上创建


1. 栈上创建(自动存储)

最常见、最推荐的方式:

#include <iostream>
#include <string>class Person {
public:std::string name;int age;Person(const std::string& n, int a) : name(n), age(a) {}void introduce() {std::cout << "Hi, I'm " << name << ", age " << age << std::endl;}
};int main() {Person p("Alice", 20);  // 栈上实例化p.introduce();return 0;
}

这里 p 会在 main 结束时自动销毁,不需要手动释放。


2. 堆上创建(动态存储)

new,需要手动释放:

int main() {Person* p = new Person("Bob", 25);  // 堆上实例化p->introduce();delete p;  // 必须释放,否则内存泄漏return 0;
}

3. 使用智能指针(推荐替代 new)

避免手动 delete,更现代安全:

#include <memory>int main() {auto p = std::make_unique<Person>("Charlie", 30);p->introduce();// 离开作用域时自动释放
}

我想确认下,你现在是想在 函数里直接用一个对象(栈上更合适),还是想把对象传来传去/长期保存(那就用 std::unique_ptr 比较安全)?

Kernel Debugging && Logging

Kernel Debugging

QEMU + kernel image

可以参考XV6实验中的做法,将XV6 OS的镜像放入QEMU中运行,在QEMU中开启gdb,调试kernel

Logging

debugfs -- 开发者调试内核

Linux debugfs(Debug Filesystem)概述

debugfs 是内核提供的一个专用于调试的虚拟文件系统。它用于暴露内核或模块的调试信息与控制接口 —— 开发者和内核调试工具常用它来查看/修改内核内部状态。它不是稳定的用户接口(ABI/行为可变),因此不应在面向用户的生产工具中当作稳定接口依赖。


如何挂载 / 检查

# 检查是否已经挂载
mount | grep /sys/kernel/debug# 如果未挂载,root 下挂载:
mount -t debugfs none /sys/kernel/debug# 安全检查(systemd 常会自动挂载)
ls -la /sys/kernel/debug

要确认内核是否启用了 debugfs,查看内核配置(例如 /boot/config-$(uname -r)CONFIG_DEBUG_FS=y),或在编译内核时确认该选项被打开。


常见用途与例子

  • ftrace / tracing/sys/kernel/debug/tracing/ 提供函数追踪、trace_pipe、set_ftrace_pid 等接口。
    例:echo function > /sys/kernel/debug/tracing/current_tracer,然后 cat /sys/kernel/debug/tracing/tracecat trace_pipe 实时查看。
  • kmemleak/sys/kernel/debug/kmemleak 用于内存泄露检测(scan、show 等)。
  • slab / slabinfo:查看 slab 分配器内部信息:cat /sys/kernel/debug/slab
  • lockdep:死锁检测、锁依赖关系检查信息通常在 debugfs 中。
  • 自定义模块的调试接口:模块可以把内部状态、控制开关、统计信息暴露到 debugfs,便于开发和调试。

常用 shell 操作示例:

# 读取调试信息
cat /sys/kernel/debug/some_debug_file# 改变内核调试选项(写入)
echo 1 > /sys/kernel/debug/some_debug_control

与 procfs / sysfs 的区别

  • debugfs:专为调试设计,接口不稳定、权限通常需要 root、用于开发者调试。
  • sysfs:稳定的设备与驱动用户空间接口(表示内核对象属性),用于正式的用户空间 API。
  • procfs:暴露进程与部分内核信息(例如 /proc),部分接口较正式但也有历史包袱。

原则:用于长期、稳定暴露的接口应使用 sysfs 或 netlink,而不是 debugfs

demesg

很好——下面用简明扼要的方式解释 dmesg 是什么、它从哪里来、常见用法和注意点。

什么是 dmesg

dmesg(display message)是一个查看 内核环形缓冲区(kernel ring buffer) 中消息的用户命令。内核和内核驱动(以及用 printk() 打印日志的代码)会把运行时信息、引导信息、设备驱动加载/卸载、错误、oops/panic 等写到这个缓冲区。dmesg 就是把这些内核日志输出到终端,便于调试与排错。

常见输出示例(典型行)

[   12.345678] usb 1-1: new high-speed USB device number 2 using xhci_hcd

方括号内通常是自系统启动以来的秒数(相对时间),后面是内核消息内容。

常用命令与选项

  • dmesg:打印全部内核缓冲区(顺序从旧到新)。
  • dmesg -T:把时间戳转为可读时间(注意:转换是近似的,内核时间与系统墙钟可能不完全精确)。
  • dmesg -H:漂亮输出(带颜色与分页,类似 human)。
  • dmesg -w:实时跟随(像 tail -f),会持续打印新产生的内核消息。
  • sudo dmesg -C:清空内核缓冲区(需要 root)。
  • dmesg --level=err,warn:只显示错误/警告等级的消息。
  • dmesg | grep -i usb:按关键词筛选(常用于定位硬件/驱动问题)。

内核日志来源和持久化

  • 内核的消息最终写入内核缓冲区(kernel ring buffer),也可以被 klogd / rsyslog / systemd-journald 等守护进程采集并写入持久日志文件(比如 /var/log/kern.log/var/log/messages 或 systemd 的 journal)。
  • 在 systemd 系统上,推荐使用 journalctl -k(或 journalctl -b)来查看内核消息的持久记录。

用途(为什么要用 dmesg

  • 查看启动过程中的内核/驱动加载信息(设备识别、驱动绑定等)。
  • 排查硬件问题(USB、SCSI、PCI 设备错误)、查看驱动报错、查看内核 panic/oops 信息。
  • 调试内核模块或驱动:模块中的 printk() 输出会出现在这里。
  • 查看内核检测到的硬件事件(比如磁盘掉线、I/O 错误、网络设备重置等)。

权限与隐私

  • 有些系统限制非 root 用户读取全部内核缓冲区(因为可能泄露敏感信息,如内存地址)。
  • 内核日志可能包含敏感信息(如内存地址、设备序列号等),在公开或贴出日志前注意脱敏。

小贴士

  • 如果你想在出现问题的瞬间抓取内核日志,dmesg -w 很方便用于实时监控。
  • 内核环形缓冲区大小有限,老日志会被新日志覆盖 —— 因此长期诊断应查看持久化日志(journalctl/var/log)。
  • 常见问题排查命令:
    dmesg | tail -n 50(看最近 50 行)
    dmesg | grep -i error / dmesg --level=err(查看错误)
http://www.wxhsa.cn/company.asp?id=3005

相关文章:

  • lc1026-节点与其祖先之间的最大差值
  • 如何绕过谷歌反爬策略爬取搜索结果
  • 求细胞数量
  • [SSL]
  • Rust 生命周期详解 - 实践
  • 笔记《机器人动力学理论及其应用》上交桂凯博士-中科深谷机器人大讲堂第10期
  • [豪の学习笔记] 软考中级备考 基础复习#9
  • Shiro概述 - 详解
  • 2025CCPC南昌邀请赛游记
  • 双因素认证暴力破解绕过技术解析(2023更新版)
  • 文本三剑客
  • 软件工程第二次作业-个人项目
  • Git 分支
  • 用 Go 打造一个服务器资源指标采集器:结合 Prometheus Exporter 实战
  • 2025年API安全建设方案最佳实践:七步五方法
  • 【数学】拉格朗日乘数法
  • 华为芯片之父,33年默默开拓,铸就“中国芯”,功成身退时却鲜有人知!
  • Redis为什么适合做分布式锁? - 浪矢
  • 百度昆仑芯高调出圈:对标寒武纪,估值或达千亿港元?
  • WPS 定制版
  • 2024年以来,数学领域已有多位在国外顶尖高校取得终身教职的学者回国
  • 685.冗余连接
  • form表单和表单控件
  • 阿里云OSS图片生成缩略图和获取视频的封面方法
  • VSCode 运行 Python
  • [mysql] 卸载
  • 树上问题
  • 突发!美国将复旦微等23家中国实体列入“实体清单”
  • [GenAI] Function Calling
  • form表单