前言

终于结束了痛苦的L2,虽然感觉确实学到了很多东西。这篇博客完成M4实验

M4 C-Real-Eval-Print-Loop(crepl)

实验背景

很多编程语言都提供了交互式的read-eval-print-loop(REPL),更俗一点的名字就是”交互的shell”。例如在命令行中输入python,就可以和Python shell交互了。现代程序设计语言都提供类似的措施,即便是非解释型的程序设计语言,也提供了类似的措施,例如Scala REPLgo-eval

实际上,C这种编译型的语言,同样可以实现”交互式”的shell——即支持即使定义函数,并且可以计算C表达式的数值。如果输入一段代码,例如strlen("Hello, World"),这段代码会经历gcc编译、动态加载、调用执行,并最终讲代码执行得到的数值11,打印到屏幕上

在本次实验中,将实现一个非常简单的C交互式shell

实验描述

crepl - 逐行从stdin中输入,根据内容进行处理:

  • 如果输入的一行定义了一个函数,则把函数编译并加载到进程的地址空间中
  • 如果输入是一个表达式,则把它的值输出

描述

解释执行每一行标准输入中的C“单行”代码(假设我们只是用int类型,即所有输入的表达式都是整数;定义函数的返回值也永远是整数),分如下两种情况:

  • 函数总是以int开头,例如
    1
    int fib(int n) { if (n <= 1) return 1; return fib(n - 1) + fib(n - 2); }
    函数接收若干int类型的参数,返回一个int数值。如果一行是一个函数,则希望其将经过gcc编译,并被加载到当前进程的地址空间中。函数可以引用之前定义过的函数。
  • 如果一行不是以int开头,可以认为这一行是一个C语言的表达式,其类型为int,例如
    1
    1 + 2 || (fib(3) * fib(4))

函数和表达式均可以调用之前定义过的函数,但不允许访问全局的状态(变量)或调用标准C中的函数。如果一行既不是合法的函数(例如调用了不允许调用的函数),也不是合法的表达式,crepl可以不保证它们执行的结果(不一定要报告错误,例如程序可以照常编译或执行,但程序要尽量不会崩溃);重复定义重命名函数,也可以当做undefined behavior,不必做出过多处理。

和之前的实验一样,并不严格限制程序的输出格式,只要每一个函数或表达式输出一行即可

实验标准

只要可以正确解析单行的函数(以int开头),并且默认其他输入都是表达式即可。可能输入不合法的C代码(例如不合法的表达式);程序应该给出错误提示,而非直接崩溃。

  • 注意允许函数和表达式调用之前(在crepl中)定义过的函数
  • 将创建的临时文件都放在/tmp/目录下。可以使用mkstemp family API创建临时文件
  • 禁止使用C标准库systempopen。这稍微增加了实验的难度,不过并没有增加多少

实验指南

解析读入的命令

框架代码里,已经包含了读入命令的循环(看起来像是一个小shell),其打印出一个提示符,然后接受输入,并进行解析

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char *argv[]) {
static char line[4096];
while (1) {
printf("crepl> ");
fflush(stdout);
if (!fgets(line, sizeof(line), stdin)) {
break;
}
printf("Got %zu chars.\n", strlen(line)); // WTF?
}
}

当在终端里按下Ctrl-d,会结束stdin输入流,fgets会得到NULL

在上述代码里,如果读入的字符串以int开头,就可以假设是一个函数;否则就可以认为是一个表达式

把函数编译成共享库

对于一个一行的函数,例如

1
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }

在之前课程中讲解过编译成共享库(shared object, so)的代码。该实验中,只需要讲文件保存到临时的文件里,例如a.c中,然后使用正确的选项调用gcc即可

选取合适的路径和文件名

如果工具在当前目录下创建文件,则有可能会失败——例如,程序可能会在一个没有访问权限的工作目录上(如文件系统的根/)。在/tmp中创建临时文件是更安全的做法。此外,glibc中还提供了mkstemp family API调动,帮助生成命名唯一的临时文件。

除了编译和命名的问题,另一个可能的困惑是,如果函数调用了其他函数怎么办?例如如下代码

