【问题标题】:How to align the TASK_TRIGGER record correctly under XE6?XE6下如何正确对齐TASK_TRIGGER记录?
【发布时间】:2014-08-29 22:03:07
【问题描述】:

我正在将 TaskSchedule API 相关代码从 Delphi 5 移植到 Delphi XE6。我遇到了结构对齐和sizeof 的问题。

实际的TASK_TRIGGER结构声明为:

typedef struct _TASK_TRIGGER {
  WORD               cbTriggerSize;
  WORD               Reserved1;
  WORD               wBeginYear;
  WORD               wBeginMonth;
  WORD               wBeginDay;
  WORD               wEndYear;
  WORD               wEndMonth;
  WORD               wEndDay;
  WORD               wStartHour;
  WORD               wStartMinute;
  DWORD              MinutesDuration;
  DWORD              MinutesInterval;
  DWORD              rgFlags;
  TASK_TRIGGER_TYPE  TriggerType;
  TRIGGER_TYPE_UNION Type;
  WORD               Reserved2;
  WORD               wRandomMinutesInterval;
} TASK_TRIGGER

我正在使用的MsTask.pas 的旧翻译(以及 MsTask 的当前 JCL 事务)将其翻译为:

  _TASK_TRIGGER = record
    cbTriggerSize: WORD;
    Reserved1: WORD;
    wBeginYear: WORD;
    wBeginMonth: WORD;
    wBeginDay: WORD;
    wEndYear: WORD;
    wEndMonth: WORD;
    wEndDay: WORD;
    wStartHour: WORD;
    wStartMinute: WORD;
    MinutesDuration: DWORD;
    MinutesInterval: DWORD;
    rgFlags: DWORD;
    TriggerType: TTaskTriggerType;
    Type_: TTriggerTypeUnion;
    Reserved2: WORD;
    wRandomMinutesInterval: WORD;
  end;

sizeof 这个记录在 Delphi 5 和 XE6 之间有所不同:

  • 德尔福5:SizeOf(TASK_TRIGGER) = 48
  • 德尔福 XE6 SizeOf(TASK_TRIGGER) = 47

ITaskTrigger.SetTrigger(TASK_TRIGGER) 的函数调用在 Dephi5 中成功,但在 Delphi XE6 中调用 The parameters are incorrect 失败。

布局

如果我天真地猜测记录的布局,我会是:

□□□□ □□□□       //cbTriggerSize, Reserved1    (4 bytes)
□□□□ □□□□       //wBeginYear,    wBeginMonth  (8 bytes)
□□□□ □□□□       //wBeginDay,     wEndYear     (12 bytes)
□□□□ □□□□       //wEndMonth,     wEndDay      (16 bytes)
□□□□ □□□□       //wStartHour,    wStartMinute (20 bytes)
□□□□□□□□        //MinutesDuration             (24 bytes)
□□□□□□□□        //MinutesInterval             (28 bytes)
□□□□□□□□        //rgFlags                     (32 bytes)
□□□□□□□□        //TriggerType                 (36 bytes)
□□□□□□□□        //Type_                       (40 bytes)
□□□□ □□□□       //Reserved2      wRandomMinutesInterval    (44 bytes)

但是当我实际检查 Delphi 5 调试器中填充的结构时,实际结构是 48 字节,在 TriggerTypeType_ 之间有额外的 4 字节填充:

□□□□ □□□□       //cbTriggerSize, Reserved1    (4 bytes)
□□□□ □□□□       //wBeginYear,    wBeginMonth  (8 bytes)
□□□□ □□□□       //wBeginDay,     wEndYear     (12 bytes)
□□□□ □□□□       //wEndMonth,     wEndDay      (16 bytes)
□□□□ □□□□       //wStartHour,    wStartMinute (20 bytes)
□□□□□□□□        //MinutesDuration             (24 bytes)
□□□□□□□□        //MinutesInterval             (28 bytes)
□□□□□□□□        //rgFlags                     (32 bytes)
□□□□□□□□        //TriggerType                 (36 bytes)
□□□□□□□□        //4 bytes padding
□□□□□□□□        //Type_                       (44 bytes)
□□□□ □□□□       //Reserved2      wRandomMinutesInterval    (48 bytes)

好的,如果这就是 Delphi 5 想要这样做的方式,我该争论谁。它肯定比我更了解 Windows 结构打包。

我检查布局的方式是在记录中放置已知的标记值:

