Delphi XE 字节数组索引

Posted

技术标签:

【中文标题】Delphi XE 字节数组索引【英文标题】:Delphi XE byte array index 【发布时间】:2016-07-22 22:02:13 【问题描述】:

我使用像这样的简单循环缓冲区

var
  Values: array [byte] of single;
  ptr: byte;

在这个测试示例中

for ptr:=0 to 10 do Values[Byte(ptr-5)]:=1;

我希望将前 5 个值和后 5 个值设置为 1,但 XE4 编译器生成的代码不正确,它使用 32 位指针数学来计算数组索引:

for ptr:=0 to 10 do Values[Byte(ptr-5)]:=1;
005B94BB C645FB00         mov byte ptr [ebp-$05],$00
005B94BF 33C0             xor eax,eax
005B94C1 8A45FB           mov al,[ebp-$05]
005B94C4 C78485E0FBFFFF0000803F mov [ebp+eax*4-$0420],$3f800000
005B94CF FE45FB           inc byte ptr [ebp-$05]
005B94D2 807DFB0B         cmp byte ptr [ebp-$05],$0b
005B94D6 75E7             jnz $005b94bf

这是我的错误代码吗?操作字节索引的正确方法是什么?

【问题讨论】:

这是代码生成器中的一个错误。它以错误的方式“优化”。看我的回答。 是的,这是一个编译器错误。很好的收获。 是的,确实不错 我不同意。我认为你的代码是错误的。为什么你期望环绕?明确地编写自己的环绕代码。 @DavidHeffernan 我应该吗?如此深入而有意义的讨论在哪里进行:) 【参考方案1】:

问题是:

Byte() 演员表中是否需要换行?

让我们比较一下反汇编和溢出检查的开/关。

$Q+
Project71.dpr.21: for ptr:= 0 to 10 do Values[Byte(ptr-5)]:= 1;
0041D568 33DB             xor ebx,ebx
0041D56A 0FB6C3           movzx eax,bl
0041D56D 83E805           sub eax,$05
0041D570 7105             jno $0041d577
0041D572 E82D8DFEFF       call @IntOver
0041D577 0FB6C0           movzx eax,al
0041D57A C704870000803F   mov [edi+eax*4],$3f800000
0041D581 43               inc ebx
0041D582 80FB0B           cmp bl,$0b
0041D585 75E3             jnz $0041d56a

$Q-
Project71.dpr.21: for ptr:= 0 to 10 do Values[Byte(ptr-5)]:= 1;
0041D566 B30B             mov bl,$0b
0041D568 B808584200       mov eax,$00425808
0041D56D C7000000803F     mov [eax],$3f800000
0041D573 83C004           add eax,$04
0041D576 FECB             dec bl
0041D578 75F3             jnz $0041d56d

使用$Q+ 时,换行有效,而使用$Q- 时,换行不起作用,并且当设置$R+ 时,编译器不会为错误的数组索引生成范围错误。

因此,对我而言,结论是:由于range check on 不会为超出范围的数组索引生成运行时错误,因此需要换行

当溢出检查开启时进行了换行这一事实进一步证明了这一点。


这应该报告为编译器中的错误。

完成: https://quality.embarcadero.com/browse/RSP-15527 "类型转换在数组索引中失败"


注意:@Rudy 在他的回答中给出了解决方法。


附录:

以下代码:

for ptr:= 0 to 10 do WriteLn(Byte(ptr-5));

生成:

251
252
253
254
255
0
1
2
3
4
5

适用于范围/溢出检查的所有组合。

同样,Values[Byte(-1)] := 1; 将 1 分配给所有编译器选项的 Values[255]。


Value Typecasts 的文档说:

结果值是通过括号中的表达式转换得到的。如果指定类型的大小与表达式的大小不同,这可能涉及截断或扩展。表达式的符号始终保留。

【讨论】:

我也测试过演员表。如果它是单独完成的,例如“I := Byte(ptr - 5); Values[I] := 1.0;”,然后一切正常。只有索引中的转换出错了。【参考方案2】:

我的代码是用 Delphi 10.1 Berlin 编写的,但结果似乎是一样的。

让我们稍微扩展一下你的小代码:

procedure Test;
var
  Values: array[Byte] of Single;
  Ptr: byte;
begin
  Values[0] := 1.0;
  for Ptr := 0 to 10 do
    Values[Byte(Ptr - 5)] := 1.0;
end;

这在 CPU 视图中给出了以下代码:

Project80.dpr.15: Values[0] := 1.0;
0041A1DD C785FCFBFFFF0000803F mov [ebp-$00000404],$3f800000
Project80.dpr.16: for Ptr := 0 to 10 do
0041A1E7 C645FF00         mov byte ptr [ebp-$01],$00
Project80.dpr.17: Values[Byte(Ptr-5)] := 1.0;
0041A1EB 33C0             xor eax,eax
0041A1ED 8A45FF           mov al,[ebp-$01]
0041A1F0 C78485E8FBFFFF0000803F mov [ebp+eax*4-$0418],$3f800000
0041A1FB FE45FF           inc byte ptr [ebp-$01]
Project80.dpr.16: for Ptr := 0 to 10 do
0041A1FE 807DFF0B         cmp byte ptr [ebp-$01],$0b
0041A202 75E7             jnz $0041a1eb

如我们所见,数组的第一个元素位于[ebp-$00000404],因此[ebp+eax*4-$0418] 确实低于数组(对于值 0..4)。

