NJU OS M4 C Read-Eval-Print-Loop

M4: C Read-Eval-Print-Loop (crepl)

简化题意:维护一个小型 C REPL。用户可以先输入函数定义,把它们加入当前进程的符号环境;之后再输入表达式,程序把表达式包装成函数、动态编译、动态装载并执行,输出结果。

Lab 4: crepl 与动态链接接口笔记

1. 这题真正要做的事

这题表面上像“解释器”,但实现上并不是自己解析并执行 C 表达式,而是把 gcc 和动态链接器当作后端:

  1. 用户输入函数定义。
  2. 程序生成临时 .c 文件。
  3. 调用 gcc -shared -fPIC 编译出 .so
  4. dlopen.so 加入当前进程。
  5. 用户输入表达式时,再生成一个只包含 wrapper 的临时 .so
  6. 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() 的任务不是“存字符串”,而是把这段函数定义真正变成当前进程里可用的符号。

典型过程:

  1. 打开临时 temp.c

  2. 把历史函数原型写进去。

  3. 把本次函数定义写进去。

  4. fork 出子进程。

  5. 子进程 execve("/usr/bin/gcc", argv, environ) 执行:

    gcc -shared -fPIC temp.c -o tempN.so
    
  6. 父进程 waitpid 等编译结束。

  7. 若编译成功,则 dlopen(tempN.so, RTLD_NOW | RTLD_GLOBAL)

  8. 成功后从函数定义里提取函数原型,保存给后续代码生成使用。

这里“保存函数原型”不是为了调用,而是为了后面生成新 .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; }

然后流程与定义函数类似:

  1. 写临时 expr.c
  2. gcc -shared -fPIC expr.c -o exprN.so
  3. dlopen(exprN.so, RTLD_NOW | RTLD_GLOBAL)
  4. dlsym(handle, "expr") 得到函数指针。
  5. 调用 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);

dlopendlsym 失败时,它能给出“未定义符号”“库打不开”等人类可读错误信息。
调动态链接问题时很有用。

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. 本地测试思路

这题的本地验证可以分三层:

  1. 能否编译 crepl 本身:

    make clean
    make
    
  2. 手动 REPL 冒烟:

    int f() { return 42; }
    f()
    21 + 21
    int g() { return f() / 2; }
    g()
    undefined_function()
    21 +
    
  3. 运行 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>
posted @ 2026-06-12 23:34  Katyusha_Lzh  阅读(4)  评论(0)    收藏  举报