1
int foo() { return bar() + baz(); }

实际上,如果编译上述程序,其可以被编译!忽略所有的warnings即可

把表达式编译成共享库

把函数编译成共享库是常规操作——库函数主要就是为我们提供函数的。
而对于表达式,可以将其包装成函数,然后进行编译即可——例如gcd(256, 144)

1
2
3
int __expr_wrapper_4() {
return gcd(256, 144);
}

注意到函数中的名称——可以通过数字为表达式生成不一样的名称。这样,输入的表达式就成为了一个函数,从而将其编译成共享库。

如果将动态库加载到地址空间,并且得到__expr_wrapper_4的地址,则可以直接进行函数调用,从而获取表达式的值

共享库的加载

实验中,可以使用dlopen加载共享库。在Makefile中,已经添加了-ldl的链接选项,可以通过阅读相关库函数的手册来学习,亦可以通过man 5 elf进行查看

试试自己用mmap加载?

我们可能会好奇dlopen等一系列函数,到底做了什么。
实际上,可以自己hack一下该程序。

我们可以假设函数仅仅访问局部变量,则可以通过解析ELF文件,将共享库的代码部分提取出来,甚至更简单的,只需要使用一个mmap将整个文件映射到地址空间中,并解析ELF文件中的符号,从而找到符号的对应地址即可。

实际上,这样就实现了一个最简单的动态加载器!

实验环境

切换到master分支,然后从github上拉取M4实验即可

1
git remote add jyy https://hub.fastgit.org/NJU-ProjectN/os-workbench.git && git checkout master && git pull jyy M4

实验实现

下面是个人的思路及其实现,实验实现

函数

实际上,处理函数,就是将其写入到临时源文件中,然后调用gcc编译成动态链接库文件,然后进行装载,并解析其中的导入符号和导出符号即可。

虽然如此,过程中还有许多需要注意的细节——例如父进程处理子进程的异常信息等

编译动态链接库文件

实际上,将输入的函数编译成动态链接库文件还是非常方便的——将输入的函数内容写入到临时源文件中,然后执行gcc -m32/-m64 -xc -fPIC -shared -o [dst] [src]命令完成编译

这里特别说明两点

  1. 由于实验指南中允许使用execv家族的函数,这里就使用execvp,从而无需特别添加路径信息
  2. 可以通过readlink函数、/proc/self/fd/[fd]和传入的文件描述符,获取临时文件的真实路径信息

    其相关的实现代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    /*
    * 即将line写入到给定的文件描述符中
    *
    * 仅仅在子进程中调用,因此如果出现异常,直接kill即可
    */
    static void
    write_to_file(const char *line, const int fd)
    {
    if(line == NULL) { return; }

    int remain_size = strlen(line), write_size = 0, idx = 0;

    while(remain_size) {
    write_size = write(fd, line + idx, remain_size);

    if(write_size < 0) {
    dprintf(STDERR_FILENO, "%s\n", strerror(errno));
    kill(getpid(), SIGCHLD);
    }

    remain_size -= write_size;
    idx += write_size;

    }

    if(fsync(fd) != 0) {
    dprintf(STDERR_FILENO, "%s\n", strerror(errno));
    kill(getpid(), SIGCHLD);
    }
    }


    /*
    * 执行gcc -xc -fPIC -shared -o
    * 这里需要根据源文件和目标文件的fd,获取其路径信息,从而完成上述命令
    *
    * 由于在子进程中执行,因此如果出现异常,直接kill即可
    */
    static void
    compiler_libso(const int src, const int dst)
    {

    char buf[BUF_SIZE] = {0};
    char src_path[BUF_SIZE] = {0}, dst_path[BUF_SIZE] = {0};

    snprintf(buf, BUF_SIZE, "/proc/self/fd/%d", src);
    if(readlink(buf, src_path, BUF_SIZE) == -1) {
    dprintf(STDERR_FILENO, "%s\n", strerror(errno));
    kill(getpid(), SIGCHLD);
    }

    snprintf(buf, BUF_SIZE, "/proc/self/fd/%d", dst);
    if(readlink(buf, dst_path, BUF_SIZE) == -1) {
    dprintf(STDERR_FILENO, "%s\n", strerror(errno));
    kill(getpid(), SIGCHLD);
    }

    char *args[] = {
    "gcc",
    #if defined __x86_64__
    "-m64",
    #elif defined __i386__
    "-m32",
    #endif
    "-w",
    "-xc",
    "-fPIC",
    "-shared",
    "-o",
    dst_path,
    src_path,
    NULL
    };


    /*
    * 实验指南中允许调用exec家族
    * 那么为了方便起见,直接调用execvp即可
    */
    if(execvp(args[0], args) == -1) {
    dprintf(STDERR_FILENO, "%s\n", strerror(errno));
    kill(getpid(), SIGCHLD);
    }
    }


    /*
    * 即子进程编译函数为动态链接库
    *
    * 这里直接写入函数即可,无需过多包装
    *
    * 然后编译即可
    */
    static void
    compile_fun(const char *line, const int fd)
    {
    char buf[BUF_SIZE] = {0};
    int src_file;


    //首先申请一个临时源文件,用来存放待编译的源文件
    init_temp_file_name(buf, "/tmp/crepl-src");
    if((src_file = mkstemp(buf)) == -1) {
    //初始化失败,则输出错误信息即
    dprintf(STDERR_FILENO, "%s\n", strerror(errno));
    kill(getpid(), SIGCHLD);
    }

    //直接写入表达式
    write_to_file(line, src_file);


    //然后编译即可
    compiler_libso(src_file, fd);
    }

