d,dip1035系统变量

986 篇文章 1 订阅
12 篇文章 0 订阅

概述

程序的内存安全性取决于程序员和语言实现维护程序数据运行时不变量的能力.
D编译器知道内置类型(如数组和指针)的运行时不变量,并且可用编译时检查来确保正确.这些检查对用户定义类型并不总是足够的.为了可靠维护编译器硬编码知识之外的不变量,D程序员必须求助于手动验证@safe代码和防御性的运行时检查.
DIP提出了一种新的语言特性,@system变量,来解决D的内存安全系统中缺乏表现力问题.在@safe代码中,不能直接写入@system变量,也不能通过转换,重叠,void初化等不受控制方式改变它们的值.因此,可依赖它们来存储受运行时不变量约束的数据.

内容

序号内容
1背景
2基本原理
3先前工作
4描述
5替代方案
6重大更改和弃用
7参考
8版权和许可
9审查

背景

D内存安全系统区分了安全值和不安全值,可在@safe代码中自由使用安全值,而不会导致未定义行为,但不能自由使用不安全值.只有安全值类型是安全类型;同时具有安全和不安全值类型是不安全类型.(更详细定义,请参阅D语言规范函数安全部分.)
D编译器内置知道哪些类型是安全的,哪些不是.从广义上讲,指针,数组和其他引用类型是不安全的;整数,字符和浮点数是安全的;聚集类型的安全性取决于其成员的安全性.
类型的运行时不变量(或仅"不变量")是区分该类型安不安全的规则.(注意本DIP中的"不变量"并不是指合约编程中的不变量块),满足不变量值是安全的;否则,不安全.因此,带运行时不变量类型都是不安全的,不带的,则安全.
为确保不违反它们的不变量,在@safe代码中,限制了不安全类型:
1,不能空初化他们.
2,不能在联中重叠它们.
3,当U是不安全类型时,不能转换T[]U[].

基本原理

尽管上述系统对内置类型及其不变量工作,但它未对程序员提供方法来指示用户定义类型有编译器不知道的附加不变量.因此,维护这样不变量需要程序员付出额外努力.对不安全类型,程序员要手动验证这些不变量是否在@safe代码中维护.对安全类型,程序员还要插入防御性运行时检查来确保维护这些不变量.

示例:用户定义切片

module intslice;

struct IntSlice
{
    private int* ptr;
    private size_t length;

    @safe
    this(int[] src)
    {
        ptr = &src[0];
        length = src.length;
    }

    @trusted
    ref int opIndex(size_t i)
    {
        if (i >= length) assert(0);
        return ptr[i];
    }
}

不变量:length值必须等于ptr指向数组长度.
首先,注意到,此代码编写时是内存安全的(越界访问).只有两个函数可直接访问ptrlength,且都正确地维护了不变量.
然而,为了证明这段代码是内存安全的,程序员不能仅验证@trusted函数正确性.相反,必须手动检查每个涉及.ptrlength的函数.
如果ptrlength@system变量,则直接访问它们的代码都必须是@trusted,程序员不必手动验证@safe代码来证明维护了IntSlice的不变量.
在其他不变量涉及两个或多个变量间关系的用户定义类型中也有相同模式,如标记联和引用计数智能指针.

示例:短串

module shortstring;

struct ShortString
{
    private ubyte length;
    private char[15] data;

    @safe
    this(const(char)[] src)
    {
        assert(src.length <= data.length);

        length = cast(ubyte) src.length;
        data[0 .. src.length] = src[];
    }

    @trusted
    const(char)[] opIndex() const
    {
        // 应可跳过检查边界
        return data.ptr[0 .. length];
    }
}

不变量:length<=15
再一次,有个建立不变量的构造器,及依赖不变量来干活的成员函数.然而,与前例不同,这段代码在编写时并不是内存安全的,尽管看起来是.
为此,考虑以下程序,该程序在@safe代码中使用ShortString,导致未定义行为:

@safe
void main()
{
    import shortstring;
    import std.stdio;

    ShortString oops = void;
    writeln(oops[]);
}