trigger.cbTriggerSize := $1111; // WORD;
trigger.Reserved1 := $2222; // WORD;
trigger.wBeginYear := $3333; // WORD;
trigger.wBeginMonth := $4444; // WORD;
trigger.wBeginDay := $5555; // WORD;
trigger.wEndYear := $6666; // WORD;
trigger.wEndMonth := $7777; // WORD;
trigger.wEndDay := $8888; // WORD;
trigger.wStartHour := $9999; // WORD;
trigger.wStartMinute := $aaaa; // WORD;
trigger.MinutesDuration := $bbbbbbbb; // DWORD;
trigger.MinutesInterval := $cccccccc; // DWORD;
trigger.rgFlags := $dddddddd; // DWORD;
trigger.TriggerType := TASK_TIME_TRIGGER_DAILY; // TTaskTriggerType;
trigger.Type_.Daily.DaysInterval := $ffff; // TTriggerTypeUnion;
trigger.Reserved2 := $1111; // WORD;
trigger.wRandomMinutesInterval := $2222; // WORD;

并在 CPU 窗口中查看生成的内存布局:

(交替的成员红色和绿色,红色是填充);

在 Delphi 5 中总共有 48 bytes

输入 XE6

当我在 Delphi XE6 中进行相同的测试时,它的包装方式不同(而且非常可怕):

首先,它无法在 32 位边界上分配堆栈变量;不过没关系。
CPU 窗口拒绝完全在结构上启动视图 - 坚持它开始在 DWORD 边界上显示内存;不过没关系。
记录在$18EB31处确实没有对齐:

所以我们会继续这样做。

□□□□ □□□□       //cbTriggerSize, Reserved1    (4 bytes)
□□□□ □□□□       //wBeginYear,    wBeginMonth  (8 bytes)
□□□□ □□□□       //wBeginDay,     wEndYear     (12 bytes)
□□□□ □□□□       //wEndMonth,     wEndDay      (16 bytes)
□□□□ □□□□       //wStartHour,    wStartMinute (20 bytes)
□□□□□□□□        //MinutesDuration             (24 bytes)
□□□□□□□□        //MinutesInterval             (28 bytes)
□□□□□□□□        //rgFlags                     (32 bytes)
□□ □□□□ □□      //TriggerType, Type_, 1 byte padding (36 bytes)
□□□□□□□□        //4 bytes padding                    (40 bytes)
□□□□□□ □□       //3 bytes padding, part of Reserved2 (44 bytes)
□□ □□□□         //Remainnder of Reserved2, wRandomMinutesInterval    (47 bytes)

这是设计上的怪物,还是编译器代码生成错误?

你试过 {$ALIGN ON} 吗?

当然。

sizeof(TASK_TRIGGER) = 52

失败。

{$MINENUMSIZE 4} 怎么样?

好的。

sizeof(TASK_TRIGGER) = 50

失败。

是的,但是您是否同时尝试了这两种方法?

触摸。

sizeof(TASK_TRIGGER) = 52

失败。

这几乎就像 Delphi 拒绝相信它是 Windows 编译器。

总结

$ALIGN  $MINENUMSIZE  $OLDTYPELAYOUT    "packed"  |  sizeof
======  ============  ================  ========     ======
ON      4             ON                yes              57    
ON      4             OFF               yes              50    
ON      4             ON                no               52
ON      4             OFF               no               52

ON      2             ON                yes              50
ON      2             OFF               yes              50
ON      2             ON                no               52
ON      2             OFF               no               52

ON                    ON                yes              49
ON                    OFF               yes              49
ON                    ON                no               52
ON                    OFF               no               52

等我有耐心再补充。

【问题讨论】:

  • 获取 C 编译器并查看布局。使用和offsetof() 宏。
  • @DavidHeffernan 我这里有一个功能编译器(Delphi 5)。我只需要 Delphi XE6 来创建与 Windows ABI 兼容的结构。
  • @Ian D5 在结构布局上很烂。认真获得一个真正的编译器!一种理解windows头文件的人。从马嘴里获取你的信息。
  • @Ian 我们已经看到了 ;-)

标签: delphi delphi-xe6


【解决方案1】:

正确的布局如下:

00-01 cbTriggerSize: WORD;
02-03 Reserved1: WORD;
04-05 wBeginYear: WORD;
06-07 wBeginMonth: WORD;
08-09 wBeginDay: WORD;
10-11 wEndYear: WORD;
12-13 wEndMonth: WORD;
14-15 EndDay: WORD;
16-17 wStartHour: WORD;
18-19 wStartMinute: WORD;
20-23 MinutesDuration: DWORD;
24-27 MinutesInterval: DWORD;
28-31 rgFlags: DWORD;
32-35 TriggerType: TTaskTriggerType;
36-43 Type_: TTriggerTypeUnion;
44-45 Reserved2: WORD;
46-47 wRandomMinutesInterval: WORD;

