支持整数-指针转换的C内存模型

支持整数‐指针转换的正式C语言内存模型

摘要

ISO C标准未规定使用非可移植惯用法(如整数‐指针转换)的许多有效程序的语义。近期关于C语言的形式化定义和形式化验证实现也继承了这一特性。通过采用高级抽象内存模型,这些工作支持常见优化。然而,这使得无法对大量依赖于常见实现行为的底层代码进行推理,而形式化验证在这些场景中具有广泛的应用。

我们提出了首个允许许多常见优化并完全支持对指针表示进行操作的形式化内存模型。所有算术操作对于已类型转换为整数的指针都是明确定义的。重要的是,我们的模型也易于理解和编程使用。我们所有的结果都在Coq中得到了完全形式化。

分类与主题描述符 D.3.1[编程语言]:形式化定义与理论; D.2.4[软件工程]:软件/程序验证
Keywords C内存模型, 整数‐指针转换, 编译器, 优化, 验证

1. 引言

众所周知,ISO C标准 [5] 未对大量语法上合法的C语言程序给出语义。相反,许多程序表现出实现定义、未指定或未定义的行为,其中未定义行为对符合标准的实现不施加任何要求。这导致了较为争议的做法:复杂的C语言编译器从未定义行为的实例出发进行反向推理,从而得出某些代码路径必定为死代码等结论。此类变换可能导致令人惊讶的程序行为非局部变化以及难以发现的错误 [13, 14]。

因此,为了准确捕捉C标准的细微之处,人们已经做出了大量努力,这些努力要么是通过提供一种替代性的语言定义,要么是提供一种符合标准的实现 [2, 9, 11]。

C内存模型一直备受关注:跨平台的内存底层访问是C语言家族语言的显著特征,对于操作系统内核和语言运行时等应用至关重要。然而,微妙的指针别名规则[6], 、对特定实现行为的依赖,以及对未初始化内存指针的处理,使得即使是单线程程序的推理也变得非平凡。

标准C内存模型的一个广受欢迎但此前未被形式化的扩展是将指针作为整数值进行无限制操作。尽管语言定义提供了一个整数类型uintptr_t,可以合法地在指针类型之间进行类型转换,但它对所得的整数值[5,§7.20.1.4p1]并未作任何要求。然而,在底层代码中,操纵指针表示形式有许多重要的应用场景。

例如,将指针类型转换为整数在Linux内核和JVM实现中被广泛用于对指针的位操作。另一种常见用法出现在C++标准库( std::hash)中,其中指针的位表示被用作哈希表索引的键。这种方法很有用,因为获取指针是一种低成本且能获得唯一键的方式。

支持位级别指针操作最直接的方法是采用通常所说的具体内存模型。这种方法最接近机器实际执行的操作:指针与适当宽度的整数值具有相同的表示形式,并且它们只是索引到一个表示内存的单一平面数组中。

然而,有限内存与允许将任意整数进行指针类型转换的结合,使得许多基本的编译器优化变得无效。例如,考虑一个函数f,它初始化一个局部变量a,然后调用某个未知的外部函数g。我们可能期望编译器能够推断出a的值不会受到对g调用的影响,并执行常量传播:

extern void g() ; 
int f(void){ 
    int a= 0; 
    g() ; 
    return a; 
}
→
int f(void){ 
    int a= 0; 
    g() ; 
    return 0; 
}
→
int f(void){
    g() ; 
    return 0; 
}

允许出于个人或课堂教学目的制作本作品的数字版或印刷版副本,但前提是不得为盈利或商业利益制作或分发这些副本,且所有副本必须在首页注明本声明及完整引用信息。对于本作品中由非ACM实体拥有的组成部分,其版权必须得到尊重。允许注明出处的摘要性使用。如需以其他方式复制、重新发布、上传至服务器或分发至列表,则需事先获得特定许可和/或支付费用。请向 Permissions@acm.org申请许可。版权由版权所有者/作者持有。出版权利授权 给ACM。PLDI’15,2015年6月13日至17日,美国俄勒冈州波特兰市 ACM。978‐1‐4503‐3468‐6/15/06 http://dx.doi.org/10.1145/2737924.2738005

然而,使用一个简单的具体内存模型时,我们必须考虑 g 可能“猜测”到 a 在内存中的位置并修改其值的可能性。如果我们 的语义以确定性方式分配内存,并且 f 的调用者为 g 适当地设置了内存状态,则可能发生这种情况。

编译器可能会通过完全移除现在未使用的局部变量a来进行进一步优化程序。这种转换再次被具体模型所禁止,因为它可能会改变程序行为:由于分配的内存单元减少了一个,原本因耗尽内存而失败的对g的调用现在可能会成功。

为了实现此类编译器优化,大多数经过验证的编译工作转而依赖于逻辑的 内存模型。这些模型将指针表示为分配块标识符和该块内偏移量的组合,其中有效分配块标识符的集合通常是无限的。在上述示例中,第一项优化是允许的,因为无法从其他块的逻辑地址伪造出变量 a 的逻辑地址。此外,第二项优化也是允许的,因为内存是无限大的。

逻辑模型允许大多数编译器优化,但无法支持许多使用指针与整数之间类型转换的底层C编程习惯用法,会将包含这些习惯用法的程序视为未定义(i.e.,错误的)。

在本文中,我们提出了一种用于C/C++的准具体内存模型,该模型结合了上述方法的优点。它为操作指针位级表示的程序赋予语义,同时允许对不使用这些低级特性的代码进行与逻辑模型相同的优化。关键的是,我们在不显著增加验证编译器所需证明技术复杂性的同时,保持了一个程序员易于理解的简单模型。

实现这一功能的关键技术要素是采用两种不同的指针值表示形式:具体的和逻辑的,并提供两者之间的转换过程。默认情况下,指针以逻辑形式表示,只有在将其类型转换为整数类型时,逻辑指针值才会被realized为具体的32位(或64位)整数值。当一个整数被类型转换回指针值时,它将被映射到相应的逻辑地址。

准具体模型对逻辑模型进行了保守扩展。它为比逻辑模型所支持的更多的程序赋予了语义,而不会改变那些在逻辑模型下已有语义的程序的语义。因此,在逻辑模型中对程序的任何正确推理在准具体模型中仍然成立,但准具体模型还支持如具体模型中的指针算术运算的推理。

最后,准具体模型并非旨在取代C标准中的内存模型。与具体模型和逻辑模型一样,它是(非正式的)C标准的一种形式化精化,可用于对程序和程序变换进行形式化推理(如在编译器验证中)。

