程序员的自我修养 - 从Hello World!说起

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。

背景知识

  • 不要让CPU打盹:分时多任务、进程、抢占式分配CPU;
  • 磁盘:扇区、LBS(逻辑扇区号)、文件系统;
  • 内存不够怎么办:1. 分段(解决地址空间不隔离);2. 分页【页大小、虚拟页/物理页、页错误和MMU(虚拟地址到物理地址的转换)】(解决使用效率低);
  • 线程:一个进程由一个或多个线程组成;一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成;状态(就绪、运行、等待);Linux的多线程(fork、exec和clone、写时复制);
  • 线程安全:竞争与原子操作、同步与锁(二元信号量和信号量【即多元信号量】、互斥量和临界区、读写锁、条件变量);可重入;
  • volatile:阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回;
  • 对象构造(pInst = new PInst())包含三个顺序的步骤:1. 分配内存;2. 在内存的位置上调用构造函数;3. 将内存的地址赋值给pInst;【在上述三个步骤中,CPU完全有可能动态调整2和3的执行顺序!】
  • 线程并发模型:1. 一对一模型;2. 多对一模型(将多个用户线程映射到一个内核线程上);3. 多对多模型;

Hello World 程序构建

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf("Hello World!");
return 0;
}

初级:

  1. 预处理: $ gcc -E hello.c -o hello.i 或 $ cpp hello.c > hello.i
  2. 编译: $ gcc -S hello.i -o hello.s(现在版本的 GCC 把预处理和编译两个步骤合并成一个步骤,使用一个cc1的程序。)
  3. 汇编:$ as hello.s -o hello.o 或 $ gcc -c hello.s -o hello.o(根据汇编指令和机器指令的对照表一一翻译即可。)
  4. 链接:$ ld -static /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-linux-gnu/4.1.3/crtbeginT.o -L/usr/lib/gcc/1486-linux-gnu/4.1.3 -L/usr/lib -L/lib hello.o —start-group -lgcc -lgcc_eh -lc —end-group /usr/lib/gcc/i486-linux-gnu/4.1.3/crtend.o /usr/lib/crtn.o

中级一:编译器做了什么

  • 扫描:有限状态机(分割成一系列记号,如关键字,字面量、特殊符号等),涉及的程序有lex;
  • 语法分析:上下文无关语法和下推自动机 -> 语法树,涉及的程序有yacc;
  • 语义分析:静态语义和动态语义(在运行期才能确定的语义)-> 为语法树的节点标识了类型;
  • 源代码优化:三地址码和P-代码;生成中间代码(其将编译器划分为前端和后端)
  • 目标代码生成及优化:编译器后端包括代码生成器和目标代码优化器,这个过程十分依赖目标机器。事实上,定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。

中级二:模块拼装(静态链接)

  • 地址和空间分配
  • 符号决议(Symbol Resolution)
  • 重定位(Relocation):每一个要被修正的地方叫做一个重定位入口(Relocation Entry)

中级三:目标文件里有什么(概念,理论)

知识补充(4种ELF格式,Linux下可使用file命令查看):
ELF,COFF格式的变种,COFF(Common File Format)的主要贡献是在目标文件里面引入了“段”的机制;

  1. 可重定位文件(.o/.obj),
  2. 可执行文件(linux可执行文件或.exe)
  3. 共享目标文件(.so/.dll),分两种:一种是供链接库链接以产生新的目标文件;另一种则是与可执行文件结合,作为进程映像的一部分来运行;
  4. 核心转储文件
  • 一般目标文件将这些信息按不同的属性,以“节”的形式存储,有时候也叫“段”;它们唯一的区别是在ELF的链接视图和装载视图。
    1. 文件头:描述整个文件的文件属性,包括文件是否可执行、是静态还是动态链接,程序入口地址等;还包括一个段表,用以描述文件中各个段在文件中的偏移位置及段的属性等;
    1. 机器指令 -> 代码段(.code或.text段);
    1. 全局变量和局部静态变量数据 -> 数据段(.data段)
    1. 未初始化的全局变量和局部静态变量 -> .bss段(预留位置,不占文件空间)
  • 指令和数据分段的好处:有利于保护(指令段只读)、提高缓存命中率、共享;

中级三:目标文件里有什么(示例, PC_x64)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int printf( const char* format, ... );
int global_init_var = 84;
int global_uninit_var;
void func1( int i ) {
printf( "%d\n", i );
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1( static_var + static_var2 + a + b );
return a;
}
// $ gcc -c SimpleSection.c
// $ objdump -h SimpleSection.o 结合 $ readelf -a SimpleSection.o
// $ objdump -s -d SimpleSection.o # -s以十六进制打印所有段的内容,-d将所有包含指令的段反汇编

各个段的header信息
各个段详细信息及部分汇编

  • 将二进制文件,如图片,MP3等作为目标文件中的一个段的办法:
  • $ objcopy -I binary -O elf32-i386 -B i386 image.jpg image.o
  • $ objdump -ht image.o
  • 程序员自定义段不能使用“.”作为前缀,示例(hackr?): attribute((section(“FOO”))) int global = 42;

中级三:目标文件里有什么(补充材料)

  • ELF文件头信息:$ readelf -h SimpleSection.o
  • ELF文件头结构及相关常数被定义在 “/usr/include/elf.h”
  • ELF文件段表:$ readelf -S SimpleSection.o
  • 重定位表:对于每一个需要重定位的代码段或数据段,都会有一个相应的重定位表。
  • 字符串表


未完待续

以上内容摘录,总结于以下参考书籍:

  1. 《程序员的自我修养——链接、装载与库》,俞甲子、石凡、潘爱民著