[PATCH] GCC 16.2 Ada:修复自动 Lock_Free 受保护对象的清理遗漏
摘要
我在 FreeBSD 15.1 amd64 上复现了 GCC/GNAT 16.2.0 Ada 前端的一个缺陷。 当反复构造生命周期很短的简单受保护对象时,该缺陷会导致进程内存无限增长。
一个符合 GNAT 自动 Lock_Free 实现条件的受保护类型,可能先被展开为包含原生
System.Tasking.Protected_Objects.Protection _object 字段和
Initialize_Protection 的表示形式,随后在分析受保护体时才被标记为
lock-free。之后生成清理代码时会信任这个较晚确定的 Uses_Lock_Free 状态,
因此即使原生 _object 已经被创建并初始化,也会省略 Finalize_Protection。
这是 GCC 上游编译器的缺陷,但其可见的运行时后果是在 FreeBSD 上复现的。 这里同时提供一个小型 FreeBSD ports 风格的下游补丁,使启用 Ada 的 GCC 16.2 port 无需等待上游处理即可包含此修复。
FreeBSD port 范围
测试所用编译器是一个启用了 Ada 的 GCC 16.2.0 构建,它基于 FreeBSD GCC port 的配置和补丁集。
当前官方 lang/gcc16 port 并未启用 Ada 前端,因此我并不是在声称当前
lang/gcc16 二进制软件包本身会受到影响。本报告没有调查基于较早 GCC
版本的 FreeBSD Ada ports,也不对它们是否受影响作任何声明。
用于 GCC 16.2 基线的 FreeBSD GCC port 补丁没有修改与该缺陷有关的三个
GCC Ada 源文件(exp_ch7.adb、exp_ch9.adb、sem_ch9.adb)。
可稳定复现的编译器症状
对于 GCC/GNAT 16.2.0,默认的自动选择情形会产生:
case Initialize Finalize lock-free ops Protection field
default (automatic) yes no yes present
Lock_Free => True no no yes absent
Lock_Free => False yes yes no present
不一致的默认情形就是这个 bug。
最小源代码如下:
procedure Protected_Finalize is
protected type Prot is
procedure Touch;
private
Value : Integer := 0;
end Prot;
protected body Prot is
procedure Touch is
begin
Value := Value + 1;
end Touch;
end Prot;
Object : Prot;
begin
Object.Touch;
end Protected_Finalize;
使用以下命令编译:
gcc16 -c -O0 -fdump-tree-original protected_finalize.adb
未经修改的 GCC 16.2 tree dump 中包含
system.tasking.protected_objects.initialize_protection,但不包含
system.tasking.protected_objects.finalize_protection。
FreeBSD 运行时后果
使用每批 100 个请求、共 50 批的独立工作负载时,GCC/GNAT 16.2.0 基线产生:
case constructions RSS growth VSZ growth
plain_eight 0 +4 KiB 0 KiB
direct_protected_one 5,000 +628 KiB +528 KiB
protected_one 5,000 +628 KiB +528 KiB
protected_eight 40,000 +5012 KiB +5016 KiB
lock_free_eight 40,000 +4 KiB 0 KiB
线程数和文件描述符数保持稳定。在 FreeBSD 上,原生受保护对象路径最终会使用
pthread_mutex_init / pthread_mutex_destroy,因此应生成的 finalization 一旦缺失,
就会直接表现为原生互斥量状态所占内存的增长。
根本原因
GCC 16.2 前端的处理顺序如下:
Expand_N_Protected_Type_Declaration
-> Uses_Lock_Free is still False
-> generate native Protection _object
Build_Record_Init_Proc
-> generate Initialize_Protection
Analyze_Protected_Body
-> automatically Set_Uses_Lock_Free
cleanup generation
-> Is_Simple_Protected_Type tests not Uses_Lock_Free
-> skip existing Finalize_Protection path
LLDB 跟踪确认,对于该复现程序,Make_Initialize_Protection 会在较晚的
Set_Uses_Lock_Free 之前到达。
修复
该修复让 Is_Simple_Protected_Type 根据实际已经构造出的展开表示来分类类型,
而不是依据之后仍可能发生变化的 Uses_Lock_Free 标志。它先检查对应 record
已经完成分析并且包含 _object,然后复用 Find_Protection_Type 来确认该字段
是普通的 System.Tasking.Protected_Objects.Protection 类型。
显式 Lock_Free => True 的表示中没有 _object 字段,因此其行为保持不变;
带 entry 的受保护类型也继续沿用原来的路径。
FreeBSD ports 风格的补丁包含在下面:
patch-gcc_ada_exp__ch7.adb
可在 GCC 16.2 源码根目录中使用 patch -p0 应用。
补丁验证
FreeBSD 补丁中实际生效的那段编译器修改,已在隔离的 GCC/GNAT 16.2 编译器树中完成构建和测试。
补丁后的结果为:
default (automatic) Initialize=yes Finalize=yes lock-free=yes
Lock_Free => True Initialize=no Finalize=no lock-free=yes
Lock_Free => False Initialize=yes Finalize=yes lock-free=no
在相同工作负载下,FreeBSD 上的独立运行测试中,各种情形都没有出现与构造次数 成比例的增长:
plain_eight RSS +4 KiB, VSZ 0 KiB
direct_protected_one RSS +4 KiB, VSZ 0 KiB
protected_one RSS +4 KiB, VSZ 0 KiB
protected_eight RSS +4 KiB, VSZ 0 KiB
lock_free_eight RSS +4 KiB, VSZ 0 KiB
选定的一组 GCC Ada DejaGNU 回归测试完成,其中 20 项测试按预期通过、0 项意外 失败、0 项 unresolved 测试。额外检查表明,在自动选择情形下,受保护 subtype、数组和 堆对象的成对 finalization 已恢复;一个带 entry 的受保护类型,其 tree dump 在 补丁前后逐字节完全相同。
请求 FreeBSD 采取的措施
在该问题被其所使用的编译器源码修复之前,请考虑在启用了 Ada 的 GCC 16.2 port(例如未来的 GNAT 16 port 或等价 port)中加入这里提供的源码补丁。
如果有助于审查,我可以提供独立运行时复现程序、原始测量数据、编译器 tree dump、LLDB 顺序跟踪以及 DejaGNU 摘要。完整证据保留在本地;这个小型源码补丁 和本报告旨在足以支持 FreeBSD 的初步审查。
如果 maintainer 要求使用 Bugzilla
FreeBSD 对现有 port 提交补丁的常规路径是 Bugzilla。如果 Ada maintainer 在选定受影响或目标 port 后希望在那里跟踪此问题,可使用:
Product: Ports & Packages
Component: Individual Port(s)
Summary: [PATCH] lang/<maintainer-selected-port>: fix GCC 16.2 Ada protected cleanup
请以未压缩形式附上 patch-gcc_ada_exp__ch7.adb。不要只是为了填写该字段而用
lang/gcc16 代替:它当前的官方构建并未启用 Ada。应使用 FreeBSD maintainer
所选择的确切目标 port。
patch-gcc_ada_exp__ch7.adb:
--- gcc/ada/exp_ch7.adb.orig
+++ gcc/ada/exp_ch7.adb
@@ -5432,12 +5432,34 @@
------------------------------
function Is_Simple_Protected_Type (T : Entity_Id) return Boolean is
+ Comp : Entity_Id;
+ Rec_Typ : Entity_Id;
+
begin
- return
- Is_Protected_Type (T)
- and then not Uses_Lock_Free (T)
- and then not Has_Entries (T)
- and then Is_RTE (Find_Protection_Type (T), RE_Protection);
+ if not Is_Protected_Type (T) or else Has_Entries (T) then
+ return False;
+ end if;
+
+ Rec_Typ := Corresponding_Record_Type (T);
+
+ if No (Rec_Typ) or else not Analyzed (Rec_Typ) then
+ return False;
+ end if;
+
+ -- Base the decision on the representation that was actually built.
+ -- Automatic lock-free selection may happen after the corresponding
+ -- record and its initialization procedure have already been created.
+
+ Comp := First_Component (Rec_Typ);
+ while Present (Comp) loop
+ if Chars (Comp) = Name_uObject then
+ return Is_RTE (Find_Protection_Type (T), RE_Protection);
+ end if;
+
+ Next_Component (Comp);
+ end loop;
+
+ return False;
end Is_Simple_Protected_Type;
-------------------------------