我们的贡献是: 首个形式化内存模型,该模型完全支持整数‐指针类型转换, 同时允许标准编译器优化。
•一种在我们的内存模型下证明程序等价性的技术,及其应用于验证若干标准优化方法,这些优化方法在存在整数‐指针类型转换时难以验证。

本文报告的所有证明均已完全在Coq中形式化,并可在以下项目网页中找到。
http://sf.snu.ac.kr/intptrcast

2. 技术背景

在本节中,我们介绍一种最小化的类C语言编程语言,并回顾具体内存模型(§2.1)以及CompCert的逻辑模型(§2.2), 因为我们的准具体模型基于它们构建。随后,我们回顾行为精化(§2.3),这是用于证明编译器优化正确的关键定义。

为了简化表述,本文重点讨论整数‐指针转换的处理,而不涉及C内存模型的许多正交方面。具体来说:
我们假设一个32位架构:字长为4字节,地址空间的大小等于232。
我们仅考虑整数和指针值,而省略浮点数或字符等其他类型的值。
我们还省略了子字运算,并假设每个地址存储一个32位值。
•我们不考虑并发。

除了并发之外,我们所做的其他简化都很容易被取消。

为了具体起见,我们考虑以下简单的类C语言:

Typ ::= 整数 | 指针
Bop ::= + | 减 | 乘 | 逻辑与 | =
Exp ::= Int | Var | Global | Exp Bop Exp
RExp ::= Exp | malloc(Exp) | free(Exp) | (Typ) Exp | 输入() | 输出(Exp)
Instr ::= Fid(Exp, ..., Exp); | Var= RExp | Var=* Exp | *Exp= Exp | if (Exp) Instr else Instr | while (Exp) Instr
Decl ::= Fid(Typ Var, ..., Typ Var){var Typ 变量; Instr}

输入和输出操作会产生外部可见的事件;所有其他操作旨在对C语言中的相应操作进行建模。 T表示一个 T列表。为简化起见,我们省略了返回指令,而是通过函数的指针值参数返回值。

2.1 具体模型

具体内存由一个大小为 2^32的值数组和一个已分配块列表组成,其中已分配块表示为(p, n)形式的对,包含块的起始地址及其大小。从未分配地址加载或向未分配地址存储会引发错误(即,未定义行为)。由于在具体模型中指针仅仅是整数,因此值仅为32位整数。结果是,具体模型原生支持整数-指针类型转换。

Mem def =(int32→ Val)× list Alloc 
Alloc def ={(p, n) | p ∈ int32 ∧ n ∈ int32}
Val def ={i ∈ int32}

内存分配将一个块插入到已分配块列表中,而释放内存则将其移除。总体而言,已分配块列表应保持一致:
• If(p, n) is allocated, then ∅ 6=[p, p+ n) ⊆(0, 2^32 − 1).
如果块 (p1 , n1) 和 (p2 , n2) 是不同分配,则它们的范围 [p1 , p1 + n1) 和 [p2 , p2 + n2) 是不相交的。

然而,正如我们所见,具体模型不支持标准的编译器优化,例如在存在外部函数调用的情况下进行常量传播和死分配消除。

这是因为该模型未提供一种机制来确保某个模块对部分内存具有独占控制,从而假设未知代码可以读取和更新每个已分配内存单元的内容。

2.2 CompCert的逻辑模型

在CompCert的逻辑模型中,内存由一组有限的具有唯一块标识符的逻辑块组成。每个块都是一个固定大小的值数组,并带有一个有效性标志,用于指示该块是可访问的还是已被释放。与之前一样,访问已释放块会引发错误。值要么是32位整数,要么是逻辑地址。此处,逻辑地址(l, i)由块标识符 l和块内的偏移量 i组成。

Mem d=ef BlockID⇀fin Block
Block d=ef{(v, n, c) | v ∈ bool ∧ n ∈ N ∧ c ∈ Valn}
Val d=ef{i ∈ int32}]∪{(l, i) ∈ BlockID × int32}

逻辑模型相较于具体模型的一个重要优势是,只要函数不允许其地址逃逸,就可以对逻辑块拥有独占控制。原因在于无法构造出一个已分配块的逻辑地址。该属性保证了许多有用优化的正确性,例如跨函数调用的常量传播和死分配消除。

另一个优点是程序具有无限内存,使其分配行为无法被观察到,从而使得编译器易于移除死代码分配。

除此之外,逻辑模型具有稍显复杂的语义,其主要缺点是对整数到指针转换的支持非常有限。因此,CompCert 对整数‐指针转换的支持非常弱。通常,这些转换被视为空操作(即恒等函数),而非未定义行为(即错误),因此整数(或指针)类型的变量可以同时包含整数和逻辑地址。在 CompCert 的高级语言(CompCert C 和 Clight)中,一旦指针被转换为整数,对该整数进行的任何算术运算都会返回一个未指定的值。然而,在其低级语言(Cminor、RTL、等等)中,某些整数操作(即加法、减法和相等性测试)在特殊情况下也对指针值有明确定义。更具体地说,例如,将整数与地址相加、在同一逻辑块内的地址之间进行减法,以及地址与空值之间的相等性测试都是有明确定义的。

2.3 行为精化

编译器正确性的标准概念是行为精化,即 目标程序行为的集合必须是源程序行为集合的子集。我们考虑的是行为的集合而非单一行为,因为通常情况下,由于非确定性,一个程序可能具有多种行为。

给定程序可能生成的一组 I/O 事件,行为有以下三种形式之一:
1. 产生有限序列 I/O 事件 e1 , · · ·, en , term 的终止执行。
2. 仅产生有限序列 I/O 事件 e1 , · · ·, en , nonterm 的发散执行。
3. 产生无限序列 I/O 事件 e1 , · · ·, en , · · · 的发散执行。

我们将 C11 标准中描述的未定义行为视为所有行为的集合。这捕捉到了直观的属性‐编译器在未定义行为方面的性质:如果目标程序的行为是未定义的,则源程序的行为也是未定义的。如果源程序的行为是未定义的,则编译器可以选择任意程序作为其结果。

那么内存耗尽的行为集合是什么?2直观上,即使源程序没有内存耗尽,我们也应允许目标程序内存耗尽,因为诸如寄存器分配等重要的编译器变换可能会增加程序的内存需求。对称地,由于内存耗尽对于程序员来说并不像真正的未定义行为(e.g.,访问已释放的内存)那样严重,因此当源程序出现内存耗尽时,目标程序的行为不应是任意的,而应与源程序行为一致,同样表现为内存耗尽。

因此,CompCertTSO [12]将内存耗尽建模为行为的空集,即无任何行为。然而,如果在发生内存不足错误之前已有 I/O 事件发生,情况会如何?在内存耗尽之前丢弃 I/O 事件是荒谬的:目标程序应始终执行源程序可能执行事件的一个前缀。