装载动态链接库

实际上,装载动态链接库,就是通过mmap函数,将前面编译好的动态链接库映射入内存中即可,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
* 载入子进程编译完成的动态链接库
*
* 路径根据fd,然后调用readlink获取即可
*
* 如果成功载入,则返回载入的地址
* 否则,处理异常并返回NULL即可
*/
static void*
load_libso(const int fd)
{
void *res = NULL;
struct stat sb;


// 获取文件大小
if(fstat(fd, &sb) == -1) {
char buf[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
return NULL;
}


/*
* 这里特别提一下prot字段
* 子进程执行完编译后,fd对应的文件模式就是711
* 而fd打开的模式是O_RDWR | O_CREAT | O_EXCL
*
* 这里以PROT_READ | PROT_WRITE | PROT_EXEC实际上并不冲突,虽然不知道为什么
*/
if((res = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0)) == NULL) {
char buf[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
crepl_print(CREPL_PRINT_MODE_FAIL, buf);

return NULL;
}


#ifdef lib
char buf[BUF_SIZE] = {0}, path[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "/proc/self/fd/%d", fd);
readlink(buf, path, BUF_SIZE);
printf("load the %s at %p\n", path, res);
#endif

return res;
}

解析动态链接库

解析动态链接库,主要通过man 5 elf,获取相关的资料即可,并进行解析即可

实际上,解析动态链接库,主要就是解析动态链接库的符号,也就是处理动态链接库的导入符号导出符号

  1. 导出符号
    处理导出符号,就是将其动态链接库中的全局函数类型的符号名称和符号地址记录下来即可
    具体来说,这里的导出符号在.dynsym段中,其段是一个数组,数组元素的结构如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    typedef struct {
    uint32_t st_name;
    Elf32_Addr st_value;
    uint32_t st_size;
    unsigned char st_info;
    unsigned char st_other;
    uint16_t st_shndx;
    } Elf32_Sym;

    typedef struct {
    uint32_t st_name;
    unsigned char st_info;
    unsigned char st_other;
    uint16_t st_shndx;
    Elf64_Addr st_value;
    uint64_t st_size;
    } Elf64_Sym;

    其中,重点在st_name字段和st_value字段:st_name字段表示符号名称在.dynstr字符串表中的下标;
    st_value表示符号地址相对动态链接库载入的地址的偏移。通过这些,即可确定一个导出符号的名称及其符号地址

  2. 导入符号
    处理导入符号,就是将其依赖库的对应的解析过的导出符号的地址,覆盖到对应的信息中。
    具体来说,导入符号在.rela.plt/.rel.plt段中,其段是一个数组,元素结构如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    typedef struct {
    Elf32_Addr r_offset;
    uint32_t r_info;
    } Elf32_Rel;

    typedef struct {
    Elf64_Addr r_offset;
    uint64_t r_info;
    } Elf64_Rel;

    typedef struct {
    Elf32_Addr r_offset;
    uint32_t r_info;
    int32_t r_addend;
    } Elf32_Rela;

    typedef struct {
    Elf64_Addr r_offset;
    uint64_t r_info;
    int64_t r_addend;
    } Elf64_Rela;

    重点在于r_offset字段和r_info字段:r_offset表示该导入符号的got表的地址相对动态链接库载入地址的偏移,也就是应该将导入符号的地址覆盖到这里;r_info,可以通过ELFN_R_SYM(info)宏,获取该导入符号在.dynsym符号表的下标,从而可以获取导入符号的字符串。因此,通过这些信息,可以将其依赖的导入符号的地址信息覆写到动态链接库的对应地址处

最后,其逻辑如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/*
* 即解析加载的动态链接库
*
* 即保存导出符号,同时覆盖导入符号即可
*/
#if defined __x86_64__
#define uintptr_t uint64_t
#define Elf_Ehdr Elf64_Ehdr
#define Elf_Shdr Elf64_Shdr
#define Elf_Sym Elf64_Sym
#define Elf_Rel Elf64_Rela
#define ELF_ST_TYPE(info) ELF64_ST_TYPE(info)
#define ELF_ST_BIND(info) ELF64_ST_BIND(info)
#define ELF_R_SYM(info) ELF64_R_SYM(info)
#elif defined __i386__
#define uintptr_t uint32_t
#define Elf_Ehdr Elf32_Ehdr
#define Elf_Shdr Elf32_Shdr
#define Elf_Sym Elf32_Sym
#define Elf_Rel Elf32_Rel
#define ELF_ST_TYPE(info) ELF32_ST_TYPE(info)
#define ELF_ST_BIND(info) ELF32_ST_BIND(info)
#define ELF_R_SYM(info) ELF32_R_SYM(info)
#endif
static int
resolve_libso(const void *addr, const int fd)
{

char buf[BUF_SIZE] = {0};

#ifdef sym
char path[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "/proc/self/fd/%d", fd);
readlink(buf, path, BUF_SIZE);
#endif

char *shstrtab = NULL;
Elf_Ehdr *ehdr = (Elf_Ehdr*)addr;
Elf_Shdr *shdr = (Elf_Shdr*)((uintptr_t)addr + (uintptr_t)ehdr->e_shoff);
uint16_t shnum = ehdr->e_shnum;
shstrtab = (char*)((uintptr_t)addr + (uintptr_t)shdr[ehdr->e_shstrndx].sh_offset);

/*
* 遍历section table,获取.dynsym、.rela.plt/.rel.plt和.dynstr名称的section即可
*/
int dynsym_size = 0, rel_size = 0;
char *dynstr = NULL;
Elf_Sym *dynsym = NULL;
Elf_Rel *rel = NULL;

for(int i = 0; i < shnum; ++i) {
//即解析符号表和字符串表
if(strcmp(&shstrtab[shdr[i].sh_name], ".dynsym") == 0) {

dynsym = (Elf_Sym*)((uintptr_t)shdr[i].sh_addr + (uintptr_t)addr);
dynsym_size = (shdr[i].sh_size) / (shdr[i].sh_entsize);

}else if(strcmp(&shstrtab[shdr[i].sh_name], ".dynstr") == 0) {

dynstr = (char*)((uintptr_t)shdr[i].sh_offset + (uintptr_t)addr);

}else if(strcmp(&shstrtab[shdr[i].sh_name], ".rela.plt") == 0 ||
strcmp(&shstrtab[shdr[i].sh_name], ".rel.plt") == 0) {

rel = (Elf_Rel*)((uintptr_t)shdr[i].sh_offset + (uintptr_t)addr);
rel_size = (shdr[i].sh_size) / (shdr[i].sh_entsize);

}
}

#ifdef sym
printf("lib[%s], dynsym => %p, dynsym_size: %d, dynstr => %p, rel => %p, rel_size: %d\n",
path, dynsym, dynsym_size, dynstr, rel, rel_size);
#endif

char *sym_name = NULL;
void *sym_addr = NULL;


/*
* 开始解析导入符号
*/
for(int i = 0; i < rel_size; ++i) {
sym_name = &dynstr[dynsym[ELF_R_SYM(rel[i].r_info)].st_name];
for(int j = 0; j < symbols_size; ++j) {
if(strcmp(sym_name, symbols[j].name) == 0) {
sym_addr = symbols[j].addr;
break;
}
}

if(sym_addr == NULL) {
snprintf(buf, BUF_SIZE, "'%s' undeclared (first use in crepl)\n", sym_name);
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
return 1;
}

*(uintptr_t*)((uintptr_t)addr + (uintptr_t)rel[i].r_offset) = (uintptr_t)sym_addr;

#ifdef sym
printf("import: lib[%s], resolve[%s], got[%p] = %p\n", path, sym_name, (void*)((uintptr_t)addr + (uintptr_t)rel[i].r_offset), sym_addr);
#endif

}



/*
* 开始解析导出符号
*
* 如果ELFN_ST_BIND(info)是STB_GLOBAL,
* 且ELFN_ST_TYPE(info)是STT_FUNC
* 则其是导出符号
*/
for(int i = 0; i < dynsym_size; ++i) {
if(ELF_ST_BIND(dynsym[i].st_info) == STB_GLOBAL && ELF_ST_TYPE(dynsym[i].st_info) == STT_FUNC) {
sym_name = &dynstr[dynsym[i].st_name];
sym_addr = (void*)((uintptr_t)addr + (uintptr_t)dynsym[i].st_value);

symbols[symbols_size].addr = sym_addr;
symbols[symbols_size].name = sym_name;
++symbols_size;

#ifdef sym
printf("export: lib[%s], resolve[%s] => %p\n", path, sym_name, sym_addr);
#endif

}
}

return 0;
}

父进程与子进程通信

实际上,这里的细节就比较多了

  1. 如何建立通信
    这里是通过管道符进行通信的。具体的使用方法可以通过man 2 pipe查看。
    • 在进程中调用pipe(pipefd),初始化一个管道
    • 然后调用fork,创建子进程
    • 初始化父、子进程的管道设置
      • 对于子进程来说,一般是关闭读管道(pipefd[0]);然后通过dup2(pipefd[1], STDERR_FILENO),将子进程标准错误输出重定向到管道写端;然后关闭写管道(pipefd[1])
      • 对于父进程来说,是关闭写管道(pipefd[1]);然后通过fcntl(pipefd[0], F_SETFD, O_NONBLOCK)设置读管道非阻塞即可。这里特别注意一下,一定要关闭父进程的写管道,否则调用read会阻塞——只有当管道没有写引用的时候,才能正常读
  2. 怎样进行通信
    如果gcc没有正确编译,则其没有exit退出,或exit退出值非0。因此,父进程首先通过调用waitpid(pid, &wstatus, 0),等待子进程退出或被信号终止后,通过判断WIFEXITED(wstatus) && !WEXITSTATUS(wstatus),来判断子进程是否正常完成编译——如果为真,则表示gcc正确编译,则输出相关信息即可;否则,编译有异常,则输出中断信号和管道信息

    其逻辑如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    /*
    * 其任务相对简单一些
    * 直接waitpid,等待子进程结束即可
    *
    * 然后通过查看waitpid中的状态信息,判断是正常返回,还是异常结束
    *
    * - 对于函数来说
    * - 如果WIFEXITED为真,且WEXITSTATUS为0,则说明子进程正常编译动态链接库,则解析动态链接库即可
    * - 否则:则输出strsignal和管道的输出信息
    * - 对于表达式来说
    * - 如果WIFEXITED为真,且WEXITSTATUS为0,则说明子进程正常编译动态链接库,则解析动态链接库并执行包装的表达式即可
    * - 否则: 则输出strsignal或者管道输出信息即可
    *
    * 这里需要注意的是
    * 父进程必须关闭管道描述符的写段,否则无法分别进程是否结束:
    * 这里根据网上资料,如果未关闭写段,仍然有指向写段的指针,则即使子进程结束了,但是内核仍然认为有数据要被写入,从而导致无法通过read读取的字节数判断子进程是否终止
    */
    static void
    parent(const char *line, const int line_type, const pid_t pid, const int fd)
    {
    int wstatus;


    //关闭不会使用的管道写端
    if(close(pipefd[1]) != 0) {
    char buf[BUF_SIZE] = {0};
    snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
    crepl_print(CREPL_PRINT_MODE_FAIL, buf);
    return;
    }



    //等待子进程退出或被中断
    if(waitpid(pid, &wstatus, 0) != pid) {
    char buf[BUF_SIZE] = {0};
    snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
    crepl_print(CREPL_PRINT_MODE_FAIL, buf);
    return;
    }


    /*
    * 根据解析的输入形式
    * 处理子进程相应的退出状态以及异常信息
    */

    switch (line_type)
    {
    case LINE_TYPE_FUNC:

    if(WIFEXITED(wstatus) && !WEXITSTATUS(wstatus)) {

    close(pipefd[0]);

    /*
    * 即成功编译函数
    * 此时载入并解析动态链接库文件即可
    */
    load_and_resolve(line, fd);

    }else {

    /*
    * 即编译函数过程中遇见错误
    * 则输出中断信号信息及子进程的标准错误输出即可
    */
    deal_child_error(wstatus, pipefd[0]);

    }

    break;

    case LINE_TYPE_EXP:

    if(WIFEXITED(wstatus) && !WEXITSTATUS(wstatus)) {

    close(pipefd[0]);
    /*
    * 即成功编译表达式
    * 此时载入并执行动态链接库文件即可
    */
    load_and_resolve_and_execute(line, fd);

    }else {

    /*
    * 即编译表达式过程中遇见错误
    * 则输出中断信号信息及子进程的标准错误输出即可
    */
    deal_child_error(wstatus, pipefd[0]);

    }

    break;
    }
    }

表达式

实际上,处理表达式,大体思路和处理函数基本一样——就是将其包装成一个函数,然后写入到临时源文件中,然后调用gcc编译成动态链接库文件,然后进行装载,并解析其中的导入符号和导出符号即可。
最终,调用包装后的符号即可

编译动态链接文件

其思路就是将表达式包装成一个返回值为int类型,无输入参数的函数即可,其返回值就是表达式。
因此,其写入临时源文件的数据要更多一些,其余步骤和处理函数的部分完全一样,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
* 即子进程包装表达式,并且编译成动态链接库
*
* 这里需要将表达式包装成函数写入
* 也就是添加前缀"int __crepl_hawk_wrapper_%d(void)\n{\nreturn"
* 添加后缀“;\n}"
*
* 然后编译即可
*/
static void
compile_exp(const char *line, const int fd)
{
char buf[BUF_SIZE] = {0};
int src_file;


//首先申请一个临时源文件,用来存放待编译的源文件
init_temp_file_name(buf, "/tmp/crepl-src");
if((src_file = mkstemp(buf)) == -1) {
//初始化失败,则输出错误信息即
dprintf(STDERR_FILENO, "%s\n", strerror(errno));
kill(getpid(), SIGCHLD);
}

//首先写入前缀
snprintf(buf, BUF_SIZE, "int %s(void)\n{\n\treturn\n", wrapper_name);
write_to_file(buf, src_file);

/*
* 其次写入表达式
*/
snprintf(buf, BUF_SIZE, "%s", line);
write_to_file(line, src_file);

//最后写入后缀
snprintf(buf, BUF_SIZE, ";\n}");
write_to_file(buf, src_file);



//然后编译即可
compiler_libso(src_file, fd);
}

