您的位置:首页 > 其它

软件动态更新技术总结之1 CIL:程序分析与变换的中间语言工具(sec0-sec3)

2011-06-02 15:24 495 查看
前言

更新软件的传统做法是首先关闭旧版本软件,安装新软件,最后启动新软件。如果是对可靠性实时性要求低的软件,这么做是可以的。但如果对需要提供连续服务的软件做更新,重启系统通常是不容许的,重启系统会造成服务中断,运行时状态和数据丢失等重大伤害。

我们需要对软件动态地更新,让软件在运行过程中完成版本的更替,状态数据的保存恢复,并且,不需要额外硬件的支持!很棒,是吧?(可以联想到云计算了)国际上对软件动态更新有很漂亮的研究和实现,我随后会有详细介绍,所以就有了软件动态更新技术系列文章。很久以前就有人提醒:如果不是业界的大牛就不要对某个领域做综述——因为那需要作者具备非常高的概览层次,所以我就不命名为综述文章,改成“总结文章”。虽然关注、研究CIL已经很久了,但我在实验室做的动态更新与他们的思路很不同,所以在这里介绍这些公开发表的研究是安全的。

写作风格

翻译+注释。不做一些[注n]之类的文案工作了,直接用//注释 或/*注释*/做注释,注释的文本为斜体字。

第一篇是伯克利的Necula教授在2002年LNCS的一篇关于一种C中间语言的论文(CIL: Intermediate Language and Tools for
Analysis and Transformation of C Programs)。嗯LNCS还是有很好的论文的!CIL是一种更利于静态分析和source-source编译的c的中间语言。一些很好的研究是基于CIL的,所以需要介绍它。

缩略词

source-source compile 源源编译

IL 中间语言

IR 中间表示

parse 语法分析

lvalue 左值

AST 抽象语法树

摘要:描述了CIL:一种C程序的高级表示及工具集,以此可以方便的分析和源-源变换。
与C语言相比,CIL的程序构造更少。它把C中的某些复杂构造拆解成简单的程序构造,因此它比AST的层次低。但是CIL比典型的用于编译的IL(如三地址码)层次高,于是我们得到的是一个易于分析和操纵的c程序,然后把它们以一种与source接近的形式(emit)出来。并且它有一个将ANSI C 和Microsoft C /*这是因为微软也资助了这项研究*/ 以及GNU C 翻译成CIL的前端。

为了描述CIL的结构,我们将重点描述如何区分那些在程序分析/变换中最令人费解的C的特点。我们也描述了一个基于类型的结构等价的整程序合并器,这可以把整个工程视为一个单独的编译单元。我们展示了一个使代码免于堆栈破坏(stack-mashing)的变换作为CIL的典型应用。当前我们使用CIL作为一个分析、插装C程序的一部分,这个系统利用运行时检查来保证类型安全/* 这项工作叫做CCured,它的官方网站已经关闭了,我好不容易才下载到它以前的源代码 */。

1、引论

blablabla套话。。。

1 struct { int *fld; } *str1;

2 struct { int fld[5]; } str2[4];

3 str1[1].fld[2];

4 str2[1].fld[2];

图1 二义文法的C程序片段

抽取精确的C程序意义常常需要另外处理文法,如图1中#3和#4行。它们有同样的语法但是不用语义。#3涉及了3个内存引用,#4涉及了1个内存引用/*虾米,没看出来?不要紧,后面还有注释*/。当然低级的表示不会有这样的二义性,它们通常丢失了类型、循环和其他搞成构造的结构信息。并且这种低级表示打印之后很难看出跟原source有啥关系。我们的目标是在这两种方式之间折衷。

我们瞄准的应用是可执行分析和源源变换的C程序系统。对这样的任务,一个好的IL应该是易于分析、接近源码、可处理真实世界的代码。

CIL减少了语法和概念的形式,比如:

所有循环结构都规约成单一形式

所有函数体都有明确的return,去除“->”这样的语法糖

将类型声明从代码中分析,在函数体内实行显式的类型提升和扁平作用域(用了阿尔法换名)