为了处理此问题,CompCertTSO 还会观察部分行为,可能在因内存耗尽而丢弃行为之前。
4. 一个已生成有限序列 I/O 事件 e1, · · ·, en, partial 的程序的部分执行。

与之前一样,精化被定义为目标程序的(可能部分的)行为集合包含于源程序的行为集合中。

在本文中,我们采用CompCertTSO处理内存耗尽的方法。

然而,与CompCertTSO不同的是,CompCertTSO中只有目标语言可能出现内存耗尽,而我们的源语言也可能由于指针到整数的转换而导致内存耗尽,这一点我们将在下文解释。

3. 准具体模型

我们的准具体模型仅仅是完全具体模型和完全逻辑模型的结合体。然而,在如何结合这两种模型以最小化其缺点方面存在若干设计问题。在本节中,我们介绍准具体模型,并从较高层次讨论如何解决这些设计问题。更详细的讨论及具体示例将在后续章节中展开。本节中展示的所有优化示例均由 clang ‐O2 执行。

3.1 内存表示

我们的准具体模型对逻辑模型进行了轻微的推广,以同时允许存在具体块(如具体模型中)和逻辑块(如逻辑模型中)。为此,我们为逻辑块增加一个属性 p,其值要么是 undefined,要么是一个具体地址。该属性 p用于指示该块是逻辑块(当 p未定义时),还是从地址 p开始的具体块(当p有定义时)。

Block def ={(v, p, n, c) | p ∈ int32]∪{undef} ∧ v ∈ bool ∧ n ∈ N ∧ c ∈ Val n }

我们说,当块 l 是一个从地址 p开始的具体块时,地址 (l, i) 是具体的。在这种情况下,地址 (l, i) 可以类型转换为整数 p+ i,反之亦然(详见 §4)。

与具体模型一样,具有具体地址的已分配(即有效)块列表必须一致:它们不应包含0或最大地址,并且它们的范围应互不重叠。逻辑块没有此要求,因为它们在构造上就是不重叠的。

在后续章节中,我们将讨论在设计准具体模型时遇到的若干问题,并证明我们针对这些问题所提出解决方案的正确性。

3.2 结合逻辑的和具体的块

我们的准具体模型是一种混合模型,允许具体的块和逻辑的块共存。尽管我们允许两者并存,但具体的块和逻辑的块仍然各有缺点:具体的块不提供独占所有权,而逻辑的块不允许转换为整数。

因此,人们很自然地会问,为什么我们不创建一种兼具具体块和逻辑块优点的新块概念。我们的回答是,这样的模型无法证明其他重要优化(如整数操作的简化)的正确性,而我们的准具体模型可以证明这些优化的正确性。

例如,考虑一个模型,其中某些块具有具体地址以及一些额外的权限信息,从而可以判断某个块是否被独占拥有。在这种模型中,我们希望知道即使对指针进行整数操作(例如,对指针进行base64编码然后再进行base64解码),在指针被转换为整数时也不会丢失权限信息。

然而,这会阻止图1中所示的优化。假设变量b包含一个整数,该整数具有访问某个有效块 l的权限,而变量a包含一个没有权限但等于该块具体地址的整数 l。那么源程序能够成功将123存储到块 l中,因为q具有相关权限,而目标程序则失败,因为q没有该权限。参见 §6.1了解如何在我们的准具体模型中验证此优化。

此外,尽管我们的准具体模型确实禁止了一些基于独占所有权的优化(因为一旦一个块变为具体的,在执行的剩余部分中它就不再是独占的),但我们预计在实际中该模型不会失去太多优化机会。这是因为独占拥有的块大多是局部或临时的,因此它们的具体地址不太可能被整数操作使用。详见§3.7。

3.3 选择具体的块

如 §2.1节所述,使用具体地址表示内存位置无法保证所有权,从而阻碍了某些优化。在最坏情况下,一个函数可能猜出另一个函数的私有部分资源的具体地址,然后伪造指向该地址的指针并进行修改。为了最大限度地扩展可执行的优化范围,混合模型应尽可能少地为块分配具体地址。

因此,自然的选择是仅将那些其具体地址在某些操作中真正被使用的块设为具体的。如果我们对一个指针的值执行某种计算,而该计算只有在该值为整数时才有意义(例如将其与一个整数值进行比较),那么该指针的目标必须具有一个真实的地址。在所有其他情况下,即使获取了块的地址,

foo(int a) {
    a=a & 123; // return a;
    ·}a · ·=(int) p; 
    foo(a);
    bar();
→
    foo(int a) {
        a=a 等于 123;
        // return a;
        ·}a · ·=(int) p;
        bar();
p = malloc (1);
*p= 123;
bar();
a =*p;
hash_put(h, p, a); 
→
p = malloc (1);
*p= 123;
bar();
a =*p;
hash_put(h, p, 123);

我们或许可以使用逻辑值并保持逻辑模型的所有权保证。然而,这种方法存在一个严重的问题:它无法证明某些重要整数优化的正确性,例如图2中展示的死代码消除。

假设指针p包含一个逻辑块 l。在源程序中,由于其具体地址在函数foo中被使用,因此必须为块 l分配一个具体地址。而在目标程序中,对foo的只读调用被优化掉,块 l可能不会被分配具体地址。也就是说,源程序可能比目标程序拥有更多的具体块。因此,如果 bar()访问任意的具体内存位置,则该访问在源程序中可能成功,但在目标程序中可能失败。由于目标程序中出现了源程序中不存在的失败可能性,该优化引入了新的行为,因此是无效的。

相反,我们将那些地址被强制转换为整数的块设为具体的,即使这些转换后的整数并未在任何操作中使用。这为我们提供了一种简单的方法来确定哪些块应被设为具体,并避免使整数操作与内存相关(参见 §6.2以了解如何在准具体模型中验证此示例)。在实践中,这种选择还允许执行最小具体模型中可能进行的大多数优化(详见 §3.7)。

3.4 分配具体地址

一旦我们确定了哪些块需要具体地址,就需要决定在程序执行期间何时分配这些地址。

一种方法是尽早做出此类决策(即在分配时间)。我们将块分配为逻辑块或具体块,并使得在逻辑块上的具体操作(即整数类型转换)引发内存类型错误行为(即无行为)。

由于很难确定一个块是否需要具体地址,因此我们需要非确定性地选择要分配的块类型。然而,这将给我们的模型引入反直觉的失败,实际上当分配器选择了错误类型的块时,即使存在具体块,也会导致内存类型错误行为。

