d中编写@信任代码
原文
编程中,有内存安全
概念,在一定
程度上保证不会导致破坏内存
.内存安全的极致
是可机械
验证不会破坏
内存.来防护缓冲区溢出
等攻击.D
语言定义内存安全为:允许编写相当多
有用代码,但保守禁止
粗略的.实践中,编译器
并不是万能的,它缺乏人类
非常擅长,看到的环境
,因此经常需要允许有风险的行为
.
因为编译器在内存安全
方面非常严格
,所以需要@信任
.
先讨论内存安全
和D的@安全
机制.
什么是内存安全代码?
最简单方法是检查导致不安全
代码原因.在静态
类型语言中,一般有3种
主要方法可违反
安全:
1,从有权访问
的有效内存段
外缓冲区
读写.
2,允许按指针
转换非指针
内存值.
3,用悬挂
或不再有效
指针.
在D中
,第一项很容易实现:
auto buf = new int[1];
buf[2] = 1;
使用默认
检查边界,即使不检查安全
代码,运行时
也会导致异常.但是D允许访问数组
指针来绕过它:
buf.ptr[2] = 1;
对第二点,只需要用转换
:
*cast(int*)(0xdeadbeef) = 5;
第三个也相对简单:
auto buf = new int[1];
auto buf2 = buf;
delete buf;//设置`buf`为`null`
buf2[0] = 5;//但不是`buf2`.
悬挂指针
也经常是指向不再使用
的栈数据
:
int[] foo()
{
int[4] buf;
int[] result = buf[];
return result;
}
总之,安全代码
避免导致破坏内存
.为此,必须遵守
一些规则.
注意:在D中,解引用用户空间中的null
指针不算内存安全
问题.为什么呢?
因为这会触发硬件异常
,且一般不会使程序处于破坏内存
的未定义状态
.它只是中止
程序.对用户或程序员
似乎是不可取
的,但在防止利用漏洞
方面非常好.如果null
指针指向非常大的内存空间
,则null
指针有潜在
内存问题.但对安全D
,这需要异常大的结构
才开始担心它.因而为检查null
罕见情况,而检测解引用指针
,导致的性能下降
是不值得的.
D的@safe
规则
D提供了标记编译器机械检查函数
的@safe
属性,来避免内存安全
问题.当然,有时,需要异常
处理.
如下规则
旨在防止上述问题
规范.
1,禁止
更改原始指针
值.如果@safeD
代码有指针
,它只能访问指向
值,不能访问包括索引指针
的其他值.
2,禁止转换
指针为除void*
外类型.禁止把非指针
类型转换为指针
类型.只要有效,允许其他强制
转换(如从浮
强制转换为整
).也允许动态数转换为空[]
.
3,禁止访问
与其他
类型重叠
的指针类型
的联合.类似上面的1和2
规则.
4,访问动态数组(a)
中元素或从(a)
中取切片
,必须是编译器
证明安全的,或运行时检查边界.甚至在忽略检查边界的发布
模式下时(注意:dmd
的选项-boundscheck=off(b)
会覆盖它,所以使用(b)时要格外小心).
5,在普通D中,可切片
指针来从指针
创建动态数组
.在@safeD
中,这是禁止的,因为编译器
不知道通过该指针
实际有多少可用空间
.
6,禁止
取局部变量
或(栈上变量
的)函数参数
的指针
或取引用参数
指针.例外是切片
本地静态数组,包括上面的foo
函数.这是已知问题
(可能已修复).
7,禁止在是或包含
引用间,显式
转换不变和可变
类型.在不变和可变
间可隐式
转换值类型
且非常好
.
8,禁止在是或包含
引用间,显式转换线本和共享
类型.同样,转换值类型
很好(且可隐式完成).
9,@safe
代码中禁止
D的内联汇编
功能.
10,禁止抓不是从异常
类继承的对象
.
11,D中,默认
初化所有变量.但是,可用空
初化器不初化它:
int *s = void;
@safeD
中禁止
该用法.上面指针会指向随机内存
并成为明显的悬挂指针
.
12,__gshared
变量是仍在全局
空间中的静态shared
.一般用于与C代码
交互.@safeD
中禁止访问此类变量
.
13,禁止使用动态
数组的ptr
属性(编译器在2.072
版本中发布的新规则
).
14,禁止赋值切片另一void[]
来写入void[]
数据(此规则也是在2.072
中发布的新规则
).
15,@safeD
只能调用@安全
函数或推导为@safe
函数的函数
.
需要@trusted
上述
规则可很好防止
破坏内存,但会阻止许多有效且安全
代码.如,考虑想用read
系统调用函数,原型如下:
ssize_t read(int fd, void* ptr, size_t nBytes);
该函数
,会从给定文件描述符
中读取数据
,并把它其入ptr
指向的缓冲区
中,且期望为nBytes
字节长.它返回实际读取
字节数,如果错误
,则返回负值
.
使用此函数来读数据
至栈分配
的缓冲区
类似:
ubyte[128] buf;
auto nread = read(fd, buf.ptr, buf.length);
如何在@safe
函数中完成?在@safe
代码中使用read
的主要问题
是指针
只能传递一个值
,这里是一个ubyte
.read
期望存储缓冲区
的更多字节
.在D中,一般按动态数组
传递待读取
数据.
但是,read
不是D代码
,并且使用了常见的C习惯
用法,即分别传递缓冲区和长度
,因此无法标记为@safe
.考虑以下@safe
代码调用:
auto nread = read(fd, buf.ptr, 10_000);
该调用
绝对不安全.仅在理解read
函数且调用环境
,确保
不会写缓冲区外
内存时,是安全的.
为此,D
提供了@trusted
属性,告诉编译器
函数内部代码假定为@safe
,但不要机械
检查.开发人员负责确保
代码是@safe
的.
解决问题的函数在D中可能如下所示:
auto safeRead(int fd, ubyte[] buf) @trusted
{
return read(fd, buf.ptr, buf.length);
}
//标记为@信任
每当标记整个函数为@trusted
时,请考虑是否可从会危及内存安全
的环境
中调用它.是,则一定不要
标记为@trusted
.即使想只按安全
方式调用它,编译器也不会阻止别人
不安全使用它.safeRead
应可从@safe
环境中调用很好
,所以标记为@trusted
.
safeRead
函数的更自由API
可取void[]
数组作为缓冲区
.然而,在@safe
代码中,可转换动态数组
为包括指针数组的void[]
数组.读文件数据
进指针数组
可能会导致悬挂指针
数组.因此要使用ubyte[]
.
@trusted
逃逸
@trusted
逃逸是允许如不暴露不安全调用
给程序其他部分的@系统(D不安全默认值)
调用的单个表达式.无需编写safeRead
函数,可在@safe
函数这样:
auto nread = ( () @trusted => read(fd, buf.ptr, buf.length) )();
仔细看看逃逸
,看看发生了什么.D
允许用()=>expr
语法,声明计算并返回
单个表达式的λ
函数.为了调用λ
函数,附加括号到λ
.但是,符号
优先级会应用括号
至表达式
而不是λ
,因此必须用()
包装整个λ
来调用.最后,用@trusted
标记λ
,因此外围@safe
环境可调用它.
除了简单λ
外,还可用整个嵌套
函数或多语句λ
.但是,要尽量减少这类代码.
@trusted
的经验法则
示例表明,标记为@trusted
有巨大影响.如果禁止检查
内存安全,但允许@safe
代码调用它,则你
必须确保
它不会破坏内存
.如下规则指导何处放@trusted
标记并避免陷入
麻烦:
1,保持@trusted
代码尽量小
从不机械检查@trusted
代码安全,因此必须检查每一行
的正确性.因而,始终建议保持@trusted
代码尽量小.
2,不安全调用泄漏
时,标记整个
函数为@trusted
如果泄漏
,最好把整个都标记为@trusted
,这样更符合事实,再每行检查.这不是硬性规定;如,即使它会影响
稍后按@safe
模式函数使用
的数据,前面示例中的read
调用是完全安全
的.
在函数
开头用C的malloc
分配的指针,然后在释放(free)
前,可能已被复制
到其他地方.这里,悬挂
指针可能会违反@safe
,即使机械检查
也是如此.相反,按@trusted
包装使用指针
的整个部分
,甚至整个函数
.或,使用域保护
来保证数据生命期
,直到函数结束,域保护.
在接受任意类型
的模板函数上永远不要用@trusted
D足够聪明,对遵循规则
的模板函数
,包括模板类型
成员函数,可推导出@safe
.
让编译器完成工作.为确保在正确
环境中,该函数为@safe
,请创建@safe
的单元测试
来调用它.@trusted
标记函数,允许安全检查器
忽略可能违反内存安全
的重载符号或成员
!特别是postblit
和opCast
.
在此用@trusted
逃逸仍然可以,但要非常小心.在考虑如何滥用
此类函数时,请特别考虑可能包含指针
类型.常见
错误是标记区间
函数或域用法为@trusted
.请记住,大多数区间
都是模板
,并且在迭代
类型有@system
级的后传递(postblit)
或构造器/析构器
,或从用户
提供λ
生成时,可很容易推导为@system
.
使用@safe
查找需要标记为@trusted
的部分
有时期望@safe
的模板,可能无法推导
为@safe
,且不清楚原因.这时,暂时标记
模板函数为@safe
来查看编译器
报错.如果合适,则插入@trusted
逃逸,
有时,广泛
使用的模板,标记为@safe
可能会破坏太多.则在标记@safe
的不同名下,复制模板
,并更改要检查
的调用,让它们调用替代
模板.
考虑未来如何编辑该函数
编写信任(@信任)
函数时,始终考虑
给定API
,如何使用调用它,并确保它应该是@safe
.上面很好
的示例是确保safeRead
不接受指针数组
.
但是,不安全
代码潜入还可能是,有人稍后
编辑函数一部分,使先前
验证无效,从而需要重新检查
整个函数.插入评论
来解释部分更改
会违反内存安全
请记住,拉取请求
差异并不总是显示整个环境
,包括正在编辑的长函数
是@trusted
!
用有确定生命期类型来封装@trusted
操作
有时,资源只有在创建和/或析构
时才有危险,但使用时很安全
.可以把危险操作
封装到类型的构造器和析构器
中,并标记为@trusted
,这样允许@safe
代码在生命期间
使用资源.当然要小心.
要禁止@safe
代码找出实际资源
,并绕过管理
资源类生命期
后保存一份副本
!只要@safe
代码有引用,就必须确保资源
是活动的.
如,只要不能访问负载
数据的原始指针
,引用计数
类型可完全安全
.不能按@safe
标记D
的std.typecons.RefCounted
,因为它用别名本
转移到受保护
的分配结构以完成功能
,调用该结构
都不知道引用计数
.复制
该有效负载指针
,然后释放
结构后,就有悬挂指针
了.
这不能是@safe!
有时,在很明显应禁止
时,编译器却允许函数
为@safe
或推导为@safe
.
这由以下两种引起的:
1,@安全
函数调用@信任
标记但允许系统调用
的函数,
2,@safe
系统中存在错误或漏洞
.
多数时候,是前者.@trusted
是非常棘手
,很难搞对的属性.
开发人员经常滥用@trusted
.即使是核心D开发人员
也会犯该错误!因此,推导为安全
的模板函数也是,有时甚至
很难找到原因
.
即使发现根本原因
后,一般也很难删除@trusted
标记,因为它会破坏该函数
的许多用户.但是,最好破坏期望内存安全
承诺代码,而不是遭受可能内存破坏
.越早弃用和删除
标记越好.然后对证明安全
的插入@信任
逃逸.
如果确实是系统漏洞
,请报告
问题,或在D论坛
提问.D
社区一般乐于提供帮助
,且内存安全是该语言的创建者WalterBright
的特别关注点
.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现