装载动态链接文件

和处理函数的部分完全一样,这里就不过多赘述。

解析动态链接文件

和处理函数的部分完全一样,这里就不过多赘述。

父进程与子进程通信

实际上,这里有两次父进程与子进程通信——编译包装的表达式为动态链接库,其和处理函数部分的完全一样,不过多赘述;执行包装函数,这里详细说明一下。

目前,经过前面解析动态链接文件,此时我们已经有了包装的表达式的内存地址,因此直接将该地址当做一个函数指针,然后调用即可。

其建立父、子进程通信的方式和前面完全一样,就不过多赘述。
但是其父、子进程通信的方式和前面略微不太一样。父进程根据waitpid(pid, &wstatus, 0)wstatus值判断程序是否异常,而不需要再与WEXITSTATUS(wstatus)——如果WIFEXITED(wstatus)为真,则管道中输出的就是一个int类型的二进制数据,也就是表达式的结果;否则,其一定被信号终止,则输出中断的信号信息即可

其最终的逻辑如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/*
* 执行包装函数
* 也就是symbols[symbols_size - 1]对应的符号
*
* 最终返回的结果,通过管道写端输出即可,这里输出的直接是二进制数据
*/
static void
child_execute()
{
if(strcmp(symbols[symbols_size - 1].name, wrapper_name) != 0) { kill(getpid(), SIGKILL); }

int res = ((int (*)(void))symbols[symbols_size - 1].addr)();
write(pipefd[1], &res, sizeof(res));
exit(EXIT_SUCCESS);
}



