整数溢出不一致

8
请原谅我如果这个问题之前已经被提出过。我寻找了类似问题的答案,但是仍然对我的问题感到困惑。所以我还是会提出这个问题。 我正在使用一个名为libexif的C库处理图像数据。我在我的Linux桌面和MIPS板上运行我的应用程序(它使用此库)。 对于特定的图像文件,当我尝试获取创建时间时,我得到了一个错误/无效值。进一步调试后,我发现对于这个特定的图像文件,我没有像预期那样获得标签(EXIF_TAG_DATE_TIME)。
这个库有几个实用函数。大多数函数的结构如下:
int16_t 
exif_get_sshort (const unsigned char *buf, ExifByteOrder order)
{
    if (!buf) return 0;
        switch (order) {
        case EXIF_BYTE_ORDER_MOTOROLA:
                return ((buf[0] << 8) | buf[1]);
        case EXIF_BYTE_ORDER_INTEL:
                return ((buf[1] << 8) | buf[0]);
        }

    /* Won't be reached */
    return (0);
}

uint16_t
exif_get_short (const unsigned char *buf, ExifByteOrder order)
{
    return (exif_get_sshort (buf, order) & 0xffff);
}

当库试图调查原始数据中标签的存在时,它会调用exif_get_short()并将返回值分配给一个枚举类型(int)的变量。
在错误情况下,exif_get_short()应该返回无符号值(34687),但返回了一个负数(-30871),这混乱了从图像数据中提取整个标签。
34687超出了最大可表示int16_t值的范围。因此导致溢出。当我对代码进行轻微修改后,一切似乎都正常工作了。
uint16_t
exif_get_short (const unsigned char *buf, ExifByteOrder order)
{
    int temp = (exif_get_sshort (buf, order) & 0xffff);
        return temp;
}

但由于这是一个相当稳定的库,已经使用了相当长的时间,所以我认为可能我在这里漏掉了什么。此外,这也是其他实用函数的代码结构的一般方式。例如:exif_get_long()调用exif_get_slong()。然后我将不得不更改所有实用程序函数。
让我困惑的是,当我在我的Linux桌面上运行这段代码时,对于错误文件,我看不到任何问题,原始库代码可以正常工作。这使我相信,也许UINT16_MAX和INT16_MAX宏在我的桌面和MIPS板上具有不同的值。但不幸的是,情况并非如此。两者在板子和桌面上打印出相同的值。如果这段代码失败,它也应该在我的桌面上失败。
我在这里错过了什么?任何提示都将不胜感激。
编辑: 调用exif_get_short()的代码如下:
ExifTag tag;
...
tag = exif_get_short (d + offset + 12 * i, data->priv->order);
switch (tag) {
...
...

ExifTag的类型如下:

typedef enum {
    EXIF_TAG_GPS_VERSION_ID             = 0x0000,
EXIF_TAG_INTEROPERABILITY_INDEX     = 0x0001,
    ...
    ...
    }ExifTag ;

正在使用的交叉编译器是mipsisa32r2el-timesys-linux-gnu-gcc。
CFLAGS        = -pipe -mips32r2 -mtune=74kc -mdspr2 -Werror -O3 -Wall -W -D_REENTRANT -fPIC $(DEFINES)

我在Qt中使用libexif - Qt媒体中心(实际上libexif是随Qt媒体中心一起提供的)。
编辑2:一些额外的观察: 我注意到了一些奇怪的事情。我在exif_get_short()中放置了打印语句。就在返回之前。
printf("return_value %d\n %u\n",exif_get_sshort (buf, order) & 0xffff, exif_get_sshort (buf, order) & 0xffff);
return (exif_get_sshort (buf, order) & 0xffff);

我看到以下输出:

return_value 34665 34665

然后我还在调用exif_get_short()的代码中插入了打印语句。

....
tag = exif_get_short (d + offset + 12 * i, data->priv->order);
printf("TAG %d %u\n",tag,tag);

我看到以下输出: TAG -30871 4294936425
编辑3:发布在MIPS板上拍摄的exif_get_short()和exif_get_sshort()汇编代码。
        .file   1 "exif-utils.c"
    .section .mdebug.abi32
    .previous
    .gnu_attribute 4, 1
    .abicalls
    .text
    .align  2
    .globl  exif_get_sshort
    .ent    exif_get_sshort
    .type   exif_get_sshort, @function
exif_get_sshort:
    .set    nomips16
    .frame  $sp,0,$31       # vars= 0, regs= 0/0, args= 0, gp= 0
    .mask   0x00000000,0
    .fmask  0x00000000,0
    .set    noreorder
    .set    nomacro

    beq $4,$0,$L2
    nop

    beq $5,$0,$L3
    nop

    li  $2,1            # 0x1
    beq $5,$2,$L8
    nop

$L2:

    j   $31
    move    $2,$0

$L3:

    lbu $2,0($4)
    lbu $3,1($4)
    sll $2,$2,8
    or  $2,$2,$3
    j   $31
    seh $2,$2

$L8:

    lbu $2,1($4)
    lbu $3,0($4)
    sll $2,$2,8
    or  $2,$2,$3
    j   $31
    seh $2,$2

    .set    macro
    .set    reorder
    .end    exif_get_sshort
    .align  2
    .globl  exif_get_short
    .ent    exif_get_short
    .type   exif_get_short, @function

exif_get_short:

    .set    nomips16
    .frame  $sp,0,$31       # vars= 0, regs= 0/0, args= 0, gp= 0
    .mask   0x00000000,0
    .fmask  0x00000000,0
    .set    noreorder
    .cpload $25
    .set    nomacro

    lw  $25,%call16(exif_get_sshort)($28)
    jr  $25
    nop

    .set    macro
    .set    reorder
    .end    exif_get_short

为了完整起见,这是我从我的Linux机器中取出的ASM代码。

    .file   "exif-utils.c"
    .text
    .p2align 4,,15
    .globl  exif_get_sshort
    .type   exif_get_sshort, @function

exif_get_sshort:

.LFB1:

        .cfi_startproc
    xorl    %eax, %eax
    testq   %rdi, %rdi
    je  .L2
    testl   %esi, %esi
    jne .L8
    movzbl  (%rdi), %edx
    movzbl  1(%rdi), %eax
    sall    $8, %edx
    orl %edx, %eax
    ret
    .p2align 4,,10
    .p2align 3

.L8:
    cmpl    $1, %esi
    jne .L2
    movzbl  1(%rdi), %edx
    movzbl  (%rdi), %eax
    sall    $8, %edx
    orl %edx, %eax

.L2:
    rep
    ret
    .cfi_endproc

.LFE1:
    .size   exif_get_sshort, .-exif_get_sshort
    .p2align 4,,15
    .globl  exif_get_short
    .type   exif_get_short, @function

exif_get_short:

.LFB2:
    .cfi_startproc
    jmp exif_get_sshort@PLT
    .cfi_endproc
.LFE2:
    .size   exif_get_short, .-exif_get_short

编辑4:希望这是我的最后更新 :-) 使用编译器选项设置为-O1的ASM代码