让我们逐点解决:

  • 前 10 个单词放在一起,没有填充,全部自然对齐。
  • 然后是 3 个双字,同样不需要填充来对齐 4 字节边界。
  • 接下来是 C 枚举,它实际上是一个 int。同样,对齐 4,不需要填充。
  • 现在是工会。结构的联合,其中最大的是MONTHLYDATE,由于对齐,其大小为 8。
  • 另外两个词在它们的自然偏移处适合,没有填充。

假设 C 头文件指定对齐的结构,您需要使用 {$MINENUMSIZE 4}{$ALIGN ON} 编译它。

您省略的详细信息是 JEDI 单元的编译器选项以及枚举和联合的声明。查看 github repo 中的单元,我看到 {$MINENUMSIZE 4}{$ALIGN ON} 这很好。枚举是一个普通的 Delphi 枚举类型。也很好。但是联合和它包含的记录都被打包了。这是错误的,并导致联合的大小错误。

我还从 JEDI 来源看到了这一点:

_TASK_TRIGGER = record 
// SP: removed packed record statement as seemed to affect SetTrigger

似乎这个单元的作者对打包和对齐有点困惑。

XE6 如何认为这可能是 47 字节,这超出了我的理解。尤其是因为我看不到所有细节,因为不幸的是这个问题省略了一些。无论如何,您确实需要枚举大小为 4,并对齐记录,因此 47 个数据点可能不是关键点。我建议我们忽略它。

适当的 XE6 数据点是大小为 52 的 {$MINENUMSIZE 4}{$ALIGN ON} 大小写。这里我们看到,由于某种难以理解的原因,并集消耗了 12 个字节。我假设你的工会是根据 JEDI github repo 中的声明。是吗?

在您提供的事实面前,这对我来说就像一个 Delphi XE6 编译器错误。旧的 Delphi 版本在对齐结构方面是出了名的差。我认为现代版本是正确的,但也许不是。但是,可能混淆所有这一切的是您正在使用的标题翻译。当然,它似乎对包装感到困惑。而且我们还看不到您的所有代码。我只在 github 上看到过最新的。也许问题出在那里而不是编译器。 @LURD 的调查表明 XE6 编译器正确地布置了结构。


处理此类问题的方法是使用 MS 编译器转到原始头文件。包含标题并使用sizeofoffsetof 转储布局。从马的口中可以这么说。

然后对您的 Delphi 编译器执行相同操作并比较布局。代替 C++ offsetof 使用我在这里展示的技巧:Can we implement ANSI C's `offsetof` in Delphi?

至于你如何进行,一旦你知道了正确的布局,应该很容易说服编译器以同样的方式放置记录。从 github repo 中的 JEDI 代码开始,并删除所有使用 packed。试试那个尺寸。如果这不起作用,请调查联合的布局。作为最后的手段,您可以打包所有东西并手动填充。如果问题确实存在,也许与工会这样做就足够了。

更新: LURD 的回答似乎表明,在联合和包含的结构中删除打包的使用可以提供正确的布局。

注意:我手边没有任何编译器,所以以上所有内容都是从我的脑海中生成的。我可能在细节上犯了错误。然而,我相信使用 MS 编译器向您展示正确布局的一般建议是解决所有对 Win32 结构布局的疑问的一般建议。使用手头的工具,您可以解决这种性质的任何问题。

【讨论】:

  • 我提供了一个小例子来说明你的结论,XE6 的大小也是 48。
  • 事实证明这是对我不利的事情的结合。首先是奇怪的包装。我试图用手动填充来解决这个问题。然后我尝试添加oldtypelayout。然后我尝试了alignminenumsize 的组合。不知道 Windows ABI 是否将枚举定义为 4 或 8 个字节,或对齐间距,我正要深入研究 {$Axx}。但是复制@LURD 代码以吐出结构偏移量指出了问题所在:我的手动填充仍然在那里!我现在看到所有WinApi.xxx 文件都是{$ALIGN ON} {$MINENUMSIZE 4}。只需将其添加到所有 Windows 结构中即可。
  • 我想我最初被 XE6 使用的默认非缓存对齐、非 DWORD 对齐的结构布局分心和害怕。
  • 是的,对齐结构,枚举是整数。不过也有例外。打包了许多本机 api 结构。还有其他奇怪的地方。因此,请务必使用带有头文件的 ms 编译器。
  • 默认非缓存对齐、非 DWORD 对齐、结构布局了解大小和对齐方式很重要。对齐的结构总是将成员放置在其对齐的倍数上。结构的对齐方式由其最大成员的对齐方式决定。但是,当您在 delphi 中打包影响布局 对齐的结构时。压缩结构的对齐方式为 1。因此编译器不会强制将其放置在特殊的字节边界处。