我们的解决方案是将所有块都作为逻辑块进行分配,并在转换时为逻辑块分配具体地址。只有当空闲的具体空间不足时,这种转换才可能导致内存耗尽。通过延迟到转换点再将块变为具体块,我们可以消除关于块是具体还是逻辑的非确定性。也就是说,块始终是逻辑的,直到第一个转换点,之后则变为具体的。

d1= a +(b 减 c1); 
d2= a +(b 减 c2);
→
t= a+ b;
d1=t 减 c1; 
d2=t 减 c2; 

这也允许所有权转移优化,例如图3中的常量传播示例,其中指针在某个点之前是私有拥有的,然后变为公开可用。在此示例中,已分配块最初是逻辑的,在转换为整数时(可能在调用 hash put 时)变为具体的。此时,该块的所有权从私有转移到公共。由于在调用 hash put 之前,a 被视为逻辑的,因此我们可以在调用之前正常执行常量传播。(形式化细节请参见 §6.3。)

另一方面,上述具有非确定性分配的模型不允许此类优化。

该优化在上述模型中不被允许的原因如下:当目标中的已分配块是具体的时,源程序中的相应分配必须是具体的;否则,当函数 hash put 将块的地址类型转换为整数时,源程序不会产生任何行为,而目标程序则会成功。因此,您将失去对该块的所有权,并且由于 bar() 的存在而无法证明常量传播的正确性。

然而,请注意,这可能不是一个决定性的例子,因为这种优化并未在所有实际使用的编译器中应用(clang ‐O2 及更高版本会执行此优化,但 gcc ‐ O2 及更高版本不会)。

3.5 指针上的操作

在 §3.3节中,我们解释了将越少的块变为具体化,就越能充分利用逻辑块所提供的所有权保证。我们已经看到,那些要求指针具有整数值的操作会强制规定必须具体化的块数量的下限。因此,我们可以通过减少需要具体地址的操作数量来改进我们的模型。

我们借鉴了CompCert内存模型的做法,在该模型中,一些算术操作(例如整数‐指针加法,以及同一块内指针与指针之间的减法)即使在没有具体地址的情况下也是良定义的(参见 §2.2节)。

CompCert在处理算术运算时的一个缺点是,它通过引入逻辑地址作为整数类型变量的可能值,使得一些重要的算术优化(例如图4中所示的优化)变得无效。为了说明这一点,假设整型变量a、b、c1和c2都包含相同的逻辑地址(l,0)。所示的源程序会成功地将(l,0)赋值给变量d1和d2,因为b ‐ c1和 b ‐ c2等于0。然而,在目标程序中,变量t得到一个未指定的值,因为两个逻辑地址的加法是未定义的。因此,目标程序的行为多于源程序,该优化无效。

我们通过使用类型检查来避免这一缺点。与LLVM IR一样,我们使用类型来确保整型变量仅包含整数值。这使得我们能够证明对整型变量进行各种算术优化的正确性(详见 §6.4),同时在可能的情况下为逻辑地址上的操作赋予语义。参见 §4以了解本模型中算术操作的类型相关语义示例。

3.6 DeadCast 消除

由于我们到目前为止的设计决策,类型转换在我们的模型中已成为重要的有副作用的操作,决定了逻辑块被赋予具体地址的时机。这导致了死代码消除可能出现问题。由于将指针类型转换为整数在内存中具有副作用,因此删除

foo(ptr p, int n) {
    var ptr q, int a, r;
    q = malloc (n); 
    a =(int) p;
    r=a * 123; 
    // return r;
}
→
foo(ptr p, int n) {
    var ptr q, int a, r; 
    q=malloc (n);
    a =(int) p;
    r=a * 123; 
    // return r;
}
·f o· ·o(p, n);
bar();
→ ···
bar();

在准具体模型中,死代码转换操作并没有明显的合理性。

然而,事实上,我们可以解决这个问题,并在我们的框架中支持死类型转换消除。该解决方案源于我们的模型在一个更广泛编译框架中的位置。我们期望准具体模型用于编译器中的中层中间表示,而后端低级语言将使用完全具体模型。在准具体模型中,将指针转换为整数会对内存产生副作用,因此我们无法消除类型转换操作。然而,在完全具体模型中,从指针到整数的类型转换是一个无操作,此类类型转换总是可以被消除。因此,我们可以在后端执行死代码转换消除优化。

然而,当死代码类型转换与死代码分配结合时,我们仍然存在问题。在具体模型中,死代码块的分配不能被移除,因为否则源中的任意访问可能成功,而在目标中却会失败。

我们解决此问题的方法是在从准具体模型转换到完全具体模型的过程中,同时移除与死代码块结合的无用类型转换。例如,考虑图5中所示的死代码调用消除优化。当源和目标均使用准具体模型时,由于函数中的类型转换操作,该优化不成立;当源和目标均使用具体内存时,由于函数中的分配操作,该优化也不成立。然而,当源使用准具体模型而目标使用完全具体模型时,该优化是有效的(详见 §6.5中的形式化细节)。

尽管我们的解决方案无法证明移除所有无用类型转换的正确性,但在实践中应能涵盖大多数情况(参见 §3.7)。

3.7 准具体模型的缺点

尽管我们的准具体模型旨在允许多种可能的优化,但它仍然禁止了一些合理的优化。特别是,如果一个函数新分配了一个块并将其地址转换为整数,则该块的所有权保证将丢失。即使该块在实际上仍然是本地拥有的,一旦其地址被转换为整数,我们就无法再执行依赖于其局部性的优化,例如死代码消除或常量传播。

然而,我们认为在实际程序中,地址被转换为整数的块不太可能是完全局部的。除非该地址将与其他代码段共享,否则几乎没有理由将局部指针转换为整数。

例如,考虑以下简单的程序示例,我们可能希望在将指针类型转换为整数后仍执行局部性优化。该程序是 §3.6中示例的一个变体,其中我们将q转换为整数而不是p,因此在我们的模型中,局部块变为具体的,无法被消除。尽管此优化在我们的框架中无法实现,但函数foo不过是一个不可预测的数字生成器,不太可能出现在真实程序中。

foo(指针 p, 整数 n) {
    变量 指针 q, 整数 a, r; 
    q = malloc (n);
    a =(整数) q;
    r = a * 123;
    // return r;
}
→
foo(指针 p, 整数 n) {
    变量 指针 q, 整数 a, r; 
    q= malloc (n); 
    a=(整数) q;
    r= a * 123; 
    // return r;
}
· · ·
foo(起始地址, 大小);
bar();
→ ···
bar();