初化ShortString很可能会产生违反其不变量的实例.因为opIndex依赖该不变量来跳过检查边界,所以会导致越界访问内存,而不是安全,可预测的崩溃.
为什么编译器允许在@安全代码中空初化一个ShortString?因为,根据语言规范,只包含正字节数据的安全类型,因此不能不变量.因此,@安全代码可自由初化短串为包括未指定值的任意值,而不会损坏内存.
为使代码内存安全,程序员必须在opIndex中包含额外检查边界:

@safe
const(char)[] opIndex() const
{
    return data[0 .. length];
}

解决方案不能令人满意:程序必须在运行时做多余工作来弥补语言表达能力不足,或者放弃@safe.如果可标记ShortString.length@system,则不会存在该困境.
同样可应用在用户定义类型上,来对编译器认为"安全"的类型施加不变量,如在终开关语句中的enum类型和外部库按数组索引使用的句柄整数.

示例:int作为指针

有时需要对外部库使用的句柄强制域语义.此类类型示例是:
Unix文件描述符,OpenGL对象名,在WebAssembly上下文中JS对象句柄.
他们按简单intuint类型表示,但因为它们引用可分配或释放的资源,作用类似指针.但是,当类型没有指针时,将忽略scope.因为int是安全类型,所以都可从@safe代码中创建int值,因此从域整逃逸导致的内存损坏也可,由不访问变量就创建相同值而产生.即使在中包装,也不会检查:

struct File
{
    private int fd;
}

File gFile;

@safe void escape(scope File f)
{
    gFile = f; // 允许
}

也可用指针把句柄放在union中,但这会不必要地增加结构大小到size_t.sizeof.涉及到@安全代码中的限制时,最好按指针表示int fd;.

全局变量的初值

允许标记聚集字段为@system,帮助编译器维护用户定义类型运行时不变量,但确保变量不是用不安全值开始构造也很重要.禁止在@safe函数中构造不安全值,且在@system@trusted函数中构造它们,就是程序员负责内存安全.在@safe函数中访问不安全类型的全局变量时,编译器应保守并拒绝访问,或检查基本缺陷:

int* x = cast(int*) 0xDEADBEEF;
extern int* y;
int* z = new int(20);

void main() @safe
{
    *x = 10; // 禁止
    *y = 10; // 禁止
    *z = 10; // 可能允许
}

由于在@safe函数中禁止初化cast(int*)0xDEADBEEF表达式,且由于y的初值未知,编译器应按可能包含不安全的值注解x和y变量,因此无法在@safe函数中访问它们.这时,只有z已知具有安全初值,因此在@safe代码中,编译器可允许访问它.
当程序员想放松约束时,允许应用@trusted和@安全变量上很有用,而@系统加强约束很有用.

@trusted int* x = cast(int*) 0xD000; 
//假设是个好的地址
@safe    extern int* y0; 
//假定总是有安全值
@system  extern int* y1;
//可能有不安全的值
@system int* z = new int(20); 
//开始安全,但可能在`@trusted`代码中设置为不安全
enum Opt {a, b, c}
@system  Opt opt = Opt.a; 
//@trusted`代码依赖,它在区间,而不是`cast(Opt)100`.

先前工作

为了实现内存安全,需要封装数据/限制访问数据.
是为了抽象(防止用户依赖细节),而不是保护(确保不变量始终成立).可用于保护只是意外.

描述

@system的现有规则
在更改提议前,先概述了现有规则下哪些声明可有@system属性:

@system int w = 2; 
//编译,闲着
@system enum int x = 3; 
//编译,闲着
enum E
{
    @system x, 
    //错误:`@system`不是枚举成员的有效属性,
    y,
}
//编译,闲着
@system alias x = E; 
//编译,闲着
@system template T() {} 
void func(@system int x) 
//错误:不支持函数参数的`@system`属性
{
    @system int x; // 编译,闲着
}
template Temp(@system int x) {} 
//错误:需要基本类型,而不是@

可附加函数属性到变量声明中,但不能提取它们:

@system @nogc pure nothrow int x;
pragma(msg, __traits(getFunctionAttributes, x)); //错误:第一个参数不是函数
pragma(msg, __traits(getAttributes, x)); // tuple()

提议变更

(0),@safe代码中禁止访问@system标记变量或字段.
包括读写变量.尽管可允许读取具有@system安全类型变量,但限制它,使规则更简单.
示例:

@system int x;

struct S
{
    @system int y;
}

S s;

@safe
void main()
{
    x += 10;//错误:无法修改x`@system`变量
    s.y += 10;//错误:无法修改y字段`@系统`变量
    int y = x;//错误:无法读x`@system`变量
    @system int z;
    z += 1; //错误:无法修改z`@system`变量
}
//按`@system`函数推导
auto foo()
{
    x = 0;
}

@safe代码中进一步禁止@系统变量或字段的操作是:
1,用&创建指向它的可变指针.
2,按参数传递给有refconst标记的函数参数
3,无constref返回

@system变量别名时,别名与符号限制相同.

@system int x = 3;
alias xAlias = x;

void increment(ref int x) @safe
{
    x++;
}

void checkX(const(int)* x) @safe
{
    assert(*x < 10);
}

@safe
void main()
{
    xAlias += 1;//错误,不能修改x`@system`变量
    increment(xAlias); 
    //错误,不能取x`@system`变量可变引用
    checkX(&x);//错误,即使指针是`常`且`typeof(x)`是安全类型.
}

@safe代码中中允许初化@system变量或字段.包括静态初化,自动生成构造器,用户定义构造器和类型的.init值.

@system int x;

shared static this() @safe
{
    x = 3;//允许,这是初化
    x = 3;//第二次禁止,这是赋值`@system`变量
}

struct T
{
    @system int y;
    @system int z = 3; // 允许
    this(int y, int z) @safe
    {
        this.y = y;//允许,这是初化
        this.y = y;
        //第二次禁止,这是赋值给`@system`变量
        this.z = z;//禁止,这是赋值
    }
}

struct S
{
    @system int y = 2;
}

void main() @safe
{
    S s0 = {y: 3}; //静态初化
    S s1 = S(3); //自动生成构造器
    S s2 = S.init; // `.init`初值
    S s3; // 同上
    s3 = s2; //禁止
}

请注意,虽然可能需要在初化@system变量附近需要注解@trusted,但因为无@trusted赋值语法,不能实现它.@trusted作为函数注解有其局限性:
1,它不适用于全局或局部变量,因为@trustedλ移动声明到该函数的域.
2,它不仅信任初化=左侧变量,还信任=右侧初化表达式.用@trusted函数按ref返回变量并为其赋值,不算初化该变量.
3,它禁用-dip1000检查scope/return scope.

struct S
{
    this(ref scope S s) @system
    {
        *(cast(int*) 0xDEADBEEF) = 0;
    }
}

struct Wrapper(T)
{
    @system T t;
    this(T t) @trusted
    {
        this.t = t;//哎呀!调用`@系统`复制构造器
    }
}

void main() @safe
{
    auto w = Wrapper!S(S.init);//`11`信号杀死程序

    () @trusted {@system int x = 3;}();
        //x不再在域内
}

@system int x = (() @trusted => 3)();
//仍未标记赋值`@trusted`
//() @trusted {@system int x = 3;}();//不管用

(1),至少有一个@system字段的聚集是不安全类型
这种聚集有与@安全代码中指针类型相同限制,使得不能用数组转换等隐式写入@系统变量.即使聚集不包含指针成员,也不会去掉.

struct Handle
{
    @system int handle;
}

void main() @safe
{
    Handle h = void; // 错误
    union U
    {
        Handle h;
        int i;
    }
    U u;
    u.i = 3; // 错误

    ubyte[Handle.sizeof] storage;
    auto array = cast(Handle[]) storage[]; // 错误

    scope Handle h0;
    static Handle h1 = h0; // 禁止
}

