570 likes | 768 Views
编 译 原 理 教学联系编程语言实际的探讨. 中国科学技术大学 计算机科学技术系 陈意云 0551-3607043, yiyun@ustc.edu.cn. 课 程 的 目 的 和 意 义. 要让学生 对程序设计语言的设计和实现有深刻的理解 对和程序设计语言有关的理论有所了解 对宏观上把握程序设计语言来说起一个奠基的作用 有助于快速理解、定位和解决在程序编译、测试与运行中出现的问题 把所学 概念和技术应用到一般软件 的 设计中 编译技术在软件安全、程序理解和软件逆向工程等方 面有着广泛的应用。. 教 学 内 容 分 析.
E N D
编 译 原 理教学联系编程语言实际的探讨 中国科学技术大学 计算机科学技术系 陈意云 0551-3607043,yiyun@ustc.edu.cn
课 程 的 目 的 和 意 义 要让学生 • 对程序设计语言的设计和实现有深刻的理解 • 对和程序设计语言有关的理论有所了解 • 对宏观上把握程序设计语言来说起一个奠基的作用 • 有助于快速理解、定位和解决在程序编译、测试与运行中出现的问题 • 把所学概念和技术应用到一般软件的设计中 编译技术在软件安全、程序理解和软件逆向工程等方 面有着广泛的应用。
教 学 内 容 分 析 一门非常难学的课程 • 编译程序规模较大,不可能把所有细节都讲清楚 • 涉及不少理论知识,如形式语言和自动机理论、语法制导定义和属性文法、类型系统、程序分析等 • 包含很多大大小小的算法
教 学 内 容 分 析 教学上采取的策略 • 涉及的基本理论必须了解 • 对编译原理和技术的宏观理解及全局把握比了解一些局部的算法重要得多 • 准备了很多从实际程序编译和运行时碰到的问题中抽象出来例子 • 规模较大的课程设计:两个合作完成一个Java小子集的编译器
教 学 联 系 实 际 的 探 讨 在课堂上和习题中准备了实例 • 源于实际程序的编译或运行时碰到的问题 • 选择C语言作为实例语言 • 编译成汇编语言程序 • 不是类型安全的语言 • 语言教学略去C语言中一些较复杂的概念和特征 • 引导用所学的知识去分析、解决、理解实际编程中遇到的问题和发生的现象 • 激发学习编译原理和技术的热情
例题1: 类型系统 在X86/Linux机器上,编译器报告最后一行有 错误:incompatible types in return typedef int A1[10]; | A2 *fun1( ) typedef int A2[10]; | { return(&a); } A1 a; | typedef struct {int i;}S1; | S2 fun2( ) typedef struct {int i;}S2; | { return(s); } S1 s; 在C语言中,数组和结构都是构造类型,为什 么第2个函数有类型错误,而第1个函数没有?
例题1: 类型系统 在X86/Linux机器上,编译器报告最后一行有 错误:incompatible types in return typedef int A1[10]; | A2 *fun1( ) typedef int A2[10]; | { return(&a); } A1 a; | typedef struct {int i;}S1; | S2 fun2( ) typedef struct {int i;}S2; | { return(s); } S1 s; 在C语言中,除结构类型外,都是用结构等价 的方式,而对结构类型用的是名字等价的方式
例题2: 类型系统 C语言 • 称&为地址运算符,&a为变量a的地址 • 数组名代表数组第一个元素的地址 问题: 如果a是一个数组名,那么表达式a和&a的值都是数组a第一个元素的地址,它们的使用是否有区别的?
例题2: 类型系统 C语言 • 称&为地址运算符,&a为变量a的地址 • 数组名代表数组第一个元素的地址 问题: 如果a是一个数组名,那么表达式a和&a的值都是数组a第一个元素的地址,它们的使用是否有区别的 用四个C文件的编译报错或运行结果来提示
例题2: 类型系统 typedef int A[10][20]; A a; A *fun() { return(a); } 该函数在Linux上用gcc编译时,报告的错误如下: 第6行:warning: return from incompatible pointer type
例题2: 类型系统 typedef int A[10][20]; A a; A *fun() { return(&a); } 该函数在Linux上用gcc编译时,没有错误。
例题2: 类型系统 typedef int A[10][20]; typedef int B[20]; A a; B *fun() { return(a); } 该函数在Linux上用gcc编译时,没有错误。
例题2: 类型系统 typedef int A[10][20]; A a; fun() { printf(“%d,%d,%d\n”, a, a+1, &a+1);} main() { fun();} 该程序的运行结果是: 134518112, 134518192, 134518912
例题2: 类型系统 结论 对元素类型为t的数组a[ i1 ][ i2 ]…[ in]来说, • 表达式a的类型是 pointer(array(0.. i2 –1, … array(0.. in –1, t)…)) • 表达式&a的类型是 pointer(array(0.. i1 –1, … array(0.. in –1, t)…))
例题3: 存储分配 在SPARC/Solaris工作站上,下面两个结构 的size分别是24和16,为什么不一样? typedef struct _a { typedef struct _b { char c1; char c1; long i; char c2; char c2; long i; double f; double f; } a; } b;
例题3: 存储分配 在SPARC/Solaris工作站上,下面两个结构 的size分别是24和16,为什么不一样? typedef struct _a { typedef struct _b { char c1; 0 char c1; 0 long i; 4 char c2; 1 char c2; 8 long i; 4 double f; 16 double f; 8 } a; } b; 数据在存储分配时有对齐问题
例题3: 存储分配 在X86/Linux机器的结果和SPARC/Solaris工 作站不一样,是20和16。 typedef struct _a { typedef struct _b { char c1; 0 char c1; 0 long i; 4 char c2; 1 char c2; 8 long i; 4 double f; 12 double f; 8 } a; } b; 不同机器的对齐情况不一样
例题4: 存储分配 在X86/Linux机器上,下面两个结构的size分 别是6和8,为什么不一样?(short和long分别 对齐到2和4) typedef struct _a { | typedef struct _b { short i; | long i; short j; | short k; short k; | } b; } a;
例题4: 存储分配 在X86/Linux机器上,下面两个结构的size分 别是6和8,为什么不一样?(short和long分别 对齐到2和4) typedef struct _a { | typedef struct _b { short i; | long i; short j; | short k; short k; | } b; } a; 需要考虑到它们作为数组元素时的连续存放
例题5: 存储分配 一个C语言程序及其在X86/Linux操作系统上的编译结 果如下。根据所生成的汇编程序来解释程序中四个变 量的存储分配、作用域、生存期和置初值方式等方面 的区别 static long aa = 10; --静态外部变量 short bb = 20; --外部变量 func() { static long cc = 30; --静态内部变量 short dd = 40; --自动变量 }
例题5: 存储分配 .data | .align 4 .align 4 | .type cc.2,@object .type aa,@object | .size cc.2,4 .size aa,4 | cc.2: aa: | .long 30 .long 10 | .text .globl bb | .align 4 .align 2 | .globl func .type bb,@object | func: .size bb,2 | . . . bb: | movw $40,-2(%ebp) .value 20 | . . .
例题5: 存储分配 • 存储分配 • aa、bb和cc分配在静态数据区,dd分配在栈区 • 作用域 • bb是全局的,aa局部于本文件,cc改名为cc.2以免和其它函数中的名字出现冲突,dd局部于函数 • 生存期 • aa、bb和cc的生存期都是整个程序运行时间,dd的生存期是func函数的相应激活的活动期间 • 置初值方式 • aa、bb和cc的置初值是在目标程序装入时完成,dd是动态置初值
例题6: 存储分配 main() { char *cp1, *cp2; cp1 = "12345"; cp2 = "abcdefghij"; strcpy(cp1,cp2); printf("cp1 = %s\ncp2 = %s\n", cp1, cp2); } 运行结果是: cp1 = abcdefghij cp2 = ghij 为什么cp2所指的串被修改了?
例题6: 存储分配 因为常量串“12345”和“abcdefghij”连续分配 在常数区 执行前: 1 2 3 4 5 \0 a b c d e f g h i j \0 cp1 cp2
例题6: 存储分配 因为常量串“12345”和“abcdefghij”连续分配 在常数区 执行前: 1 2 3 4 5 \0 a b c d e f g h i j \0 cp1 cp2 执行后: a b c d e f g h i j \0 f g h i j \0 cp1 cp2
例题6: 存储分配 因为常量串“12345”和“abcdefghij”连续分配 在常数区 执行前: 1 2 3 4 5 \0 a b c d e f g h i j \0 cp1 cp2 执行后: a b c d e f g h i j \0 f g h i j \0 cp1 cp2 现在的编译器大都把程序中的串常量单独存 放在一个只读的数据段中,则运行时报错
例题7: 存储分配与函数调用 下面的程序运行时输出3个整数。试从运行 环境和printf的实现来分析,为什么此程序会 有3个整数输出? main() { printf(“%d, %d, %d\n”); }
例题7: 存储分配与函数调用 下面的程序运行时输出3个整数。试从运行 环境和printf的实现来分析,为什么此程序会 有3个整数输出? main() { printf(“%d, %d, %d\n”); } 调用时参数逆序进栈,便于被调用函数找到 第一个实参,由第一个实参确定后面有多少个 参数
例题8: 存储分配与函数调用 int fact(i) | main() { int i; | printf("%d\n", fact(5)); { | printf("%d\n", fact(5,10,15)); if(i==0) | printf("%d\n", fact(5.0)); return 1; | printf("%d\n", fact()); else | } return i*fact(i-1); } 该程序在X86/Linux机器上的运行结果如下: 120 120 1 Segmentation fault (core dumped)
例题8: 存储分配与函数调用 请解释下面问题: • 第二个fact调用:结果为什么没有受参数过多的影响? • 第三个fact调用:为什么用浮点数5.0作为参数时结果变成1? • 第四个fact调用:为什么没有提供参数时会出现Segmentation fault?
例题8: 存储分配与函数调用 请解释下面问题: • 第二个fact调用:结果为什么没有受参数过多的影响? 解答:参数表达式逆序计算并进栈,fact能够取到第一个参数
esp 局部变量 ebp 控制链 低 返址 栈 参数 . . . 高 例题8: 存储分配与函数调用 请解释下面问题: • 第三个fact调用:为什么用浮点数5.0作为参数时结果变成1? 解答:参数5.0转换成双精 度数进栈,占8个字节。 它低地址的4个字节看成整 数时正好是0。
esp fact局部变量 ebp fact控制链 低 fact返址 栈 main控制链 main返址 高 例题8: 存储分配与函数调用 请解释下面问题: • 第四个fact调用:为什么没有提供参数时会出现Segmentation fault? 解答:由于没有提供参数, fact把老ebp(控制链) (main的活动记录中保存 的ebp)当成参数,它一定 是一个很大的整数,使得活 动记录栈溢出。
例题9: 代码生成策略 在SPARC/SUNOS上,经某编译器编译,程序的结果 是120。把第4行的abs(1)改成1的话,则程序结果是1 int fact() { static int i=5; if(i==0) { return(1); } else { i=i-1; return((i+abs(1))*fact());} } main() { printf("factor of 5 = %d\n", fact()); }
例题9: 代码生成策略 int fact() { static int i=5; if(i==0) { return(1); } else { i=i-1; return((i+abs(1))*fact());} } main() { printf("factor of 5 = %d\n", fact()); } 由表达式的代码生成策略引起的 • 先计算有函数调用的子表达式,若两边都有时,仍按从左到右的次序计算。
例题10: 代码生成策略 下面C语言程序如下, 运行时输出105, 为什么? main() { long i; i=10; i=(i+5) + (i=i*5); printf("%d\n",i); }
需几个R 二元运算 表达式 表达式 需2个R 需3个R 例题10: 代码生成策略 下面C语言程序如下, 运行时输出105, 为什么? main() { long i; i=10; i=(i+5) + (i=i*5); printf("%d\n",i); } 由寄存器分配策略决定了先算右边子表达式
例题11: 代码生成策略 下面的程序在X86/Linux机器上编译后的运行结 果是7,而在SPARC/SUNOS机器上编译后的运 行结果是6。试分析运行结果不同的原因。 main() { long i; i = 0; printf("%ld\n", (++i)+(++i)+(++i) ); }
+ + := := := i i + + i + i i i 1 1 1 例题11: 代码生成策略 按一般的代码生成,i = i +1的计算结果保留在 寄存器中,因此这三个i = i +1的计算次序不会 影响最终的结果。结果应该是6。
+ + := := := i i + + i + i i i 1 1 1 例题11: 代码生成策略 按一般的代码生成,i = i +1的计算结果保留在 寄存器中,因此这三个i = i +1的计算次序不会 影响最终的结果。结果应该是6。 结果是7的话,一定是 某个i = i +1的结果未保 留在寄存器中。上层 计算对它的引用落在 计算另一个i = i +1的 后面
+ + := := := i i + + i + i i i 1 1 1 例题11: 代码生成策略 • 如果机器有INC指令的话,编译器极可能产生一条INC指令来完成i = i +1 • X86/Linux机器上果真是这么做的
+ + := := := i i + + i + i i i 1 1 1 例题11: 代码生成策略 将表达式改成(++i)+((++i)+(++i)), 结果会怎样?
+ + := := := i i + + i + i i i 1 1 1 例题11: 代码生成策略 将表达式改成(++i)+((++i)+(++i)), 结果会怎样? 在SPARC/SUNOS机器上的结果仍然是6。 在X86/Linux机器上的结果是9。
例题12: 代码优化 UNIX 下的C编译命令cc的选择项g和O的解释如下, 其中dbx的解释是“dbx is an utility for source-level debugging and execution of programs written in C”。 试说明为什 么用了选择项g后,选择项O便被忽略 -g Produce additional symbol table information for dbx(1) and dbxtool(1) and pass -lg option to ld(1) (so as to include the g library, that is: /usr/lib/libg.a). When this option is given, the -O and -R options are suppressed. -O[level] Optimize the object code. Ignored when either -g, -go, or -a is used. ...
例题12: 代码优化 • dbx是一个源级的调试工具,它要求编译器产生的目标代码能让程序员按照源程序的控制结构,逐条语句地观察程序变量值的变化情况。因此目标代码的执行流程必须准确地对应到按源程序的控制结构逐条语句地执行源程序 • 优化有可能改变程序的执行流程(如代码外提,无用赋值删除)。另外,复写传播导致的无用赋值删除还可能使得变量的存储单元的值没有及时更新(从源程序执行流程的观点),其更新的值在某个寄存器中 • C编译命令的这两个选择项的要求是矛盾的,因此只能保留一个
例题13: 编译和连接 long gcd(p,q) long p,q; { if (p%q == 0) return q; else return gcd(q, p%q); } main() { printf("\n%ld\n",gcdx(4,12)); } 上述程序用gcc命令得到的编译结果如下 In function ‘main’:undefined reference to ‘gcdx’ ld returned 1 exit status. 请问,这个gcdx没有定义,是在编译时发现的,还是 在连接时发现的?
例题13: 编译和连接 long gcd(p,q) long p,q; { if (p%q == 0) return q; else return gcd(q, p%q); } main() { printf("\n%ld\n",gcdx(4,12)); } 上述程序用gcc命令得到的编译结果如下 In function ‘main’:undefined reference to ‘gcdx’ ld returned 1 exit status. 请问,这个gcdx没有定义,是在编译时发现的,还是 在连接时发现的? 连接时发现
例题14: 编译和连接 一些C程序设计的教材上指出“在需要使用标准I/O 库中的函数时,应在程序前使用 #include <stdio.h> 预编译命令,但在用printf和scanf函数时,则可以不 要。” 但事实上并非仅限于这两个函数。例如下面的C程 序编译后运行时输出字符A并换行,它并没有预编译 命令#include <stdio.h>。试解释为什么 main() { putchar('A'); putchar('\n'); }
例题15: 编译和连接 C的一个源文件可以包含若干个函数,该源文 件经编译可以生成一个目标文件;若干个目标 文件可以构成一个函数库。如果一个用户程序 引用库中的某个函数,那么,在连接时的做法 是下面三种情况的哪一种,说明你的理由。 • 将该库函数的目标代码连到用户程序 • 将该库函数的目标代码所在的目标文件连到用户程序 • 将该函数库全部连到用户程序
例题15: 编译和连接 是第二种情况 • 第一种方式似乎最合理,但是C语言是以文件为编译单位,生成的目标文件对应到源文件 • 另一方面,一个函数库虽然可以由几个目标文件组成,但是它们只是简单地组成一个库,因此从库中抽出一个目标文件并不困难