【解决方案2】:

按照大卫的回答,{$ALIGN ON} {$MINENUMSIZE 4} 并删除打包声明,

program Project8;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,Windows;

{$ALIGN ON}
{$MINENUMSIZE 4}

Type

{$EXTERNALSYM _TASK_TRIGGER_TYPE}
  _TASK_TRIGGER_TYPE = (
{$EXTERNALSYM TASK_TIME_TRIGGER_ONCE}
    TASK_TIME_TRIGGER_ONCE, // 0   // Ignore the Type field.
{$EXTERNALSYM TASK_TIME_TRIGGER_DAILY}
    TASK_TIME_TRIGGER_DAILY, // 1   // Use DAILY
{$EXTERNALSYM TASK_TIME_TRIGGER_WEEKLY}
    TASK_TIME_TRIGGER_WEEKLY, // 2   // Use WEEKLY
{$EXTERNALSYM TASK_TIME_TRIGGER_MONTHLYDATE}
    TASK_TIME_TRIGGER_MONTHLYDATE, // 3   // Use MONTHLYDATE
{$EXTERNALSYM TASK_TIME_TRIGGER_MONTHLYDOW}
    TASK_TIME_TRIGGER_MONTHLYDOW, // 4   // Use MONTHLYDOW
{$EXTERNALSYM TASK_EVENT_TRIGGER_ON_IDLE}
    TASK_EVENT_TRIGGER_ON_IDLE, // 5   // Ignore the Type field.
{$EXTERNALSYM TASK_EVENT_TRIGGER_AT_SYSTEMSTART}
    TASK_EVENT_TRIGGER_AT_SYSTEMSTART, // 6   // Ignore the Type field.
{$EXTERNALSYM TASK_EVENT_TRIGGER_AT_LOGON}
    TASK_EVENT_TRIGGER_AT_LOGON // 7 // Ignore the Type field.
    );
{$EXTERNALSYM TASK_TRIGGER_TYPE}
  TASK_TRIGGER_TYPE = _TASK_TRIGGER_TYPE;
  TTaskTriggerType = _TASK_TRIGGER_TYPE;

{$EXTERNALSYM PTASK_TRIGGER_TYPE}
  PTASK_TRIGGER_TYPE = ^_TASK_TRIGGER_TYPE;
  PTaskTriggerType = ^_TASK_TRIGGER_TYPE;


type
{$EXTERNALSYM _DAILY}
  _DAILY = {packed} record
    DaysInterval: WORD;
  end;
{$EXTERNALSYM DAILY}
  DAILY = _DAILY;
  TDaily = _DAILY;


type
{$EXTERNALSYM _WEEKLY}
  _WEEKLY = {packed} record
    WeeksInterval: WORD;
    rgfDaysOfTheWeek: WORD;
  end;
{$EXTERNALSYM WEEKLY}
  WEEKLY = _WEEKLY;
  TWeekly = _WEEKLY;


type
{$EXTERNALSYM _MONTHLYDATE}
  _MONTHLYDATE = {packed} record
    rgfDays: DWORD;
    rgfMonths: WORD;
  end;
{$EXTERNALSYM MONTHLYDATE}
  MONTHLYDATE = _MONTHLYDATE;
  TMonthlyDate = _MONTHLYDATE; // OS: Changed capitalization


type
{$EXTERNALSYM _MONTHLYDOW}
  _MONTHLYDOW = {packed} record
    wWhichWeek: WORD;
    rgfDaysOfTheWeek: WORD;
    rgfMonths: WORD;
  end;
{$EXTERNALSYM MONTHLYDOW}
  MONTHLYDOW = _MONTHLYDOW;
  TMonthlyDOW = _MONTHLYDOW; // OS: Changed capitalization


