如何在XE6下正确对齐TASK_TRIGGER记录?

时间:2014-08-29 22:03:08

标签: delphi delphi-xe6

我正在将Delphi 5中的TaskSchedule API相关代码移植到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;

{1}这条记录在Delphi 5和XE6之间有所不同:

  • Delphi 5:sizeof
  • Delphi XE6 SizeOf(TASK_TRIGGER) = 48

SizeOf(TASK_TRIGGER) = 47的函数调用在Dephi5中成功,但在使用ITaskTrigger.SetTrigger(TASK_TRIGGER)的Delphi XE6时失败。

布局

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

The parameters are incorrect

但是当我实际检查Delphi 5调试器中的填充结构时,实际结构是48个字节,在□□□□ □□□□ //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) TriggerType之间有额外的4个字节填充:

Type_

好的,如果这就是德尔福5想要做的事情,那我就是谁来争论。它确实比我更了解Windows结构包装。

我检查布局的方法是在记录中放置已知的哨兵值:

□□□□ □□□□       //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)

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

enter image description here

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

在Delphi 5中总计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;

输入XE6

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

enter image description here

首先,它无法管理在32位边界上分配堆栈变量;但那没关系。
CPU窗口拒绝在结构上准确地启动视图 - 坚持它开始在DWORD边界上显示内存;但那没关系。
该记录确实未与48 bytes对齐:

enter image description here

所以我们将继续这样做。

$18EB31

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

你试过{$ ALIGN ON}吗?

不确定。

□□□□ □□□□       //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)

enter image description here

失败。

{$ MINENUMSIZE 4}怎么样?

好。

sizeof(TASK_TRIGGER) = 52

enter image description here

失败。

是的,但你们一起尝试过吗?

TOUCHE。

sizeof(TASK_TRIGGER) = 50

enter image description here

失败。

这几乎就像Delphi不相信它是Windows编译器一样。

摘要

sizeof(TASK_TRIGGER) = 52

我会耐心地添加更多内容。

2 个答案:

答案 0 :(得分:3)

正确的布局如下:

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字节边界上对齐。
  • 接下来是真正的int的C enum。再次,对齐4,不需要填充。
  • 现在是工会。结构的联合,其中最大的是MONTHLYDATE,由于对齐,其大小为8。
  • 另外两个字符合自然偏移而没有填充。

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

您省略的详细信息是JEDI单元的编译器选项以及枚举和联合的声明。看一下github仓库中的单位,我看到{$MINENUMSIZE 4}{$ALIGN ON}这是好的。枚举是一个简单的Delphi枚举类型。也不错。但是工会和它所包含的记录都是包装好的。这是错误的,导致联合的大小错误。

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

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

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

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

适当的XE6数据点是{$MINENUMSIZE 4}{$ALIGN ON}大小为52的情况。这里我们看到由于一些不可理解的原因,联合消耗了12个字节。我假设您的联盟符合JEDI github repo中的声明。是吗?

从您提供的事实来看,这对我来说就像是一个Delphi XE6编译器错误。旧的德尔菲版本在对齐结构方面非常差。我认为现代版本是正确的,但也许不是。但是,可能会混淆所有这些是您正在使用的标题转换。当然,它似乎对包装感到困惑。我们还没有看到你的所有代码。我只看过最新的github。也许问题出在那里而不是编译器。 @LURD的研究表明,XE6编译器正确地列出了结构。


你应该处理这样的问题的方法是使用MS编译器转到原始头文件。包括标题并使用sizeofoffsetof转储布局。从马的嘴里可以这么说。

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

至于如何继续,一旦你知道正确的布局,它应该很容易说服编译器以相同的方式打印记录。从github repo中的JEDI代码开始,删除packed的所有用法。尝试大小。如果这不起作用,请调查联合的布局。作为最后的手段,你可以手动包装所有东西和垫。如果问题确实在那里,或许与工会这样做就足够了。

更新: LURD的答案似乎表明,删除联合中包含的使用和包含的结构会给出正确的布局。

注意:我没有任何编译器方便所以上面的所有内容都来自我的头脑。我可能在具体细节上犯了错误。但是,我相信使用MS编译器向您展示正确布局的一般建议是解决Win32结构布局的所有疑问的一般建议。有了这个工具,您就可以解决任何这种性质的问题。

答案 1 :(得分:2)

按照David的回答,{$ 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

自D2010扩展RTTI以来,可以制作一个通用程序来列出记录中的所有偏移量。如果有人有兴趣,请在这里展示一个例子。

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;