NJU OS M4 C Read-Eval-Print-Loop
M4: C Read-Eval-Print-Loop (crepl)
简化题意:维护一个小型 C REPL。用户可以先输入函数定义,把它们加入当前进程的符号环境;之后再输入表达式,程序把表达式包装成函数、动态编译、动态装载并执行,输出结果。
Lab 4: crepl 与动态链接接口笔记
1. 这题真正要做的事
这题表面上像“解释器”,但实现上并不是自己解析并执行 C 表达式,而是把 gcc 和动态链接器当作后端:
- 用户输入函数定义。
- 程序生成临时
.c文件。 - 调用
gcc -shared -fPIC编译出.so。 - 用
dlopen把.so加入当前进程。 - 用户输入表达式时,再生成一个只包含 wrapper 的临时
.so。 - 用
dlsym找到 wrapper 函数地址并调用,得到表达式值。
所以核心不变式是:
- 已定义函数必须能被后续函数和后续表达式看到。
- 语法错误、未定义符号、编译失败都要被识别出来,而不是默默返回成功。
2. 整体运行模型
可以把 crepl 理解成“进程内不断扩展的符号环境”:
用户输入函数定义
-> 生成临时 temp.c
-> gcc -shared -fPIC temp.c -o tempN.so
-> dlopen(tempN.so, RTLD_NOW | RTLD_GLOBAL)
-> 新函数加入当前进程的全局符号环境
用户输入表达式
-> 生成临时 expr.c
-> expr.c 中写入历史函数原型 + wrapper
-> gcc -shared -fPIC expr.c -o exprN.so
-> dlopen(exprN.so, RTLD_NOW | RTLD_GLOBAL)
-> dlsym(handle, "expr")
-> 调用 expr() 得到结果
这里有两层“可见性”:
- 编译期可见性:新
.c文件里要有旧函数的声明,不然编译器不知道test_func()的类型。 - 运行期可见性:旧函数所在
.so必须已经被dlopen(..., RTLD_GLOBAL)加入全局环境,不然新库装载时找不到符号。
3. compile_and_load_function() 的过程
compile_and_load_function() 的任务不是“存字符串”,而是把这段函数定义真正变成当前进程里可用的符号。
典型过程:
-
打开临时
temp.c。 -
把历史函数原型写进去。
-
把本次函数定义写进去。
-
fork出子进程。 -
子进程
execve("/usr/bin/gcc", argv, environ)执行:gcc -shared -fPIC temp.c -o tempN.so -
父进程
waitpid等编译结束。 -
若编译成功,则
dlopen(tempN.so, RTLD_NOW | RTLD_GLOBAL)。 -
成功后从函数定义里提取函数原型,保存给后续代码生成使用。
这里“保存函数原型”不是为了调用,而是为了后面生成新 .c 文件时补声明,例如:
int f(int x);
int g() { return f(42); }
4. evaluate_expression() 的过程
表达式不能直接 dlsym,因为 dlsym 找的是符号地址,而不是“裸表达式”。
所以表达式必须先被包装成一个真正的函数。
例如用户输入:
test_eval() / 2
需要临时生成类似:
int test_eval();
int expr() { return test_eval() / 2; }
然后流程与定义函数类似:
- 写临时
expr.c。 gcc -shared -fPIC expr.c -o exprN.so。dlopen(exprN.so, RTLD_NOW | RTLD_GLOBAL)。dlsym(handle, "expr")得到函数指针。- 调用
expr(),把返回值写进*result。
这里 wrapper 最关键的一点是:必须 return expression;,否则函数返回值未定义。
5. 关键接口
5.1 装载共享库:dlopen
void *dlopen(const char *filename, int flags);
本实验里最重要的 flags:
RTLD_NOW:现在就解析符号,失败立刻暴露。RTLD_GLOBAL:把这个库导出的符号放进全局可见环境,供后续新库使用。
如果前面定义了:
int f() { return 42; }
后面定义:
int g() { return f() + 1; }
那么 f 所在的库必须是 RTLD_GLOBAL 加载的,否则装载 g 时可能找不到 f。
5.2 查找 wrapper:dlsym
void *dlsym(void *handle, const char *symbol);
用法:
int (*fun)() = dlsym(handle, "expr");
这里 expr 必须是你生成的 wrapper 名字。
dlsym 找到的是符号地址,所以表达式必须先包成函数。
5.3 错误信息:dlerror
char *dlerror(void);
当 dlopen 或 dlsym 失败时,它能给出“未定义符号”“库打不开”等人类可读错误信息。
调动态链接问题时很有用。
5.4 行编辑和历史:readline / add_history
char *readline(const char *prompt);
void add_history(const char *line);
作用:
readline提供可编辑输入行。add_history把输入加入历史,之后上下键才能翻。
注意:
- 只有
readline()不够;不调用add_history(),上下键没有历史可翻。 - 链接时还需要
-lreadline。
6. Makefile 与链接
本实验的主程序本身要链接:
-ldl:为了dlopen/dlsym-lreadline:为了readline/add_history
它们本质上属于链接选项,应放进 LDFLAGS,而不是混在只管编译参数的 CFLAGS 里。
7. 本地测试思路
这题的本地验证可以分三层:
-
能否编译
crepl本身:make clean make -
手动 REPL 冒烟:
int f() { return 42; } f() 21 + 21 int g() { return f() / 2; } g() undefined_function() 21 + -
运行
tests.c里的UnitTest- 这些测试会直接调用
compile_and_load_function()和evaluate_expression()。 - 如果
main()是无限 REPL,测试触发方式要额外处理,否则程序不会自然退出。
- 这些测试会直接调用
8. 这次实现里踩过的坑
- 把所有动态库都输出到同一个固定路径,如
/tmp/tmp.so或/tmp/expr.so,导致dlopen复用旧对象,新代码没有真正装入。 - 把共享库路径写成
.c,把源码文件和.so混淆。 execve("usr/bin/gcc", ...)少了开头的/;execve不帮你查PATH。- 子进程
execve失败后没有立刻_exit(...),导致失败路径被误当成功路径继续执行。 - 写完临时
.c后没fclose就编译,结果gcc读到半截文件。 - 只保存了旧函数的存在,却没在新生成的
.c文件里写旧函数声明,导致编译期找不到test_func()的类型。 - 误以为“之前已经
dlopen过函数库”就不需要声明;实际上编译期声明和运行期符号解析是两回事。 - 表达式 wrapper 写成
int expr(){ expression; },忘了return,函数返回值未定义。 dlsym要找的是函数名,不是裸表达式;所以必须显式生成expr()之类的 wrapper。- 在字符串拼接时只覆盖
_buffer,导致前序函数原型没真的写进临时源码。 - 以为用了
readline就天然支持上下键历史;实际上还要add_history()。 - 头文件加上了
readline,但 Makefile 没加-lreadline,最终报undefined reference to readline。
9. 总结
- 这题的本质不是“自己实现 C 解释器”,而是“把编译器和动态链接器接成一个 REPL”。
gcc -shared -fPIC负责把输入代码变成共享对象。dlopen(..., RTLD_NOW | RTLD_GLOBAL)负责把函数定义累积进当前进程的符号环境。dlsym负责从表达式 wrapper 中找到真正可调用的函数入口。- 编译期声明和运行期符号可见性必须同时满足,才能让“后定义代码调用先定义函数”稳定成立。
代码
点击查看代码
#include <stdio.h>
#include <stdbool.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include <readline/readline.h>
#include <readline/history.h>
const char C_PATH[] = "/tmp/tmp.c";
char SO_PATH[1 << 16], _buffer[1 << 16];
extern char ** environ;
char *fun_sig[105];
int fun_cnt = 0;
// Compile a function definition and load it
bool compile_and_load_function(const char* function_def) {
FILE *fp = fopen(C_PATH, "w");
for (int i = 0; i < fun_cnt; i++) {
fprintf(fp, "%s\n", fun_sig[i]);
}
fprintf(fp, "%s\n", function_def);
fclose(fp);
sprintf(SO_PATH, "/tmp/tmp%d.so", fun_cnt);
int pid = fork();
if (pid == 0) {
const char *argv[] = {
"gcc", "-shared", "-fPIC", C_PATH, "-o", SO_PATH, NULL
};
execve("/usr/bin/gcc", argv, environ);
_exit(1);
} else {
int status;
void *handle;
if (waitpid(pid, &status, 0) < 0) return 0;
if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) return 0;
if ((handle = dlopen(SO_PATH, RTLD_NOW | RTLD_GLOBAL)) == NULL) return 0;
int n = strlen(function_def);
int ptr = 0;
for (int i = 0; i < n; i++) {
if (function_def[i] == '{') break;
_buffer[ptr++] = function_def[i];
}
_buffer[ptr++] = ';';
_buffer[ptr] = '\0';
fun_sig[fun_cnt++] = (char *)malloc(sizeof(char) * (ptr + 1));
memcpy(fun_sig[fun_cnt - 1], _buffer, ptr + 1);
}
return 1;
}
// Evaluate an expression
int expr_cnt = 0;
bool evaluate_expression(const char* expression, int* result) {
int len = 0;
for (int i = 0 ; i < fun_cnt; i++) {
sprintf(_buffer + len, "%s\n", fun_sig[i]);
len += strlen(fun_sig[i]) + 1;
}
sprintf(_buffer + len, "int expr(){return %s;}", expression);
FILE *fp = fopen(C_PATH, "w");
fprintf(fp, "%s", _buffer);
fclose(fp);
sprintf(SO_PATH, "/tmp/expr%d.so", expr_cnt);
int pid = fork();
if (pid == 0) {
const char *argv[] = {
"gcc", "-shared", "-fPIC", C_PATH, "-o", SO_PATH, NULL
};
execve("/usr/bin/gcc", argv, environ);
_exit(1);
} else {
int status;
void *handle;
int (*fun)();
if (waitpid(pid, &status, 0) < 0) return 0;
if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) return 0;
if ((handle = dlopen(SO_PATH, RTLD_NOW | RTLD_GLOBAL)) == NULL) return 0;
if ((fun = dlsym(handle, "expr")) == NULL) return 0;
++expr_cnt;
*result = fun();
}
return 1;
}
bool is_function(const char *buf) {
if (strlen(buf) >= 3 && buf[0] == 'i' && buf[1] == 'n' && buf[2] == 't') return 1;
return 0;
}
int main() {
while (1) {
char *buffer = readline("crepl>");
if (buffer == NULL) break;
add_history(buffer);
int ptr = 0;
while (ptr < (1 << 16) && buffer[ptr] == ' ') ptr++;
char *_buffer = buffer + ptr;
if (is_function(_buffer)) {
if (compile_and_load_function(_buffer)) {
printf("Ok, function added.\n");
} else {
printf("Function input error!\n");
}
}
else {
int res;
if (evaluate_expression(_buffer, &res)) {
printf("Expression evaluated = %d.\n", res);
} else {
printf("Expression input error!\n");
}
}
}
}
</details>

浙公网安备 33010602011771号