我们的编译器主要分成了两个部分, 第一步部分就是语义分析阶段。在语义分析阶段我们主要做了下面这些事情:
- 类型推导, 我们会推导出对应node的类型, 比如我们会推导出pid这个函数的返回值类型
- 内存分配, 我们会将对应的数据结构分配到寄存上面活着栈上面
- 类型检查, 这一步部分还是有待完善的。
首先我们先简单的看一下我们获取跟踪函数参数的语法:
arg(1, "128s");
- 函数的第一个参数是获取参数的位置, 比如1表示函数的第一个位置
- 函数的第二个参数是获取参数的类型信息或参数的大小,比如128就是16字节, s表示对应的参数类型是字符串类型。
首先我们需要知道在我们的x86-64下面用于函数传递参数的寄存器, 下面这些 %rd
i,%rsi
,%rdx
,%rcx
,%r8
,%r9
寄存器用来给函数参数传递值。
首先是对应的annote这个阶段, 在这个节点我们会计算对应的函数的类型和对应的大小:
static int probe_arg_annotate(node_t *call) {
node_t *arg = call->call.vargs;
intptr_t reg;
reg = arch_reg_arg(arg->integer);
arg->integer = reg;
call->dyn->type = TYPE_INT;
call->dyn->size = sizeof(int64_t);
return 0;
}
这里我们需要注意的一点就是, 我们的arg函数返回值是int, 这是因为我们存储的是跟踪点函数参数的地址值。
然后我们会在栈上开辟一段空间, 在这段空间上面我们会对这个地址值进行存储:
static int probe_reg_loc_assign(node_t *call) {
node_t *probe;
call->dyn->addr = node_probe_stack_get(probe, call->dyn->size);
call->call.vargs->dyn->loc = LOC_VIRTUAL;
return 0;
}
然后就是编译, 编译的过程主要就是从我们的struct pt_regs *ctx
这个变量中去读取对应的值, 然后存储在我们刚才开辟的栈空间上的。
emit_stack_zero(prog, call);
emit(prog, MOV(BPF_REG_1, BPF_REG_10));
emit(prog, ALU_IMM(BPF_ADD, BPF_REG_1, call->dyn->addr));
//寄存器的大小
emit(prog, MOV_IMM(BPF_REG_2, arch_reg_width()));
emit(prog, MOV(BPF_REG_3, BPF_REG_9));
//src的base位置
emit(prog, ALU_IMM(BPF_ADD, BPF_REG_3, sizeof(uintptr_t)*arg->integer));
emit(prog, CALL(BPF_FUNC_probe_read));
获取到我们参数的base地址, 然后就是根据我们的大小继续去读