(2)除非初值不是@safe,无注解的变量和字段都是@safe.
关于变量和字段规则如下:
1,按@system推导(()=>x)时,初化表达式x是@system函数.
2,按@system标记时,与类型无关,结果始终@system.
3,按@trusted标记时,按(()@trusted=>x)对待初化表达式x.
4,@safe标记时,初化表达式必须是@safe.
5,无注解时,仅当类型不安全且初化表达式为@system时结果为@system.

int* getPtr() @system {return cast(int*) 0x8035FDF0;}
int  getVal() @system {return -1;}

extern int* x0;//默认@安全
int* x1 = x0;// @safe, (() => x0)是@safe
int* x2 = cast(int*) 0x8035FDF0;  // @system, (() => cast(int*) 0x8035FDF0)是@system
int* x3 = getPtr();// @system, (() => getPtr())是@system
int  x4 = getVal();// @safe,整是安全的
@system int x5 = 1; //@system,请求的
@trusted int* x6 = getPtr();// @safe,信任的
@safe int* x7 = getPtr();//错误,不能用@系统初化@安全
struct S {
    // 字段,规则一样.
    int* x9 = x3; // @system
    int  x8 = x5; // @safe
}

编译器知道结果值安全的时,可对不安全类型搞例外.

int* getNull() pure @system {return null;}
int* n = getNull();
//尽管有`@system`初化表达式的不安全类型,按`@safe`推导

(@system{})域或(@system:)冒号注解类似影响函数一样,会影响变量.

@system
{
    int y0; // @system
}

@system:
int y1; // @system

语法变化

在这个DIP需要的地方已经允许放@system注解,所以无语法变化.

替代方案

private

有人建议,在@safe代码中应禁止用比如tupleof__traits(getMember)绕过private.
虽然提供确保@safe代码中的不变量方法,符合本DIP,但反对用private.
首先,在@safe代码中禁止绕过private,并不确保用户定义类型运行时不变量.当聚集无不安全类型成员时,仍可通过联中重叠,空初化或数组转换来间接写入字段.
其次,private仅作用于模块级别,除非手动验证不违反@safe模块中其他代码,@trusted成员函数不能假定维护了结构的不变量.这削弱了程序员轻松区分需要手动和可自动检查代码能力,特别是因为某些成员函数(如构造器,析构器和重载符号)必须在同一模块中定义操作数据.
最后,禁止用__traits(getMember,...).tupleof绕过可见性会破坏依赖于此的@safe代码,并且15371问题明确请求此行为.

用不变块指定不安全类型

有人建议添加invariant块使变成不安全类型.

struct Handle
{
    invariant
    {
        //无没有运行时检查,只是标记`句柄`为不安全类型
    }
    private int fd;
}

然而,合约编程是内存安全外单独功能,空不变{}块像是可安全移除的.突然用不变块引入@safe限制和scope语义不行.最重要的是,它仍不能防止@trusted代码之外的修改.

重大更改和弃用

已允许附加@system属性到变量,但不会增加编译器检查.此提案中额外检查@system变量可能会导致现有@safe代码中断(注意,@system代码完全不受此DIP影响).然而,由于@系统变量目前不做事情,作者怀疑用户根本不会添加该属性到变量中,更不用说在@safe代码中变量了.最大风险是变量意外落入@system{}块内或@system:节下.

@system:

int x;//突然在`@safe`代码中不再可写
void unsafeFuncA() {};
void unsafeFuncB() {};

void main() @safe
{
    x++; //不再允许了
}

在新规则下误构造指针,可推导为@system.

struct S
{
    int* a = cast(int*) 0x8035FDF0;
}

void main() @safe
{
    S s;
    *s.a = 0;//现在给出错误
}

每当此时,有可能内存损坏,因此出现编译器错误.
尽管如此,还是提出了两年弃用期,而不是触发错误,在破坏新内存安全规则时给出弃用消息.还可添加-preview=systemVariables预览标志,立即触发违规错误,按警告对待其他弃用消息.预览期结束时,还有-revert=systemVariables标志来恢复它,以便用户可选择更长久保留旧行为.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值