我们的模型另一个更为合理的限制出现在以下情况:某个局部块被私有地使用一段时间后,将其地址转换为整数并释放到公共部分(例如,将该整数用作哈希表的键)。考虑以下示例,它是 §3.4 中示例的一个变体,其中我们将新分配块的地址类型转换为整数,并将该整数用作哈希表的键:

· · ·
p= malloc(1); 
*p= 123; 
b=(int) p; 
bar(); 
a=*p; 
hash_put(h, b, a);
· · ·
→
· · ·
p= malloc(1); 
*p= 123; 
b=(int) p; 
bar(); 
a=*p; 
hash_put(h, b, 123);
· · ·

这种常量传播优化在准具体内存模型中是无效的,因为在调用 bar 之前,新分配的块被类型转换为整数。(不过,如果将类型转换移到对 bar 的调用之后,则它会变得有效。)然而,尽管 clang ‐O2 确实执行此类所有权转移优化,但 gcc ‐O2 或更高版本并不会执行,并且这类优化可被视为不太常用的次要优化。

4. 语言语义

本节描述如何使用准具体内存模型的思想来为 §2的类C语言赋予语义。

空指针 我们将空指针表示为逻辑地址 (0,0),并按如下方式初始化块 0:

m(0)=(v, p, n, c) with v= true, p= 0, n= 1.

对块0的唯一特殊处理是:我们(i)在通过加载或存储访问它时引发未定义行为;以及(ii)在释放它时不做任何操作(因为C语言中允许free(0))。

整数与指针之间的转换 我们首先通过具象化和有效性检查来定义整数与逻辑地址之间的类型转换。具象化函数 ↓m在内存 m下将逻辑地址转换为相应的整数,前提是该逻辑地址所在的内存块具有具体地址。有效性谓词validm用于检查一个逻辑地址是否位于有效块的范围内。

(l, i)↓m def = p+ i if m(l)=(v, p, n, c)∧ p is defined 
valid m(l, i) iff m(l)=(v, p, n, c)∧ v= true ∧(0 ≤ i< n)

将逻辑地址(l, i)类型转换为整数时,首先实现块 l,然后如果该地址有效,则具象化地址(l, i);否则引发未定义行为。

将整数 i 进行类型转换时,若存在对应的地址,则生成有效的逻辑地址(l, j),并具象化为 i;否则引发未定义行为。注意,若整数类型转换成功,则其结果对应唯一一个地址。

cast2int m(l, i) def =(l, i)↓m if valid m(l, i){after realizing l}
cast2 p tr m (i) def =(l, j) if valid m(l, j)∧(l, j)↓m = i

Computing with Logical Values 我们现在根据操作数的静态类型来定义二元操作的语义。当两个操作数均为 int 类型时,我们执行普通的整数加法、减法等操作。当一个或多个参数的类型为 ptr 时,我们对这些操作在有明确定义的情况下赋予特殊语义,否则引发未定义行为:

(p+ a, m) ⇓(l, i1+ i2) if p=(l, i1)∧ a= i2
(a+ p, m) ⇓(l, i1+ i2) if a= i1 ∧ p=(l, i2)
(p- a, m) ⇓(l, i1 − i2) if p=(l, i1)∧ a= i2 
(p1- p2, m) ⇓ i1 − i2 if p1=(l, i1)∧ p2=(l, i2)
(p1= p2, m) ⇓ i1= i2 if p1=(l, i1)∧ p2=(l, i2)
(p1= p2, m) ⇓ false if p1=(l1, i1)∧ p2=(l2, i2)∧ l16= l2 ∧ validm(l1, i1)∧ validm(l2, i2)

这种相等性的定义是对ISO C标准中给出的指针相等性的精化;例如,即使p不是指向已分配块的指针,它也允许我们得出 p = p的结论,而在C标准中,此类比较的结果是未定义的。

准具体和具体语义 利用这些定义,我们可以为语言结构提供通常的操作语义定义,并在准具体内存模型中执行内存操作(加载、存储、分配和类型转换)。静态类型检查使我们能够将变量划分为指针类型变量(其值始终为逻辑地址,并按上述方式处理)和整数类型变量(其值始终为普通整数,无需特殊处理)。

我们还为该语言赋予了纯粹的具体语义,并将其作为具有具体内存的低级中间语言使用。在此语义中,所有内存块都被实现,所有值都仅为整数,解释为整数值或内存单元的物理地址。

5. 推理原则

在本节中,我们通过一个运行示例对推理原则进行高层次的概述。

5.1 运行示例与非形式化验证

考虑以下示例变换,该变换确实由“clang ‐O2”执行。此变换涉及四种不同的优化:常量传播(CP)、死加载消除( DLE)、死存储消除(DSE)和死分配消除(DAE)。

foo(ptr p){ 
    var ptr q, int a; 
    1: q= malloc(1); 
    2: *q= 123; 
    3: bar(p); 
    4: a=*q; 
    5: *p= a; 
}
→
foo(ptr p){
    // DAE 
    // DSE 
    bar(p); 
    // DLE 
    *p= 123;// CP 
}

我们将论证,在 foo 的两个版本(源和目标)中的每一行,所执行的指令(如果有的话)的效果是等价的。为了做到这一点,我们将假设源程序和目标程序的内存之间存在初始关系,并证明这种关系的某种变体在整个函数中持续存在,同时依赖于对其他函数的调用以维持类似的关系。

这种关系将指定每块内存中的一个公共部分,并要求源和目标的公有内存中相关位置具有等价值;同时也会指定每块内存中的一个私有部分,使得源程序可以在目标程序未进行相应更改时修改其私有内存,反之亦然。有关此关系的技术细节以及我们对等价性的概念,请参见 §5.2。

我们从第1行开始,假设源程序和目标程序的内存满足以下条件 (见图6 (a)):
- assume(等价参数) 参数p 在源和目标中分别包含 等价的 参数 vsrc 和 vtgt;
- assume(等价公共内存) 在源中存在一组内存块 mpub:src,在目标中存在 mpub:tgt,它们是等价的且可被任意函数公共访问;
- assume(源私有内存) 在源中存在一组不相交的块mprv:src,每个块均由单个函数独占拥有;
- assume(目标私有内存) 目标中存在一组不相交的块mprv:tgt,其中每个块均由单个函数独占拥有。

执行第1行后,我们将新分配的块(称其为 l)添加到源内存 mprv:src的私有部分。需要注意的是,我们可以将块 l添加到源内存的私有部分,因为这是一个新的逻辑块,因此由foo独占拥有。

执行第2行后,该块 l包含123(见图6 (b))。

