CALL: libc 和 C++ 标准库

CALL 是 (Compiler/Assembler/Linker/Loader)的简称。如果你和 C/C++ 打过交道的话,没有理由会对这几个词太陌生。所以今天这是一篇水文。

Levels of Representation/Interpretation

level

  • “XX 是一门 解释型语言”
  • “XX 是编译型语言”

抛开正确性,一定程度上我们可以尝试填空(Python / C++、Go)并且知道:

  • Python 可能会运行一个解释器(官方是 CPython, 可能有 Pypy 这样的),然后解释 Python 的字节码
  • C++、Go 会编译成 binary, 就我理解,这是“机器上的字节码”(这是跟 Python 字节码对应的,实际情况可能手是反过来的), 然后执行。这其中 Go 可能会有 Runtime、GC 这样的开销。

但是同时,Python 也能通过一些方式打包成 exe (虽然很巨大),同时 LLVM 这些层次的引入让我们的理解模糊了起来。所以我们要明确一下这个 Level。

A6642C36-5B8D-4168-877C-9D3FCCD84A44

  • Language translation gives us another option
  • In general, we interpret a high-level language when efficiency is not critical and translate to a lower-level language to increase performance
  • Although this is becoming a “distinction without a difference”
 Many intepreters do a “just in time” runtime compilation to bytecode that either is emulated or directly compiled to machine code (e.g. LLVM)

所以这个问题实际上是很含糊不清的,第三点里面 JIT 等的引入更让事情扑朔迷离了起来。具体其实可以参考这个链接里的说法:https://www.zhihu.com/question/19608553。

一般被称为“解释型语言”的是主流实现为解释器的语言,但并不是说它就无法编译。例如说经常被认为是“解释型语言”的Scheme就有好几种编译器实现,其中率先支持R6RS规范的大部分内容的是Ikarus,支持在x86上编译Scheme;它最终不是生成某种虚拟机的字节码,而是直接生成x86机器码。

实际上解释器的性能劣势也不一定是一种坏事,像我去年去 PyCon 听的“慢解释是一种优势”,虽然有点破罐子破摔的味道,但是如果你在 C/C++ 下开 asan/valgrind 或者带 gcc -g, 和 Go 这种带 Runtime 的、V8这些可以提供的debug比较,难免会有羡慕的想法。

Interpreter provides instruction set independence: run on any machine

就是这样。

CALL chain

8009A6B1-A4E5-46A4-A40F-BD41F4E01D34

这是一张水图。可能还要处理一下预处理之类的过程,但是大概流程是这样没错。

Compile

Compile 的过程大概是

  • Lexer
  • Parser
  • Semantic Analysis and Optimization
  • Code generation

不过看上面转的那篇文章,似乎形式有变,这方面我不是很了解。Lexer/Parser 的部分可以参考我之前的 Lex/Yacc 入门。总之,我们现在把源代码编译后可以转化为一种对应的 IR, 即 nmsl.c -> nmsl.S.

Assembler

Assembler 接下来会nmsl.s -> nmsl.o.

  • Reads and Uses Directives
  • Replace Pseudo-instructions
  • Produce Machine Language rather than just Assembly Language
  • Creates Object File

顺便给出这个 part 一个很有意思的 slide:

顺便

ELF

  • object file header: size and position of the other pieces of the object file
  • text segment: the machine code
  • data segment: binary representation of the static data in the source file
  • relocation information: identifies lines of code that need to be fixed up later

  • symbol table: list of this file’s labels and static data that can be referenced

  • debugging information
  • A standard format is ELF (except Microsoft)
 http://www.skyfree.org/linux/references/ELF_Format.pdf

这个我觉得还是 csapp 写得好…总之生成的目标文件会满足这样的形式。

Linker

Combines several object (.o) files into a single executable (“linking”)

5B0267D5-78CF-4CB7-97B7-69D4D5D1DF88

  • Step 1: Take text segment from each .o file and put them together
  • Step 2: Take data segment from each .o file, put them together, and concatenate this onto end of text segments
  • Step 3: Resolve references
    • Go through Relocation Table; handle each entry
    • That is, fill in all absolute addresses

这段我感觉 CSAPP 讲的稍微详细一些。

在应用层面上,这里其实还涉及(不一定是这里引入的)name mangling,calling convention这种 C/C++ 相关的问题,所以可能 extern "C" 在这种情况下就相对很好理解了。

Loader

When one is run, loader’s job is to load it into memory and start it running

In reality, loader is the operating system (OS)

  • loading is one of the OS tasks
  • And these days, the loader actually does a lot of the linking:
 Linker’s ‘executable’ is actually only partially linked, instead still having external references

这里可以参考 CSAPP 里面链接的时机相关的概念。

libc/libc++

qsort 是一个 <cstdlib> 下的函数,如果你去 libc++ 找的话,会发现事情好像不太对:

https://github.com/llvm-mirror/libcxx/blob/7c3769df62c0b3820130aa868397a80a042e0232/include/cstdlib

这里只有 using 和函数声明,没有对应的实现。

实际上 C++ 的标准库(以 libc++) 为例,可能会根据模版生成需要的函数/类。所以我们可以看到对应的一些源代码。

C语言的库函数实际上通常以链接库的形式在 libc 中提供,链接的时候我们找到:https://stackoverflow.com/questions/26277283/gcc-linking-libc-static-and-some-other-library-dynamically-revisited