{$EXTERNALSYM _TRIGGER_TYPE_UNION}
  _TRIGGER_TYPE_UNION = {packed} record
    case Integer of
      0: (Daily: DAILY);
      1: (Weekly: WEEKLY);
      2: (MonthlyDate: MONTHLYDATE);
      3: (MonthlyDOW: MONTHLYDOW);
  end;
{$EXTERNALSYM TRIGGER_TYPE_UNION}
  TRIGGER_TYPE_UNION = _TRIGGER_TYPE_UNION;
  TTriggerTypeUnion = _TRIGGER_TYPE_UNION;


  _TASK_TRIGGER = record
    cbTriggerSize       : WORD;
    Reserved1           : WORD;
    wBeginYear          : WORD;
    wBeginMonth         : WORD;
    wBeginDay           : WORD;
    wEndYear            : WORD;
    wEndMonth           : WORD;
    wEndDay             : WORD;
    wStartHour          : WORD;
    wStartMinute        : WORD;
    MinutesDuration     : DWORD;
    MinutesInterval     : DWORD;
    rgFlags             : DWORD;
    TriggerType         : TTaskTriggerType;
    Type_               : TTriggerTypeUnion;
    Reserved2           : WORD;
    wRandomMinutesInterval : WORD;
  end;

  PTaskTrigger =^_TASK_TRIGGER;

const
  PTrigger : PTaskTrigger = Nil;

begin
  WriteLn(SizeOf(_TASK_TRIGGER));
  WriteLn(Integer(@PTrigger^.cbTriggerSize));
  WriteLn(Integer(@PTrigger^.Reserved1));
  WriteLn(Integer(@PTrigger^.wBeginYear));
  WriteLn(Integer(@PTrigger^.wBeginMonth));
  WriteLn(Integer(@PTrigger^.wBeginDay));
  WriteLn(Integer(@PTrigger^.wEndYear));
  WriteLn(Integer(@PTrigger^.wEndMonth));
  WriteLn(Integer(@PTrigger^.wEndDay));
  WriteLn(Integer(@PTrigger^.wStartHour));
  WriteLn(Integer(@PTrigger^.wStartMinute));
  WriteLn(Integer(@PTrigger^.MinutesDuration ));
  WriteLn(Integer(@PTrigger^.MinutesInterval ));
  WriteLn(Integer(@PTrigger^.rgFlags ));
  WriteLn(Integer(@PTrigger^.TriggerType ));
  WriteLn(Integer(@PTrigger^.Type_ ));
  WriteLn(Integer(@PTrigger^.Reserved2  ));
  WriteLn(Integer(@PTrigger^.wRandomMinutesInterval  ));
  ReadLn;
end.

结果:

48
0
2
4
6
8
10
12
14
16
18
20
24
28
32
36
44
46

所以 XE6 的大小也是 48。

更新

恢复打包声明后,结果仍然是 48。 但布局不同:

48
0
2
4
6
8
10
12
14
16
18
20
24
28
32
36
42
44

从具有扩展 RTTI 的 D2010 开始,可以创建一个通用过程来列出记录中的所有偏移量。如果有人感兴趣,这里只是显示一个示例。

procedure ListRecordFieldsOffset( ARecTp: PTypeInfo; const AList: TStrings);
// Uses Classes,RTTI,TypInfo
// Example call: ListRecordFieldsOffset(TypeInfo(TMyRec),MyList);
var
  AContext : TRttiContext;
  AField   : TRttiField;
begin
  if Assigned(ARecTp) and (ARecTp^.Kind = tkRecord) and Assigned(AList) then
  begin
    AList.BeginUpdate;
    for AField in AContext.GetType(ARecTp).GetFields do
    begin
      AList.Add(AField.Name + ': ' + AField.FieldType.ToString + ' = ' +
        IntToStr(AField.Offset));
    end;
    AList.EndUpdate;
  end;
end;

【讨论】:

  • +1 谢谢。它是 52,在联合上打包并包含结构?
  • @DavidHeffernan,在所有打包结构和联合恢复后,它仍然是 48。嗯,我可以发誓在我改变之前我还有别的东西。
  • 我希望是 46。在 MONTHLYDATE 末尾会有两个字节的填充缺失。
  • 你恢复了所有打包的声明吗?一个是联合类型,四个是包含类型?
  • @DavidHeffernan,是的。我什至从 JCL 源中重新复制并重新启动了编译器。
猜你喜欢
  • 1970-01-01
  • 2019-10-08
  • 2016-08-07
  • 2013-08-28
  • 2015-11-08
  • 1970-01-01
  • 2021-11-16
  • 2017-04-30
  • 1970-01-01
相关资源
最近更新 更多