在第3行,我们保证对函数bar的调用是equiv-alent asfollows:
- 保证(等价参数) 传递给 bar 的参数 vsrc 和 vtgt 是等价的;
- guarantee(等价公共内存) mpub:src和 mpub:tgt,它们是等价的且公开可访问的;
- 保证(源私有内存) mprv:src][l 7→ 123] 中的每个位置均由单个函数独占拥有;
- 保证(目标私有内存) mprv:tgt中的每个位置均由单个函数独占拥有。

当对 bar 的调用返回时,我们可以假设新的公共内存彼此是等价的(尽管它们可能与之前的公共内存不同),而私有内存保持不变(见图 6 (c)):
- 假设 (等价公共内存)我们有新的公共内存 m ′ pub:src和 m ′ pub:tgt,它们由 mpub:src和mpub:tgt演化 而来(内存演化的定义见 §5.3),并且是等价且公开可访问的;
- 假设 (源私有内存) m p rv:src][l 7→ 123]保持不变;
- 假设 (目标私有内存) m p rv:t g t保持不变。

在第 4 行,我们从源的私有内存加载值 123 并将其存储到变量 a 中。在第 5 行,在源中,我们将变量 a 的值(即 123)存储到地址为 vsrc 的内存单元中。在目标中,我们将常量 123 存储到地址为 vt g t 的单元中。由于我们在等价位置vsrc 和 vt g t 存储了等价值,因此将得到等价的公共内存 m ′′ pub:src 和m ′′ pub:t g t ,同时保持私有内存不变。

最后,我们回到 foo 的调用者(见图 6 (d))

保证(等价公共内存) m′′ pub:src和 m′′ pub:tgt,它们由 mpub:src和 mpub:tgt演变而来,并且是等价的且公开可访问的;
保证 (源私有内存) mprv:src;
保证 (目标私有内存) mprv:tgt, 其中我们可以忽略块 l,因为它将不再被使用。注意,我们在此保证 foo 返回时具有与其初始给定相同的私有内存,正如我们之前假设函数 bar 在第3行之后具有相同性质一样。

5.2 内存不变量

如上文非正式讨论所述,我们通过使用内存不变式来证明程序精化,该不变式对公有内存(在源程序和目标程序中必须等价)和私有内存(在两者之间可以不同)施加条件。现在,我们正式定义在非正式示例中使用的内存等价概念以及对私有内存的条件。

内存等价性的思想是对CompCert内存注入[10]的一种简化。我们对具体块的条件借鉴了CompCertTSO对有限内存的支持[12]。更多比较见 §7。

内存等价 我们定义了一种比简单相等性更宽松的等价概念,因为在存在(不相关的)私有内存时,简单相等性会过于严格。我们说源中的块集合msrc与目标中的块集合 mtgt是等价的,当它们满足以下条件时成立。首先,在 msrc中的块标识符与mtgt中的块标识符之间应存在一个双射,记为 α。其次,对应块(即由 α关联的块)应具有相同大小和有效性,并且在每个偏移量处所存储的值应当是等价的。当两个值(关于 α)要么是相同的整数,要么是在对应块(关于 α)中处于相同偏移量的逻辑地址时,它们是等价的。当 msrc和 mtgt在此意义上等价时,我们记作 msrc’α mtgt。

对应块的具体地址上的条件值得进一步解释。关于两个对应块是否为具体或逻辑块,我们有四种可能的情况(参见 图7的公共 部分)。图中的第一种情况(即,源:逻辑,目标:逻辑)显然应被允许。第二种情况(即,源:具体,目标: 具体)也应被允许,但仅当具体地址一致时。

第三种情况(即,源:具体,目标: 逻辑)不应被允许。如果允许这种情况,就相当于允许源内存包含比目标更多的具体块,这会导致两个问题:(i) 任意具体内存访问可能在源中成功但在目标中失败;以及 (ii) 指针到整数的转换可能在源中引发内存不足,但在目标中成功。在这两种情况下,目标可能具有比源更多的行为,这是不允许的。另一方面,最后一种情况(即,源:逻辑,目标:具体)是被允许的,因为情况恰恰相反:源可能具有比目标更多的行为,而这是允许的。

私有内存 对于私有内存中的块,我们有四种可能的情况,取决于该块是在源中还是在目标中,以及该块是具体的还是逻辑的(参 见私有 部分的图7)。除源私有外,所有情况都是允许的

源内存不应包含比目标内存更多的具体块,因此在内存等价性中不允许源为具体的块而目标为逻辑的块;同理,内存块也必须是具体的。

内存不变量 一个内存不变式 β 包括:(i)它们块标识符之间的一个双射 α,(ii)源的私有内存 mprv:src,以及(iii)目标的私有内存 mprv:tgt。当一对内存 msrc 和 mtgt 包含私有部分 mprv:src 和 mprv:tgt以及某些公共部分 mpub:src 和 mpub:tgt 时,不变式 β=(α, mprv:src, mprv:tgt)在这对内存上成立。

(msrc ⊇ mpub:src] mprv:src) ∧(mtgt ⊇ mpub:tgt] mprv:tgt) ∧ mpub:src'α mpub:tgt

其中]和 ⊆分别为不相交并集和子集关系 .

5.3 证明模拟

我们现在可以正式提出我们的推理原则。我们的基本方法是按照[4]的风格,通过局部模拟来验证程序。

源和目标中的一个函数,例如 foo,在满足以下条件时被认为是局部模拟的。首先,考虑源函数和目标函数的典型生命周期:

foo(..){ 
    foo(..){// βs
    ... 
    ... // βc 
    βsvβc
    bar(..); 
    bar(..);// βr 
    βcvβr ∧ βc=prv βr
    ... 
    → 
    ... // β′ c 
    βrvβ′ c
    gee(..); 
    gee(..);// β′ r 
    β′ cvβ′ r ∧ β′ c=prv β ′ r
    ... 
    ... // βe 
    β′ rvβe ∧ βs=prv βe
} 
}

此处假设加框的条件,其余条件则被保证。

首先,在 foo 中,未知函数调用(如 bar(..) 和 gee(..))应当同步(即当目标调用 bar 时,源也应调用 bar)。注意,当调用一个已知函数时,验证器可以选择进入被调用的函数并对其代码进行推理,或者将其视为未知函数调用。

接下来,在函数 foo 的入口点,我们假定给定的内存满足某个不变式 βs,并且参数关于 βs中的双射是等价的。然后,我们在源和目标中执行 foo 的代码,直到第一次对未知函数 bar(..) 的调用。在此处,我们必须证明当前内存满足某个不变式 βc,并且函数 bar 的参数关于 βc中的双射是等价的。

