【问题标题】:Two printfs print the same string differently两个 printfs 以不同的方式打印相同的字符串
【发布时间】:2015-03-16 12:12:13
【问题描述】:

我正在尝试创建一个处理大整数运算的库。大整数存储在结构中:

typedef struct BigInt BigInt;
struct BigInt
{
    uint32_t size;
    uint32_t *data;
};

第一个成员是一个包含数字长度的 uint32_t,第二个成员是指向实际数字数据的指针(存储在二进制补码中)。我写了一个简单的 toHex(BigInt *a) 函数,它分配内存,将大整数的十六进制值打印到字符串,并返回地址。

在我的主循环中,我有以下内容:

int main(int argc, char *argv[])
{
    char *ap, *bp;
    BigInt *a = fromUInt32(0x7fffffff), *b = fromUInt32(1), *c = fromUInt32(0x80000000);
    _add(a, b);
    ap = toHex(a);
    bp = toHex(c);
    printf("%s\n", ap);
    printf("%s\n%s\n", ap, bp);
    printf("%s\n%s\n", ap, bp);
    free(ap);
    free(bp);
    deleteBigInt(a);
    deleteBigInt(b);
    deleteBigInt(c);
}

奇怪的是,它会打印出来

0000000080000000
0
0000000080000000
0000000080000000
0000000080000000

因此,第二个 printf 语句为 ap 打印的内容与第一个和第三个 printf 语句不同。似乎第一个 printf 语句是正确的,而第二个则搞砸了。我已经使用 GDB 单步执行了我的代码,在评估了 toHex 之后,ap 指向字符串“0000000080000000”,以空指针终止。

我完全感到困惑。据我所知,可能性是:
1. 由于一些奇怪的原因,我遇到了未定义的行为。
2.在_add中我调用了一个用x86汇编代码编写的例程,其中可能有错误(但我确实遵守了GCC的调用约定,保留了esi、edi、ebx、ebp和esp)。
3. printf有一个bug,看起来不太可能。

此外,由于不释放 toHex 分配的内存,我也有明显的“内存泄漏”(引用是因为关于内存泄漏究竟是什么的观点似乎不同),但这应该无关紧要。

我的 toHex 函数是 Sourav Ghosh 请求的,如下:

char numToHex[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
char *toHex(BigInt *a)
{
    char *result, *ptr;
    // allocate enough space for 8 characters for each uint32_t and 1 terminating 0
    ptr = result = malloc(a->size * 8 + 1);
    // loop over the uint32_t's stored in a->data
    // (there are a->size of them)
    for (uint32_t i = 0; i < a->size; i++)
        // parse 8 blocks of 4 bits
        for (uint32_t j = 0; j < 8; j++)
            // grab the right bits and convert them to a hex digit
            *(ptr++) = numToHex[(a->data[i] >> ((7 - j) * 4)) & 0xf];
    // add a terminating zero byte
    *ptr = 0;
    return result;
}

我在~100 lines of C + ~70 lines of assembly 的程序中隔离了这种奇怪的行为。编译可以用

nasm -f elf -s <AssemblyName>.asm
gcc <CFile>.c <AssemblyName>.o -o <OutputProgram> -m32 -std=c99 -g

代码未注释,适用于想要自己检查行为的人。

编辑:Jan Spurny 和 Matt McNabb 敦促我使用 Valgrind。 Valgrind 说: 在 0x40A5685 读取大小为 1 的无效:vfprintf (vfprintf.c:1655) by 0x40AA7FE: printf (printf.c:34) by 0x4075904: (below main) (libc-start.c:260) 地址 0x42121af 在 a 之前 1 个字节大小为 17 的块分配在 0x40299D8:malloc(在 /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so 中)按 0x804887D:toHex(weird.c:107)按 0x8048565:main(weird.c:30)

但这没有任何意义,因为我在 toHex 中将结果设置为 malloc,之后没有更改任何内容。我现在敢打赌,汇编函数中的某些寄存器已损坏。 Edit2:检查 GDB 后,我可以看到没有寄存器损坏。我还是一头雾水。

【问题讨论】:

  • 你能告诉我们toHex()吗?
  • 你也可以尝试在 valgrind 中运行它..
  • 我也会评论_addHere are the calling conventions。我怀疑您正在编写 32 位 x86 代码,对吗?然后我猜你正在使用cdecl,其中 all 参数进入堆栈,只有 eax、ecx、edx 是调用者保存的; 所有其他寄存器都是callee-save。返回值进入 EAX 用于整数/指针和 ST0 用于浮点数
  • 我唯一能想到的是toHex 中的malloc 大小不正确,因此第二次调用中的malloc(将在第二次调用之前评估) 987654335@ 被调用)踩踏分配给第一次调用的内存。 malloc 分配的区域往往彼此靠近。在这种情况下,我看不出有什么问题,但这可能值得仔细检查。
  • 问题可能出在您的汇编代码中。尝试在 C 中实现addHelper(),看看程序的行为是否发生了变化。