这些化简办法减少了操纵C程序需要考虑的情况。多数C编译器也做了许多这样的步骤,但是CIL通过暴露抽象语法的更多结构,从而使分析变得更容易。

CIL的概念设计尽量与C保持一致,这样关于CIL程序得到的结论可以简单地映射回原C程序的语句。从CIL到C的翻译较易,包括重建C的那些习惯用法。

最后,对CIL的一个关键需求:parse并表示真实世界中代码的各种构造,比如编译器相关的扩展和行内汇编。(CIL支持所有的GCC和MSVC的扩展(除了嵌套函数(内嵌函数)),CIL可以处理整个linux的内核。

论文的其他部分描述了对C特性的处理和CIL的应用。

sec2 描述了左值lvalue的语法、类型和语义

sec3 提出了表达式和指令

sec4 提出了控制流信息

sec5 详细讲了我们是如何处理类型的

sec6 源代码层次的属性( __attribute__(()) )

sec7 用CIL做分析

sec8 将CIL用于多文件程序

sec9 相关工作

sec10未来展望

2、处理左值

左值是涉及一个存储区域的表达式。只有lvalue才能出现在赋值的左侧。理解lvalue需要的不仅是一个简单的AST,正如图1所示,str1[1].fld[2]依类型不同,可能涉及1、2、3个内存引用。解释如下:

当str和fld都是array类型,记作str, fld : array,程序涉及一个单一连续对象str1的偏移,所以是1个内存引用,记作1 mem ref。。。/*简言之,就是:

1 mem ref <= str, fld : array

2 mem ref <= str1 : array, fld : ptr。因为str1[1].fld首先被装载(1 ref),然后引用一个从这个值开始的偏移fld+2 => str[1].fld[2](2 ref)。

3 mem ref <= str, fld : ptr,类似的,str+1=>str[1](1 ref), ref str[1](2 ref),str[1].fld+2=>str[1].fld[2](3 ref)。*/

于是,那些关心这些区别的程序分析器就会发现,用AST形式很难分析lvalue。

lvalue ::= <lbase, loffset>
lbase ::= Var(variable) | Mem(exp)
loffset ::= NoOffset | Field(field, loffset) | Index(exp, loffset)

图2 CIL左值的抽象语法

正如图2所示,CIL的左值表示成base+offset的二元组,基址可以是变量的存储的起始地址或任何指针表达式我们区分了这两种情况,于是可以迅速判断我们是在访问变量的成分还是在通过指针访问一个内存区域。loffset由field序列或(索引指示器)index designator组成。

lvalue的意义是内存地址+存储于此的对象的类型。图3展示了定义这个含义的定义,变量的基址是指变量的地址和类型。

Γ(x) = τ Γ | e : Ptr(τ )

——————————— ——————————
Γ | Var(x) ⇓ (&x, τ ) Γ | Mem(e) ⇓ (e, τ )

————————————
Γ  (a, τ)@NoOffset ⇓ (a, τ )

τ1 = Struct(f : τf , ...) Γ | (a1 + OffsetOf (f, τ1), τf)@off ⇓ (a2, τ2)

————————————————————————————————————
Γ | (a1, τ1)@Field(f, off ) ⇓ (a2, τ2)

τ1 = Array(τ ) Γ | (a1 + e ∗ SizeOf (τ ), τ)@off ⇓ (a2, τ2)

——————————————————————————————
Γ | (a1, τ1)@Index(e, off ) ⇓ (a2, τ2)

图3 CIL左值的类型和求值规则

/*这些外表凶恶的公式其实很温柔,像巴黎圣母院敲钟人卡西莫多一样。如果没学过数理逻辑,只需要稍稍解释一下,长横线的上方表示前提,下方表示结论,而|-表示语法推出,|=表示语义推出*/

Γ |- lbase ⇓ (a, τ) 指lvalue的基址lbase引用了一个类型为τ,位于地址a的对象。lvalue的偏移作为在同一对象内移动<地址,类型>元组的函数。

Γ |-(a1, τ1)@o ⇓ (a2, τ2) 将o这个偏移应用到一个左值(a1, τ1),产生左值(a2, τ2) 。

重新考虑图1中的例子,可用CIL表示为str1[1].fld[2]=< Mem ( 2 + Lvalue < Mem ( 1 + Lvalue < Var (Str1), NoOffset > ), Field (fld, NoOffset ) > ) >

(2个访存uhah!)

str2[1].fld[2] = < Var (str2), Index ( 1, Index (2, NoOffset ) ) >,/*原文如此,其实这里明显不对——为啥连fld都没出现捏?我想正确的CIL表示应该是< Var (str2), Index ( 1, Field (fld, Index (2, NoOffset ) ) ) >*/

上述对lvalue的解释支持标准C的等值式,如 x == *&x 和 (*(&a.f)).g == a.f.g,这使得一些任务(如为程序插装内存访问)更加容易。正如其他的IR,同一变量的所有出现共享一个变量声明。这可以方便的修改变量属性(如名字和类型),并且允许在比较变量时使用指针相等测试。

3、表达式和指令

CIL语法有3个基本概念:表达式、指令、陈述/语句。

表达式代表函数式计算,没有副作用或控制流。

指令代表副作用,包括函数的调用,不包括过程内控制流。

语句代表局部控制流。

CIL表达式的抽象语法在图4中

常量的原始文本与值一起保存和维护。

SizeOf 和 Alignof 同时被保留,因为计算它们依赖编译器和编译选项,也因为某个程序变换可能希望改变类型。

Cast需要被显式插入,以使程序符合我们的类型系统。/* 类型系统是常见而重要的概念:用于定义如何将程序语言中的数值和表达式归类为不同类型,如何操作这些类型,这些类型如何相互作用。类型可以确认一个或一组值具有特定的目的和意义。类型系统在各种语言中有非常大的不同,也许最主要的差异存在于编译时的语法和运行时的操作实现方式。大牛说过类型系统像攀岩时使用的保护绳。有可能救你的命,也有可能不管用,但,有总比没有好。 */

StartOf 表达式在C中没有明确的与之对应的语法,但是用于表示从array到数组首元地址的隐式强制转换。若无此规则,为 *expr做的类型判定必须基于expr的类型做分情讨论,会导致两个完全不同的类型规则。添加StartOf允许语法制导的类型检查(依靠在源代码中插入显式转换)。StartOf操作符不可打印,是唯一一种将array转换为指向其首元指针的办法,其规则如下

lvalue ⇓ (a, Array(τ))

——————————————
StartOf(lvalue) ⇓ (a, Ptr(τ))

其他的表达式(例如 ?: 操作符或那些有副作用的表达式)都转换成CIL的指令或语句,下面将讨论之。

exp ::= Constant(const) | Lvalue(lvalue) | SizeOfExp(exp)
| SizeOfType(type) | AlignOfExp(exp) | AlignOfType(type)
| UnOp(unop, exp) | BinOp(binop, exp, exp) | Cast(type, exp)
| AddressOf(lvalue) | StartOf(lvalue)
instr ::= Set(lvalue, exp)
| Call(lvalue option, exp, exp list)
| Asm(raw strings, lvalue list, exp list)

图4 CIL的表达式和语句的语法

每个指令都包含一个单独的赋值或函数调用。Set指令更新lvalue的值,Call指令有一个可选的lvalue,用于存放函数的返回值。Call指令中的函数成分必须是函数类型的。CIL会去除应用到函数或函数指令上的冗余的&,*操作。函数的参数是表达式(无副作用或嵌入的控制流)。

最后,Asm指令用于捕获在系统编程中很常用的行内汇编。CIL可以识别微软和GNU风格的汇编指令,并且报告出汇编块的输入(表达式列表)和输出(左值列表)。

其他信息(如易失性volatility,原始汇编模板)会被存储而非解析。

CIL也存储所有语句的位置信息,并利用这些位置信息在发射(emit)输出时插入行指示(#line directive)。这允许在复杂变换后的程序错误信息与源代码之间建立同步关系。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