这里,我们还必须通过证明当前不变式 βc是初始不变式 βs(记为 βs v βc)的未来不变量,来表明当前内存是从初始内存逐步evolved而来的。当某个条件满足以下情况时,我们称 βc是 βs的未来不变量,这些条件排除了那些无法由语言的操作语义引起的内存变化。首先, βc中的双射应包含βs的双射,因为在执行过程中逻辑块不能被删除(当一个块被释放时,它会变为无效而非被移除)。其次,关于 β s和 β c中公有内存的其他条件是:(i) 块的大小在 β s和 β c之间不发生变化,(ii) 在 β s中无效的块在 β c中不能变为有效,以及(iii) 在 β s中的具体块在 β c中不能变为逻辑块。然而需要注意的是,由于操作语义允许更新内存中的值,因此公有内存的内容在 β s和 β c之间是可以改变的。

然后,我们考虑未知函数成功返回的情况。我们可以假设返回时的内存也满足某个f uture不变式 βr 。我们还可以假设函数 bar 不会改变 βc 中的私有内存(表示为βc=prv βr),因为在我们的准具体模型中,bar 无法访问它们。

我们继续遍历该函数,在非调用步骤中演进我们的不变式,并在其他调用点(如 gee(..))执行类似的推理。最后,当 foo 返回给其调用者时,我们必须证明在当前内存上存在某个f uture不变式 βe成立。此外,我们必须证明我们没有修改初始不变式 βs 中给出的私有内存(即 βs=prv βe)。此条件是必要的,因为如 上所述,我们假设该性质在任何其他函数调用结束时均成立。通过这种方式,我们为 foo 构造了一个局部模拟证明。

6. 验证示例

在本节中,我们展示如何验证 §3 中所示的示例。此处的所有结果均已 在 Coq 中完全形式化。

6.1 算术优化 I

考虑图1中的变换。如果我们假设整型变量仅包含整数值,而不是包含逻辑地址,那么指令 a =(a ‐ b)+(2 * b ‐ b) 对变量 a 的值没有影响,等价于无操作,因此该优化显然是正确的。

我们如何知道整型变量仅包含整数值?直接的回答是,我们的语言是静态类型检查的,如同 LLVM IR 那样。然而,之所以能够实现这一点的关键原因在于,在准具体模型中,当逻辑地址被类型转换为 int 时,我们会将其转换为整数,而不是将逻辑地址直接放入整型变量中。此外,当我们从内存加载一个值到整型变量(或指针变量)时,如果加载的值是逻辑地址(或整数值),则会引发未定义行为(即,错误)。换句话说,准具体模型在使用它的语言中引入了一种形式的动态类型检查。这使得我们能够验证此类示例中的整数算术运算优化。

6.2 死代码消除

考虑图2中的变换。此示例与前一个类似。由于我们可以假设整数类型变量仅包含整数,因此调用 foo(a) 的执行不会产生任何副作用。此外,由于我们知道函数foo的代码,因此无需将其视为未知的函数调用。相反,我们只需进入函数foo的代码并在源中执行它。

6.3 所有权转移

考虑图3中的变换。此示例类似于 §5.1中的运行示例。

假设在 malloc 之前,下面的第一个不变式成立。在源中分配块 ls 并在目标中分配块 lt,并在两个块中存储 123 后,由于块 ls 和 lt 是逻辑的且与公共部分不相交,我们可以将它们移入不变式的私有部分,从而得到下面的第二个不变式。接下来我们调用函数 bar。当它返回时,我们可以假设第三个不变式成立 (即 私有部分未被改动)。加载后,变量 a 将包含 123,因为 p 在源中包含逻辑地址 (ls ,0),在目标中包含逻辑地址 (lt , 0)。

接下来,当我们调用哈希放入时,必须确保参数是等价的。第一个参数是等价的,因为我们假设变量中的初始值是等价的;第三个参数是等价的,因为a包含123。为了证明第二个参数(ls , 0)和(lt , 0)是等价的,我们将块从私有部分移动到公有部分,并扩展双射 α ′以关联 ls 和 lt (如下第四个不变式)。这样的允许从私有部分向公有部分进行所有权转移,因为未来不变关系 (v)仅要求双射是非递减的,而不要求私有部分是非递减的。

6.4 算术优化II

我们可以很容易地验证图4中的变换,原因与 §6.1中相同:因为我们可假设所有整型变量都包含整数值。

6.5 死类型转换消除

考虑图5中的变换,在源使用准具体模型而目标使用具体模型的情况下。

我们首先假设在调用 foo 之前,以下第一个不变式成立,其中变量p在源中包含等价地址(ls, i),在目标中包含(lt, i)↓m。注意,块 lt是具体的,因为目标使用的是完全具体模型。在源中的 foo 函数内分配一个块(例如 l′s)后,我们将该块移至源的私有内存,从而得到下面的第二个不变式。这里需要重点指出的是,如果源使用的是具体模型,则无法将块 l′s移入私有部分,因为 l′s将是具体的,这会使我们的证明无效。

类型转换后,块 ls变为具体的,得到下面的第三个不变式。这里需要注意的是,如果目标语言使用的是准具体模型且 lt 是逻辑的,则我们会生成一个 ls 为具体的而 lt 为逻辑的不变式,这将是一个无效的不变式。在 foo 返回后,我们只需从源的私有部分中移除块 l′s,因为我们不会使用它,从而得到下面的第四个不变式。然后我们就可以继续验证其余代码了。

6.6 恒等编译器

作为对我们推理原则的合理性检查,我们编写了一个从采用准具体模型的语言到其自身的恒等编译器,以及一个从采用准具体模型的语言到采用具体模型的同种语言的简单编译器。后一个编译器只是消除了形如 =(int) p 的无用类型转换。我们已使用 Coq 中的推理原则成功验证了这两个编译器。

7. 讨论和相关工作

准具体模型通过为涉及指针操作的更多程序赋予语义,进一步细化了C标准。我们打算将此模型用于编译器验证任务,以扩展可验证的常见优化的范围。最终,我们希望构建一个支持C语言所有常用特性的已验证的LLVM翻译验证框架。我们还希望将我们的模型与CompCert集成,并用它来证明新的CompCert优化的正确性。我们相信,我们的思想可以很容易地应用于 CompCert(TSO)以及Vellvm [15, 16]等关联项目,因为我们的内存模型和内存不变性的概念在技术上与CompCert非常接近 (Vellvm也使用了CompCert的内存模型)。本质上,在证明中需要修改的仅是指针到整数类型转换的相关情况。

C语言的其他特性 还有许多其他的C语言特性与内存模型有某种程度的交互。其中一些特性,例如不确定值 [5, §3.19.2p1],悬空 指针 [5, §6.2.4p2],和无副作用的无限循环 [5, §6.8.5p6],,其语义在很大程度上与我们在准具体模型中使用的指针实现正交。类似地,我们的模型明确允许不安全派生指针,这在C11中是被允许的,并在C++11[5, §3.7.4p4]中定义为实现定义。我们允许这些指针是为了支持底层编程习语,例如异或链表和HotSpot JVM中的压缩普通对象指针。