exif_get_short:

.set    nomips16
.frame  $sp,32,$31      # vars= 0, regs= 1/0, args= 16, gp= 8
.mask   0x80000000,-4
.fmask  0x00000000,0
.set    noreorder
.cpload $25
.set    nomacro

addiu   $sp,$sp,-32
sw  $31,28($sp)
.cprestore  16
lw  $25,%call16(exif_get_sshort)($28)
jalr    $25
nop

lw  $28,16($sp)
andi    $2,$2,0xffff
lw  $31,28($sp)
j   $31
addiu   $sp,$sp,32

.set    macro
.set    reorder
.end    exif_get_short

8
@chris说:在移位操作中,无符号字符型会被提升为整型 - 这不会产生任何未定义的行为。 - Michael Burr
1
你能展示一下调用exif_get_short()的代码吗? 你所做的修改不应该改变exif_get_short()的行为,而且exif_get_short()不可能返回负数,因为它返回的是无符号类型(除非定义uint16_t混乱了)。但是,调用者对返回值可能会做一些错误的操作。另外,在针对MIPS目标时,你使用的编译器和编译时选项是什么? - Michael Burr
1
看看调试器上发生了什么? - Kirill Kobelev
1
@MichaelBurr:exif_get_short()函数即使原型禁止它这样做,仍然可能返回负数。至少这是我从这里收集到的信息:[链接](http://stackoverflow.com/questions/7618088/function-of-type-unsigned-int-returns-negative-number) - Spottsworth
3
这个问题的答案意思是,使用强制转换或隐式转换将负整数值转换为无符号类型并不一定会给你负数的绝对值,实际上很少能这样做到。 - Michael Burr
显示剩余9条评论
2个回答

4
MIPS汇编代码表明(虽然我不是MIPS汇编的专家,所以有很大的可能性我遗漏了一些内容或者存在错误),exif_get_short()函数只是exif_get_sshort()函数的别名。 exif_get_short()函数所做的就是跳转到exif_get_sshort()函数的地址。 exif_get_sshort()函数将其返回的16位值符号扩展为用于返回的完整32位寄存器。这没有任何问题 - 实际上这可能是MIPS ABI指定的(我不确定)。
然而,由于exif_get_short()函数只是跳转到exif_get_sshort()函数,它没有机会清除寄存器的高16位。
因此,当从缓冲区返回16位值0x8769时(无论是从exif_get_sshort()还是exif_get_short()),用于返回函数结果的$2寄存器包含0xffff8769,其可以有以下解释:
  • 作为32位signed int:-30871
  • 作为32位unsigned int:4294936425
  • 作为16位有符号int16_t:-30871
  • 作为16位无符号uint16_t:34665
如果编译器应该确保$2返回寄存器对于uint16_t返回类型具有设置为零的顶部16位,则它在发出exif_get_short()的代码中存在错误 - 它应该调用exif_get_sshort()并在返回之前清除$2的上半部分。
从您所看到的行为描述来看,调用exif_get_short()的代码期望$2返回值寄存器将具有已清除的高16位,以便可以使用整个32位寄存器作为16位uint16_t值。

我不确定MIPS ABI具体规定了什么(但我猜测它规定了exif_get_short()应该清除$2寄存器的高16位),但似乎存在一个代码生成错误,即exif_get_short()在返回之前没有确保$2完全正确,或者调用exif_get_short()的函数假设只有16位是有效的,而实际上可能只有32位中的一部分是有效的。


非常感谢您提供清晰的想法。我希望我可以投多次赞。正如您所怀疑的那样,它确实是这样的。最终通过将编译器优化选项更改为-O1来解决了问题。只是为了完整起见,我已经使用-O1优化选项更新了我的OP,并附上了ASM代码。 - Spottsworth

0

这里存在多个问题,我不知道从哪里开始说起。看看下面的代码:

  • 从缓冲区读取了 unsigned chars。
  • 将它们赋值给了 exif_get_sshort 中的 signed int16_t
  • 将此赋值给了 exif_get_short 中的 unsigned uint16_t
  • 最后将其分配给一个类型为 signed intenum

我要说,它能正常工作简直是个奇迹。

首先,从 chars 转换到 int16_t 是使用值而非表示进行的:

return ((buf[0] << 8) | buf[1]);

这会让你陷入未定义行为的深渊,特别是当结果实际上是负数时。此外,它只在实现的有符号整数表示与文件格式中使用的表示相同时才有效(我猜是二进制补码)。对于一的补码和符号-大小表示法,它将失败。因此,请检查MIPS实现的情况。

更好的方法是反过来:从缓冲区分配两个字符到uint16_t,这将是明确定义的操作,并使用它返回int16_t。如果需要,您可以进一步纠正不同表示法的值。

此外,在这里:

if (!buf) return 0;

0 是一个非常糟糕的返回值选择,因为它是一个有效的枚举常量:

EXIF_TAG_GPS_VERSION_ID             = 0x0000,

如果这被期望成为无效的默认值,那么应该返回常量而不是魔数。虽然这似乎是一个通用的函数来返回一个 int16_t,因此在这里应该使用一些其他的错误机制。
对于您的具体问题,好吧,遵循在您的MIPS实现中有符号和无符号之间的转换流程,包括默认的提升,并检查所有中间值以找到它断开的点。您的MIPS使用32位整数,而不是16位,对吗?检查 INT_MAX 和 UINT_MAX。

1
据我所知,return ((buf[0] << 8) | buf[1]); 不应该是未定义行为。评估 ((buf[0] << 8) | buf[1]) 所得到的值将在 0..65535 范围内。其中一半的值不能被 int16_t 表示,但这不是未定义行为 - C99 6.3.1.3 规定它应该是实现定义的,GCC 文档中指出:“对于转换为宽度 N 的类型,该值模 2^N 减少以使其在类型范围内;不引发信号。” GCC 也仅支持二进制补码表示。(http://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html) - Michael Burr
1
@Michael Burr,感谢您提供的信息。然而,虽然它可能在gcc上运行良好,但链接的libexif页面上的第一句话是:“是用纯可移植C语言编写的库。”他们肯定使用了与我不同的“纯可移植C语言”定义。 - Secure
正如@Michael向我说明的那样,将一个16位无符号大值强制转换为16位有符号值不是整数溢出,因此也不是未定义行为。 这是“实现定义”的行为,GCC实际上已经定义了。 因此,尽管exif不是“纯粹,可移植的C”,但代码的编写应该在所有平台上对于GCC都没有问题。老实说,这看起来像是编译器的错误。 - Nemo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接