标签: c linux pointers gcc i386


【解决方案1】:

reduce 函数有一个错误:

while (i < a->size && !(a->data[i])) i++;
if (a->data[i] & SIGNBIT) i--;

如果满足i &lt; a-&gt;size 条件,则a-&gt;data[i] 访问越界,导致未定义的行为。 reduce的另一个分支也有同样的问题


_add 函数中有一个错误(尽管在您的测试用例中没有触发):

void *k = realloc(a->data, b->size * 4);
memmove((void *)(a->data + displacement), (void *)a->data, a->size * 4);
// ....other code using `a->data`

realloc 之后,a-&gt;data 变得不确定,因此使用它会导致未定义的行为。这可以解释您的症状,因为未来的分配可能会重复使用 a-&gt;data 仍指向的相同已释放块。

也许您的意思是在此之后还有一行 a-&gt;data = k;


为了在调试代码方面获得很好的帮助,如果您可以执行以下操作,那就太好了:

  • 检查所有*alloc-family 函数的结果,如果返回NULL,则退出。否则你会得到未定义的行为(期待一个段错误是不可靠的)。
  • 用 C 重写汇编函数。出于多种原因(调试、代码可移植性、优化),这是一个好主意。甚至可能证明 gcc -O3 生成的代码比您的手写版本更快;这就是编译器所擅长的。
  • 检查调用newAddress 的结果以检查它是否实际返回了您在测试用例中所期望的结果。

【讨论】:

  • 这确实是一个bug,但是这段代码并没有在程序中执行。将整个 if (a->size size) 注释掉,然后再次编译和运行程序仍然会产生相同的输出。不过是个好地方,很抱歉这个错误。如果我的程序集调用 realloc,它返回 realloc 的返回值,否则返回零。然后在 _add 我做: uint32_t *newAddress = addHelper(...); if (newAddress) a->data = newAddress;我认为这应该可行。不过好点,我认为错误确实在程序集中,
  • @MonKeePoo 已更新。 reduce 中存在错误,尽管 IDK 如果在您的测试用例中被调用。错误可能存在的明显位置是在汇编代码中。如果您不想用 C 重写它,那么我强烈建议您尝试自己调试它:在显示 CPU 视图的调试器中逐步执行,并确保一切都完全符合您的预期
  • 您显然关心性能,所以我可以建议重新设计,以便 BigInt 的数据元素按小端顺序排列。然后要“溢出”和“减少”,您只需附加到末尾,或减少 size ;你不需要做一个memmove
  • 是的,很好的建议。我最终可能会制作第二个版本的例程。我一定会考虑的。
  • 使用__builtin_add_overflow怎么样?我相信它需要一个(非常)新版本的 gcc,但有时内置的性能比人工编写的 asm 更好。
【解决方案2】:

我在 forum.osdev.org 上创建了a thread(那里有一些非常聪明的人),jnc100 通知我 ABI 希望在调用函数时清除方向标志。我在汇编例程中设置了方向标志(在_add中调用),确实在汇编例程中清除可以解决问题。

【讨论】:

    猜你喜欢
    • 2023-03-19
    • 2021-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多