上次课内容:
本次课内容:
C程序如何从源代码生成指令序列(二进制可执行文件)
Talk is cheap, show me the code!
方法:
阅读工具的日志(查看是否支持verbose
,
log
等选项)
预处理阶段只进行文本粘贴, 不求值
\
)而拆分的字符串#ifdef
/#else
/#endif
#
##
更多信息请RTFM
套路: 借助预处理机制编写不可读代码
#\
define C(c /**/)#c
/*size=3173*/#include<stdio.h>
/*crc=b7f9ecff.*/#include<stdlib.h>
/*Mile/Adele_von_Ascham*/#include<time.h>
typedef/**/int(I);I/*:3*/d,i,j,a,b,l,u[16],v
[18],w[36],x,y,z,k;char*P="\n\40(),",*p,*q,*t[18],m[4];
void/**/O(char*q){for(;*q;q++)*q>32?z=111-*q?z=(z+*q)%185,(k?
k--:(y=z%37,(x=z/37%7)?printf(*t,t[x],y?w[y-1]:95):y>14&&y<33?x
=y>15,printf(t[15+x],x?2<<y%16:l,x?(1<<y%16)-1:1):puts(t[y%28])))
,0:z+82:0;}void/**/Q(I(p),I*q){for(x=0;x<p;x++){q[x]=x;}for(;--p
>1;q[p]=y)y =q[x=rand()%-~p],q[x]=q[p];}char/**/n[999]=C(Average?!nQVQd%R>Rd%
R% %RNIPRfi#VQ}R;TtuodtsRUd%RUd%RUOSetirwf!RnruterR{RTSniamRtniQ>h.oidts<edulc
ni #V>rebmun<=NIPD-RhtiwRelipmocResaelPRrorre#QNIPRfednfi#V__ELIF__R_
Re nifed#V~-VU0V;}V{R= R][ORrahcRdengisnuRtsnocRcitatsVesle#Vfidne#V53556
. .1RfoRegnarRehtRniRre getniRnaRsiR]NIP[R erehwQQc.tuptuoR>Rtxt.tupniR
< R]NIP[R:egasuV_Redulcn i#VfednfiVfednuVenife dVfedfiVQc%Rs%#V);I/**/main(
I( f),char**e){if(f){for(i= time(NULL),p=n,q= n+998,x=18;x;p++){*p>32&&!(
*--q=*p>80&&*p<87?P[*p- 81]:* p)?t [( -- x)]=q+1:q;}if(f-2||(d=atoi
(e[1]))<1||65536<d){;O(" \""); goto O;}srand(i);Q(16,u);i=0;Q(
36,w);for(;i<36; i++){w[i] +=w [i]<26 ? 97:39; }O(C(ouoo9oBotoo%]#
ox^#oy_#ozoou#o{ a#o|b#o}c# o~d#oo-e #oo. f#oo/g#oo0h#oo1i#oo
2j#oo3k#oo4l#o p));for(j =8;EOF -(i= getchar());l+=1){a=1+
rand()%16;for(b =0;b<a||i- main (0,e);b++)x=d^d/4^d/8^d/
32,d= (d/ 2|x<<15)&65535; b|= !l<<17;Q(18,v);for(a=0;a<18;
a++ ){if( (b&(1<<(i=v[a] ))))* m=75+i,O(m),j=i<17&&j<i?i:j;}O(C(
!) ); }O(C(oqovoo97o /n!));i= 0;for(;i<8;O(m))m[2]=35,*m=56+u[i],m[1
]= 75 +i++;O(C(oA!oro oqoo9) );k=112-j*7;O(C(6o.!Z!Z#5o-!Y!Y#4~!X!X#3}
!W !W #2 |!V!V#1{!U!U#0z! T!T#/y!S!S#.x!R!R#-w!Q!Q#ooAv!P!P#+o#!O!O#*t!N!
N# oo >s!M!M#oo=r!L!L#oo<q!K!K# &pIo@:;= oUm#oo98m##oo9=8m#oo9oUm###oo9;=8m#o
o9 oUm##oo9=oUm#oo98m#### o09] #o1:^#o2;_#o3<o ou#o4=a#o5>b#o6?c#o
7@d#o8A e#o 9B f#o:Cg#o; D h#o<Ei #o=Fj#o> Gk#o?Hl#oo9os#####
));d=0 ;} O: for(x=y=0;x<8;++
x)y|= d&(1<<u[x])?
1<< x:0;return
/* :9 */
y ; }
词法分析 -> 语法分析 -> 语义分析 -> 中间代码生成 -> 优化 -> 目标代码生成
借助合适的工具(clang
),
我们来看看每一个阶段都在做什么
clang
功能上等价于gcc
,
但能向我们更好地展示编译的中间步骤识别并记录源文件中的每一个token
C代码 = 字符串
按照C语言的语法将识别出来的token组织成树状结构
按照C语言的语义确定AST中每个表达式的类型
struct + int
)
但大多数编译器并没有严格按阶段进行词法分析, 语法分析, 语义分析
clang
的-ast-dump
把语义信息也一起输出了
man clang
可以得知clang
的阶段划分在不运行程序的情况下对其进行分析
可以检查/分析以下方面
中间代码(也称中间表示, IR) = 编译器定义的, 面向编译场景的指令集
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 0, i32* %1, align 4
store i32 1, i32* %2, align 4
store i32 2, i32* %3, align 4
%5 = load i32, i32* %2, align 4
%6 = load i32, i32* %3, align 4
%7 = add nsw i32 %5, %6
store i32 %7, i32* %4, align 4
%8 = load i32, i32* %4, align 4
%9 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i64 0, i64 0), i32 noundef %8)
ret i32 0
}
将C语言状态机翻译成IR状态机
为什么不直接翻译到处理器ISA?
clang
使用的中间代码叫LLVM IR
,
gcc
的叫GIMPLE
如果两个状态机在某种意义上 “相同”, 就可以用 “简单”的替代 “复杂”的
“相同” = 程序的可观测行为(C99 5.1.2.3节第6点)的一致性
这给编译器优化提供了非常广阔的空间
例: 常数传播
加个volatile试试
clang -S a.c
clang -S a.c --target=riscv32-linux-gnu
gcc -S a.c # 也可以用gcc生成
# apt-get install g++-riscv64-linux-gnu
riscv64-linux-gnu-gcc -march=rv32g -mabi=ilp32 -S a.c
将IR状态机翻译成处理器ISA状态机
可以通过time report观察clang
尝试了哪些优化工作
gcc -c a.c
riscv64-linux-gnu-gcc -march=rv32g -mabi=ilp32 -c a.c
# alias rv32gcc="riscv64-linux-gnu-gcc -march=rv32g -mabi=ilp32"
根据指令集手册, 把汇编代码(指令的符号化表示)翻译成二进制目标文件(指令的编码表示)
合并多个目标文件, 生成可执行文件
让我们来看日志!
有很多crtxxx.o
的文件
objdump
确认问题: printf()
的代码在哪里呢?
./a.out
# 通过一些配置工作, RISC-V的可执行文件也可以在本地执行
# apt-get install qemu-user qemu-user-binfmt
# mkdir -p /etc/qemu-binfmt
# ln -s /usr/riscv64-linux-gnu/ /etc/qemu-binfmt/riscv64
file a.out
a.out: ELF 64-bit LSB pie executable, UCB RISC-V, version 1 (SYSV)...
./a.out # 实际上是在QEMU模拟器中执行
把可执行文件加载到内存, 跳转到程序, 执行编译出的指令序列
Q: 谁来加载?
A: 运行时环境(宿主操作系统/QEMU)
关于执行的更多内容将在下次课介绍
32位 | 64位 | |
---|---|---|
c90 | TT | FT |
c99 | FT | FT |
根据clang
输出的AST整理不同组合下2147483648
的类型
32位 | 64位 | |
---|---|---|
c90 | unsigned long |
long |
c99 | long long |
long |
输出结果与2147483648
的符号有关
F
T
long
可以表示2147483648
->
long
是64位long
不能表示2147483648
->
long
是32位猜想: long
在32位环境下长度是32位,
在64位环境下长度是64位
怎么验证/推翻这个猜想?
动手写个小程序就可以啦
表面上看是这样, 但C语言标准真的是这么说的吗?
5.2.4.2 Numerical limits
An implementation is required to document all the limits specified in this
subclause, ...
5.2.4.2.1 Sizes of integer types <limits.h>
... Their implementation-defined values shall be equal or greater in magnitude
(absolute value) to those shown, with the same sign.
C语言标准并没有明确定义类型的长度, 而是全部交给具体实现来定义
C99的Abstract
... Its purpose is to promote portability, reliability, maintainability, and
efficient execution of C language programs on a variety of computing systems.
3.6 byte
NOTE 2 A byte is composed of a contiguous sequence of bits, the number of which is
implementation-defined
int
是16位int
是32位int
还是32位use of an unspecified value, or other behavior where this International Standard
provides two or more possibilities and imposes no further requirements on which is
chosen in any instance
C标准提供了多种行为可选, 具体实现需要选择
例: 函数调用时参数求值顺序是unspecified
#include <stdio.h>
void f(int x, int y) {
printf("x = %d, y = %d\n", x, y);
}
int main() {
int i = 1;
f(i ++, i ++);
return 0;
}
包含这种行为的程序, 重新编译后可能得到不同的结果
一类特殊的未指定行为, 具体实现需要将选择写到文档里
例: 类型的长度
包含这种行为的程序, 在相同的环境下运行可以得到相同的结果
behavior, upon use of a nonportable or erroneous program construct or of erroneous
data, for which this International Standard imposes no requirements
程序/数据不符合标准的行为
例: 缓冲区溢出
包含这种行为的程序, 多次运行可能也无法得到正确的结果
C99关于implementation的定义(第5节):
An implementation translates C source files and executes C programs in two
data-processing-system environments, which will be called the translation environment
and the execution environment in this International Standard...
在真实的系统中,
这相当于编译器+(处理器+操作系统+运行库)
这就是ABI(Application Binary Interface), 具体包含
ABI手册是计算机系统软硬件协同的重要体现
Q: 如何使用跨平台固定长度的数据类型?
A: #include <stdint.h>
A: char
的符号也是implementation-defined的
char
来进行算术运算
signed char
或unsigned char
“使用语言”和 “学习计算机”的目的不完全相同