1.所谓的词法分析,是可以识别标识符,然后把它们转化为token&,那么第一个问题,我想识别printf这个函数名,我究竟是怎么识别的呢?我怎么就不认为它是变数的呢?我怎么就认为这是一个函数名的呢?

2.第二,我要找到printf这个对应的函数,我是怎么找的呢?是不是我把printf这个标识符和一个字元串(「printf」)进行字元串匹配,如果匹配成功,就说明这个是输出函数,然后跳转到这个输出的代码里去呢?

3.第三,求这段代码,词法分析、语法分析的过程是怎么样的?包括中间代码生成。

#include &

int main(){ printf("Hello,World!
");

return 0;

}


虽然 printf 不是「语言自带」的,我就当你问的是一般意义上的 primitive 操作吧

1

第一种方法:你可以在 tokenizer 的阶段就写死,把所有你想作为内置运算符的直接变成特殊的 token,就像看见 "+" 吐出 Token(BINOP.PLUS) 一样,很多语言里的 if / while / for 之类的就是这么干的,比如 Python 2 里的 print

第二种方法:你可以在你的顶层作用域里把你的标准库函数们绑定上,比如通过在编译器 / 解释器启动的时候载入一个 prelude,然后在编译 / 解释的时候就当作一般的函数去对待,比如 Python 3 里的 print()

2

至于怎么找到对应函数就是看你的 scope 怎么设计,lookup 怎么实现,反正基本就是打表 + 查表

3

请自行完成作业


printf明明就是个标准的函数,你要举例子也应该用sizeof这样的操作符吧……

但是也没有什么区别啊,无非有个keyword表,分词遇到keyword就特殊处理罢了


首先必须说明一点,printf 不是语言自带的,C 中没有任何内置函数。你可以试一下不加 #include & 编译会发生什么(gcc 是会给出 implicit declaration 这样的警告信息的)。

  1. 语言的规则。C 中函数调用就是如下形式:

func_name(args)

所以遇到这种情况就会认为是函数调用。

比如如下的语句:

foo(a, b);

在词法分析阶段会识别为 foo ( a , b ) ; 这几个 token,解析为函数调用是在语法分析(parsing)阶段完成的。当然可能会涉及一些稍复杂(特殊)的情况,比如:

  • 通过函数指针调用
  • function-like 宏(预处理阶段会进行宏展开)
  • sizeof 运算符,这个是通过维护一张 keyword 的表来识别的(《Parsing Techniques》里称这种文法为 Finite Choice Grammar)

2. 这一步是在链接阶段完成的,比如有一个源文件 echo.c 使用了 printf,编译时会在符号表中留下一个占位符,编译完生成目标代码文件 echo.o。静态链接(gcc 时指定 -static 选项)的时候首先会找到 printf 所在的静态库文件(印象中是 libc.a),然后发现 printf 的占位符,会将相应的部分复制到 echo.o 中,最后生成一个(完全链接的)可执行文件。如果是动态链接,情况会更复杂一点,链接是在运行时进行的,CS:APP 第 7 章就是专门讲链接的,可以详细地了解一下。

3. 学完编译原理就知道了

可以先大致了解一下把源文件转换成可执行文件的流程以及对应的 gcc 选项,动手实践一下看看这些过程都做了什么。你的这些问题其实书上都有,如果你真的非常感兴趣,可以看 CS:APP(《深入理解计算机系统》)和龙书(《编译原理》)(以及《Parsing Techniques》)详细地了解。


打个广告 https://github.com/sunziping2016/MyC89Compiler 是我尝试写的一个C89的编译器,现在这个项目已经停滞,希望有兴趣的人能一起来完成这个项目。这个项目前端用的是Flex/Bison,后端用的是LLVM。

接下来,我来回答题主的问题。

1.

printf不是语言自带的,而是标准库自带的。换言之,printf和其他的用户自定义函数在编译的时候没有差别。一般而言,预处理器会将stdio.h展开,其中包含了printf的声明,这样编译器遇到声明的时候,就讲printf加入符号表并标注为是一个extern的函数。当然你也可以手动声明printf,下面的代码也是可以work的。

int printf (const char *format, ...);

int main() {
printf("Hello, World!
");
return 0;
}

那么printf的实现究竟在哪里呢?在linux,它是存在在一个叫libc.so的动态链接库里,这是开源的,一般实现是 https://www.gnu.org/software/libc/ ,一般而言用户只需要二进位的动态链接库,无需关心源码。在windows上,一般而言,对于visual studio开发的程序,这是在一个叫msvcrt.dll的动态链接库里。编译器在生产目标文件的时候会在里面注明需要链接上printf,而后由链接器将目标文件与链接库链接。

2.

我猜题主是不是要写个编译器。如上所述,在编译器阶段,理论上printf和其他函数是不做区别对待的。

3.

首先源代码会经过多个翻译阶段,每个阶段又可能包含词法分析和语法分析,详见 https://en.cppreference.com/w/c/language/translation_phases ,当然有些编译器只使用了其中几个而不是所有阶段。一般而言,预处理和编译是两个使用了语法分析的重要翻译阶段。预处理器处理所有的预处理命令(#开头),所以编译看到的代码如下:

...
int printf (const char *format, ...);
...

int main() {
printf("Hello, World!
");
return 0;
}

然后这是lex的代码,代表著编译阶段的词法分析器 https://www.lysator.liu.se/c/ANSI-C-grammar-l.html ;这是yacc的代码,代表著编译阶段的语法分析器 https://www.lysator.liu.se/c/ANSI-C-grammar-y.html 。代码可读性比较高。


你既然都学到编译器了,怎么会不知道头文件和函数声明。。。还有C语言什么时候有自带函数,printf不是标准库(stdio.h)里里的函数吗。。。


推荐阅读:
相关文章