这对我来说似乎是一个错误,因为对于 Ptr = 0Byte(Ptr - 5) 应该环绕到 $FB。生成的代码应该是这样的:

    mov byte ptr [ebp-$01],$00
    xor eax,eax
@loop:
    mov al,[ebp-$01]
    sub al,5                        // Byte(Ptr - 5)
    mov [ebp+4*eax-$0404],$3f800000 // al = $FB, $FC, $FD, $FE, $FF, 00, etc..
    inc byte ptr [ebp-$01]
    cmp byte ptr [ebp-$01],$0b
    jnz @loop

很好的发现!

不过有一个解决方法:

    Values[Byte(Ptr - 5) + 0] := 1.0;

这会产生:

Project80.dpr.19: Values[Byte(Ptr - 5) + 0] := 1.0;
0040F16B 8A45FF           mov al,[ebp-$01]
0040F16E 2C05             sub al,$05
0040F170 25FF000000       and eax,$000000ff
0040F175 C78485FCFBFFFF0000803F mov [ebp+eax*4-$0404],$3f800000

这很好用,虽然and eax,$000000ff 对我来说似乎没有必要。

FWIW,我还查看了优化后生成的代码。在 XE 和 Berlin 中,错误也存在,解决方法也有效。

【讨论】:

"and eax,$000000ff" 是必填项,否则会出现缓冲区溢出,肯定写在Values[-5] Values[-4] Values[-3] Values[-2]Values[-1],这是不正确的。 @Arnaud:不,这不是强制性的。 EAX 的高 24 位为 0AL 自动回绕。所以SUB AL,5 会换行,比如$02$FD。这意味着整个EAX 的值是$000000FD。这不会溢出/下溢。更改AL 不会影响EAX 的其余部分。 @Arnaud:仅在 64 位中,更改像 EAX 这样的寄存器也会影响 RAX 的高 32 位。 否则,寻址子寄存器不会影响整个寄存器的其余部分,不会影响 32 位,也不会影响 64 位 Intel/AMD,至少如果它是通用寄存器的话。 你说得对,我忽略了 asm,并假设生成的 asm 是“sub eax,5”,而实际上它是“sub al,5”。【参考方案3】:

听起来像是编译器的意外行为。但我从不认为使用byte() 转换整数总是会围绕$ff 进行四舍五入。在大多数情况下,它确实如此,例如如果您在变量之间分配值,但在某些情况下它没有 - 正如您所发现的那样。所以我永远不会在数组索引计算中使用这个byte() 表达式。

我一直观察到使用byte 变量是不值得的,您应该使用普通的integer(或NativeInt),以便它与CPU 寄存器匹配,然后不要假设任何复杂的舍入.

在所有情况下,我宁愿明确表示 255 舍入,如下所示:

procedure test;
var
  Values: array [byte] of single;
  ptr: integer;
begin
  for ptr:=0 to 10 do Values[(ptr-5) and high(Values)]:=1;
end;

如你所见,我做了一些修改:

for 循环索引定义为整数,以使用CPU 寄存器; 使用and 操作进行快速二进制舍入(写入(ptr-5) mod 256慢得多); 使用high(Values) 而不是固定的$ff 常量,它指示此舍入的来源。

那么生成的代码就快速优化了:

TestAll.dpr.114: begin
0064810C 81C400FCFFFF     add esp,$fffffc00
TestAll.dpr.115: for ptr:=0 to 10 do Values[(ptr-5) and high(Values)]:=1;
00648112 33C0             xor eax,eax
00648114 8BD0             mov edx,eax
00648116 83EA05           sub edx,$05
00648119 81E2FF000000     and edx,$000000ff
0064811F C704940000803F   mov [esp+edx*4],$3f800000
00648126 40               inc eax
00648127 83F80B           cmp eax,$0b
0064812A 75E8             jnz -$18
TestAll.dpr.116: end;
0064812C 81C400040000     add esp,$00000400
00648132 C3               ret 

【讨论】:

这是一个错误,而不是意外行为。 直到有人提出文档,否则我认为尚不清楚它是否是一个错误。 @DavidHeffernan,您的意思是,例如Byte(1000) 的结果可能不是 1000 的低字节吗? @DavidHeffernan, from Value Typecasts: The resulting value is obtained by converting the expression in parentheses. This may involve truncation or extension if the size of the specified type differs from that of the expression. The expression's sign is always preserved. 你的意思是我上面例子中的截断可能意味着除了低字节之外的其他东西? @LURD 我认为这是文档的相关部分。虽然它是不精确的。这是一个慷慨的判决。 始终保留表达式的符号。 当负值转换为无符号时,这意味着什么。这可能是一个错误,但文档并没有太大帮助。当然,行为取决于上下文是很奇怪的。用作索引时不同。上下文可以改变表达式的含义,这是 Delphi 的一个可怕的设计缺陷。

以上是关于Delphi XE 字节数组索引的主要内容,如果未能解决你的问题,请参考以下文章

DELPHI如何读取一个图片,保存到字节数组中

将 ansichar 数组复制到 char delphi 10.2 数组

C# Struct 到字节数组的编组方式与 Delphi 中的打包记录相同

DELPHI里byte型数组取值与比较

如何在 Java 中初始化字节数组?

从 C++ 中的字节数组中提取非零索引的最快方法是啥