通过状态机模型,
你已经对程序如何在计算机上运行
建立基本认识
本次课内容:
C程序如何从源代码生成指令序列(二进制可执行文件)
学习处理器设计, 为什么要了解这些?
Talk is cheap, show me the code!
// a.c
#include <stdio.h>
#define MSG "Hello \
World!\n"
#define _str(x) #x
#define _concat(a, b) a##b
int main() {
printf(MSG /* "hi!\n" */);
#ifdef __riscv
printf("Hello RISC-V!\n");
#endif
_concat(pr, intf)(_str(RISC-V));
return 0;
}
方法:
阅读工具的日志(查看是否支持verbose
,
log
等选项)
通过man gcc
并搜索-I
选项可得知头文件搜索的顺序
echo '#warning I am wrong!' > stdio.h
gcc -E a.c
# change <stdio.h> to "stdio.h" in a.c
gcc -E a.c
rm stdio.h
mkdir aaa bbb
gcc -E a.c -Iaaa -Ibbb --verbose > /dev/null
echo '#warning I am wrong, too!' > bbb/stdio.h
gcc -E a.c -Iaaa -Ibbb
echo '#define printf(...)' >> bbb/stdio.h
gcc -E a.c -Iaaa -Ibbb
启发: 动手做一些简单的尝试, 你能学会很多
预处理阶段只进行文本粘贴, 不求值
\
)而拆分的字符串套路: 借助预处理机制编写不可读代码
#\
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 ; }
识别并记录源文件中的每一个token
@
), 则报错
C代码 = 字符串
按照C语言的语法将识别出来的token组织成树状结构
按照C语言的语义确定AST中每个表达式的类型
struct + int
)
但大多数编译器并没有严格按阶段进行词法分析, 语法分析, 语义分析
clang
的-ast-dump
把语义信息也一起输出了
man clang
可以得知clang
的阶段划分在不运行程序的情况下对其进行分析
可以检查/分析以下方面
程序符合C语言的语法, 单独看每条语句也符合C语言的语义
添加编译选项-Wall
, gcc就会进行更多的代码检查工作
use-after-free
的问题
初学者的误解: 让编译器报告更多的警告会给编程带来额外的工作量
中间代码(也称中间表示, 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, ptr %1, align 4
store i32 10, ptr %2, align 4
store i32 20, ptr %3, align 4
%5 = load i32, ptr %2, align 4
%6 = load i32, ptr %3, align 4
%7 = add nsw i32 %5, %6
store i32 %7, ptr %4, align 4
%8 = load i32, ptr %4, align 4
%9 = call i32 (ptr, ...) @printf(ptr noundef @.str, i32 noundef %8)
ret i32 0
}
将C语言状态机翻译成IR状态机
为什么不直接翻译到处理器ISA?
5
种源语言, 8
种目标语言
5*8=40
个模块; 用IR,
只要编写5+8=13
个模块clang
使用的中间代码叫LLVM IR
,
gcc
的叫GIMPLE
对IR进行优化, 用更少的IR来实现同一个程序的行为
下一小节再展开讨论
clang -S a.c
clang -S a.c --target=riscv64-linux-gnu
gcc -S a.c # 也可以用gcc生成
# apt-get install g++-riscv64-linux-gnu
riscv64-linux-gnu-gcc -S a.c
将IR状态机翻译成处理器ISA状态机
编译器还会进行与目标ISA相关的优化
编译优化是现代软件构建过程中的重要步骤
真实项目普遍都使用编译优化技术
将来你也会在自己设计的处理器上运行各种经过编译优化的程序
#include <stdio.h>
int fib(int n) {
if (n == 0 || n == 1) return 1;
return fib(n - 2) + fib(n - 1);
}
int main() {
int n;
for (n = 0; n < 10; n ++) {
int ans = fib(40);
printf("ans = %d\n", ans);
}
return 0;
}
直觉的定义: 若两个程序在某种意义上一致
,
可以用简单
的替代复杂
的
严谨的定义:
一致
= 程序的可观测行为(C99
5.1.2.3节第6点)的一致性
volatile
关键字修饰变量的访问需要严格执行
看起来一致
看起来一致
正确
的为了方便理解, 我们用C代码来呈现优化前后的语义
// 优化前 | 优化后
int a = f1(); | int x = f1() + 2;
for (i = 0; i < 10; i ++) { | for (i = 0; i < 10; i ++) {
int x = a + 2; | int y = f2(x);
int y = f2(x); | sum += y + i;
sum += y + i; | }
} |
其他编译优化技术: 归纳变量分析, 循环展开, 软流水, 自动并行化, 别名和指针分析等
可在clang
的命令行中给出-O1
选项来开启更多的编译优化工作:
加个volatile
试试
编译器通常会提供不同的优化等级
在gcc
中, 针对程序性能, 有以下优化等级:
-O0
- 默认的优化等级, 大部分优化技术都关闭-Og
- 仅采取那些对调试较友好的优化技术
-O1
- 开启优化-O2
- 开启更多优化, 大部分软件项目采用的优化等级-O3
- 尝试通过生成更多的代码来换取更高的程序性能-Ofast
- 甚至采取一些违反语言标准的优化策略,
来换取更高性能gcc
还提供一些面向代码大小的优化等级:
-O0
- 默认的优化等级, 大部分优化技术都关闭-O1
- 开启优化-Os
- 面向代码大小的优化等级, 类似-O2
,
但关闭那些经常增加代码大小的优化技术-Oz
- 更激进的代码大小优化, 会牺牲性能
合并多个目标文件, 生成可执行文件
让我们来看日志!
有很多crtxxx.o
的文件
objdump
确认./a.out
# 通过一些配置工作, RISC-V的可执行文件也可以在本地执行
# apt-get install qemu-user qemu-user-binfmt
# qemu-riscv64 -h # 查看环境变量QEMU_LD_PREFIX的默认值
# mkdir -p /usr/gnemul/
# ln -s /usr/riscv64-linux-gnu/ /usr/gnemul/qemu-riscv64
file a.out
a.out: ELF 64-bit LSB pie executable, UCB RISC-V...
./a.out # 实际上是在QEMU模拟器中执行
执行程序 = 执行指令序列
编译出生成的可执行文件在外存(磁盘或SSD)中, 怎么把它放置在内存?
在Logisim中,
通过GUI的Load Image
操作将程序的指令序列读入ROM
将程序的指令序列放置在内存
的工作执行./a.out
时, 究竟是谁完成这项工作?
加载器
的特殊程序, 它的工作是
加载程序是程序运行之前的必要步骤, 因此加载器属于运行时环境的一部分
crtxxx.o
这些目标文件,
其中就包含加载器的部分功能在上述./a.out
的执行过程中, 运行时环境的作用还体现在:
main()
开始执行吗? 至少很多C语言书籍也这么说
main()
的argc
和argv
的呢?
main()
printf()
,
并不包含printf()
的代码
./a.out
的时候确实成功通过printf()
输出了信息crtxxx.o
以直接或者间接方式提供了执行printf()
的方法
main()
返回后就直接退出
main()
执行之前还做了很多准备工作main()
返回之后应该回到运行时环境
用strace和gdb都能确认
光有程序本身还不能运行