本文没有直接讨论线程,因此我们不能确定该模型可以 扩展到处理线程。然而,在此方向上未发现障碍,且准 具体模型与支持弱内存模型和线程的CompCertTSO类似, 因此我们乐观认为语义的这一扩展应能类似地实现。

一些语言特性需要对我们内存模型进行某些调整。例如,我们可以采用 Krebbers 的技术 [6], 来调整准具体模型以支持 联合类型 和 严格别名,该技术无论模型是具体的、逻辑的还是混合的均适用。

作为另一个例子,在C语言中,char* 是一种“通用”指针类型,它允许通过 memcpy 实现高效的大块数据移动。Krebber的CompCert变体 [7]已经使用逻辑内存支持这种语义,而准具体模型与此解决方案兼容。简而言之:我们允许 char 类型存储字节索引的逻辑值(例如 (l, 10) : 2,表示逻辑地址 (l,10) 的第二个字节)。这种策略之所以可行,是因为在算术操作中使用时,char 会隐式地转换为整数,因此我们可以简单地将这些类型转换视为具有副作用的操作(即,实现逻辑地址)。这种方法几乎不会丢失任何优化机会,因为字节索引的逻辑地址通常是从内存中加载的,因此编译器通常已经将其视为公共部分。

别名分析 准具体模型与常见的别名分析基本兼容。例如,它可以用 来证明基于大小的别名分析 的正确性,该分析将指向不同大小对象的指针视为不同的指针。例如,在下面的代码中,p 和 q 之间不存在别名:即使 q 指向 p所指向的块,由于该块的大小不足以容纳 double 值,因此在该块中加载或存储 double 值将会失败。

整数* p=malloc(sizeof(int));
double* q = foo (p); // p 和 q 之间无别名

它还证明了基于新鲜度的别名分析的正确性,该分析假设 malloc的结果与所有其他指针都不同。在准具体模型中,以下常量传播的例子是有效的,因为q指向一个与p所指向的块不同的新鲜块。需要注意的是,即使在新鲜块被实例化之后, p和q之间也不存在别名关系。原因是即使p和q可以类型转换为相同的整数,它们作为指针值仍然指向不同的块。

foo(ptr p){ 
    var ptr q, int a,b,r; 
    q= malloc(1); 
    a=(int) q ; 
    b=* p ; 
    * q = 123; 
    r=* p ; 
}
→
foo(ptr p){ 
    var ptr q, int a,b,r; 
    q= malloc(1); 
    a=(int) q ; 
    b=* p ; 
    * q = 123; 
    r= b; // CP 
}

Coq Formalization 本文报告的所有证明均已完全在Coq中形式化, 并可在项目网页上找到。我们的Coq形式化工作约等于1万行代码,不包括空行和库代码。该形式化工作大约花费了两个人月完成。

优化示例 本文中展示的所有优化示例均由 Clang 3.4.2 和/或 GCC 4.8.3 执行。C语言中的示例及其编译结果可在项目网页 中找到。

形式化内存模型 人们已进行了大量努力从阐明规范和定义具有形式语义的实现这两个角度来形式化 C语言 的语义 [2, 3, 8, 9, 11]。这些工作 invariably 使用逻辑内存模型的各种变体,其中每次分配都与某个抽象标识符相关联,而指针则由一个标识符和表示内存块内偏移量的某种路径组成,但 Norrish [11] 的工作除外,它使用的是具体模型。

与 CompCert 的比较 CompCert [9, 10] 及其各种扩展目前允许将指针与整数相互转换,但该语义在转换后仍保留指针的逻辑表示。因此,整型变量不仅可以包含普通的 32位整数,还可以包含逻辑指针表示。在高级语言(CompCert C 和 Clight)中,对转换后的指针执行算术操作被视为程序错误;而在低级语言(从 Cminor 到汇编)中,将整数值加减到已转换的指针上是被定义的,并且仅影响指针逻辑块内的偏移量。

此外,还有研究通过支持指针片段来扩展语义,以实现例如 memcpy 能够处理包含指针的内存 [7],,但这些扩展仍然无法完全支持对已转换为整数类型的指针值进行算术操作。

与CompCertTSO的比较 CompCertTSO编译器[12]在 CompCert的Clight语言基础上扩展了多线程和原子内存原语, 遵循x86‐TSO宽松内存模型。与我们类似,CompCertTSO的 内存模型也支持有限内存,但采用了不同的机制来实现。它设 有一个特殊的逻辑块,其中偏移量实际上充当具体的内存地址。

在编译过程中,所有内存操作都被降级为仅作用于单个有限的 逻辑块。这使得源语言和目标语言(分别具有无限和有限内存)能够共享单一的内存模型,并通过消除对CompCert内存注入的需求简化了正确性陈述。CompCertTSO以与CompCert相 同的方式处理指针‐整数转换,并具有相同的限制。

Comparison with the Symbolic Value Approach 最近, Besson 等人提出了一种对 CompCert 内存模型的扩展,为指针和未初始化的值上的位掩码操作赋予语义[1]。他们的方法涉及将惰性求值的符号表达式(包括对指针表示形式的任意操作)添加到语义值的类别中。每当需要具体值来执行下一步操作时(例如通过指针访问内存或在条件判断的守卫中),就会强制求值符号值。该映射通过作为语义参数提供的归一化函数来实现。归一化函数是偏函数,仅当符号值在所有从逻辑块标识符到具体地址的赋值下(在某些有效性条件约束下)均求值得到唯一结果时才有定义。

Besson 等人的语义必然是确定性的:非确定性被解释为未定义行为,而我们的模型则捕捉了具体地址分配的非确定性。

此外,他们的语义复杂且实际上难以处理:其规范化通过 SMT 求解器实现,总体而言语义过于复杂,无法作为普通 C语言 程序员的心智模型。我们语义中的规范化,在另一方面,是从指针到具体块和整数的直接转换。

最重要的是,尽管他们的方法为涉及指针的位掩码和未初始化值的非严格符合C语言标准的程序提供了语义,但却未能定义使用整数‐指针转换的有用程序。考虑 §3.4 节中讨论的哈希放入示例,其中指针被哈希后可能用于索引数组。由于结果内存位置将取决于内存的具体布局,因此在他们的语义中,该程序将具有未定义行为。一般来说,在我们的模型中由于指针的具体实现而表现出非确定性的任何程序,在 Besson等人 的模型中必然属于未定义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值