/*
* 通过查看waitpid中的状态信息,判断是正常返回,还是异常结束
*
* 如果WIFEXITED(wstatus)为true,则通过管道读端,获取最终结果
* 否则,则输出WTERMSIG对应值即可
*
*/
static void
deal_child_result(const char *line, const int wstatus, const int fd)
{

//设置读管道文件描述符为非阻塞模式
if(fcntl(fd, F_SETFD, O_NONBLOCK) != 0) {
char buf[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
return;
}


if(WIFEXITED(wstatus)) {

//这里子进程执行成功,通过error获取其最终结果
char buf[BUF_SIZE] = {0};
int size = 0, read_size = 0, res;

while((read_size = read(fd, buf + size, BUF_SIZE - 1)) != 0) {
if(read_size > 0) {
size += read_size;
}

usleep(READ_GAP);
}

if(size != sizeof(int)) {
snprintf(buf, BUF_SIZE, "%s\n", "internal error");
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
return;
}

res = *(int*)buf;
snprintf(buf, BUF_SIZE, "(%s) == %d.\n", line, res);
crepl_print(CREPL_PRINT_MODE_SUCCESS, buf);

}else {

//这里子进程异常,则获取其中断信号量
char buf[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "%s\n", strsignal(WTERMSIG(wstatus)));
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
}

}



/*
* 即捕获子进程的返回状态,从而完成输出即可
* 类似于前面子进程编译共享链接库的方式
* 直接waitpid,等待子进程结束即可
*
*/
static void
deal_child_execute(const char *line, const int pid)
{
int wstatus, res;

//关闭不会使用的管道写端
if(close(pipefd[1]) != 0) {
char buf[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
return;
}

//等待子进程退出或被中断
if(waitpid(pid, &wstatus, 0) != pid) {
char buf[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
return;
}


/*
* 处理子进程执行的结果
*/
deal_child_result(line, wstatus, pipefd[0]);
}



/*
* 完成包装函数的执行
* 并且根据包装函数的返回值,输出相关的信息即可
*/
static void
do_execute(const char *line)
{

char buf[BUF_SIZE] = {0};
pid_t pid;

/*
* 首先初始化管道文件符号
* 如果初始化失败
* 直接输出提示信息并且返回即可
*/
if(pipe(pipefd) != 0) {
//初始化失败,则输出错误信息即可
char buf[BUF_SIZE] = {0};
snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
goto DO_EXECUTE_DESTRUCT;
}

switch(pid = fork()) {
case -1:
//即fork失败
snprintf(buf, BUF_SIZE, "%s\n", strerror(errno));
crepl_print(CREPL_PRINT_MODE_FAIL, buf);
break;

case 0:
//执行包装函数并返回其状态信息
child_execute();

default:
//获取子进程的返回信息,并输出
deal_child_execute(line, pid);
}

DO_EXECUTE_DESTRUCT:
close(pipefd[0]);
close(pipefd[1]);
}

实验结果

下面是最终的程序的测试结果
实验结果