为什么strcmp的返回值不同?

3

以下是使用gcc编译的C代码:

char *a="a";
char *d="d";
printf("%d\n", strcmp("a", "d"));
printf("%d\n", strcmp(a, "d"));
printf("%d\n", strcmp(a, d));

当我使用-O编译时,输出结果为:

-1
-3
-1

当我没有使用-O编译时,输出结果为:
-1
-3
-3

为什么输出不同,strcmp的代码是什么?
4个回答

5
为什么输出结果不同?
因为唯一重要的是返回值的符号(正、负或零)。strcmp() 不需要返回 +1 或 -1,也不必返回一致的值。我猜测在第一个和第三个情况下,编译器优化了对 strcmp() 的调用,并将 -1 放入返回值的位置。在第二种情况下,我认为实际上调用了该函数。
strcmp 的代码是什么?
从它似乎返回第一个不同字符的字符代码之差这一事实推断,我会说这是 glibc 的 strcmp() 函数:
int
 strcmp (p1, p2)
      const char *p1;
      const char *p2;
 {
   register const unsigned char *s1 = (const unsigned char *) p1;
   register const unsigned char *s2 = (const unsigned char *) p2;
   unsigned char c1, c2;

   do
     {
       c1 = (unsigned char) *s1++;
       c2 = (unsigned char) *s2++;
       if (c1 == '\0')
     return c1 - c2;
     }
   while (c1 == c2);

   return c1 - c2;
 }

编辑:@AndreyT不相信我,所以这是GCC 4.2为我生成的汇编代码(OS X 10.7.5 64位Intel,默认优化级别-无标志):

    .section    __TEXT,__text,regular,pure_instructions
    .globl  _main
    .align  4, 0x90
_main:
Leh_func_begin1:
    pushq   %rbp
Ltmp0:
    movq    %rsp, %rbp
Ltmp1:
    subq    $32, %rsp
Ltmp2:
    leaq    L_.str(%rip), %rax
    movq    %rax, -16(%rbp)
    leaq    L_.str1(%rip), %rax
    movq    %rax, -24(%rbp)
    movl    $-1, %ecx             ; <- THIS!
    xorb    %dl, %dl
    leaq    L_.str2(%rip), %rsi
    movq    %rsi, %rdi
    movl    %ecx, %esi
    movq    %rax, -32(%rbp)
    movb    %dl, %al
    callq   _printf               ; <- no call to `strcmp()` so far!
    movq    -16(%rbp), %rax
    movq    %rax, %rdi
    movq    -32(%rbp), %rsi
    callq   _strcmp               ; <- strcmp()
    movl    %eax, %ecx
    xorb    %dl, %dl
    leaq    L_.str2(%rip), %rdi
    movl    %ecx, %esi
    movb    %dl, %al
    callq   _printf               ; <- printf()
    movq    -16(%rbp), %rax
    movq    -24(%rbp), %rcx
    movq    %rax, %rdi
    movq    %rcx, %rsi
    callq   _strcmp               ; <- strcmp()
    movl    %eax, %ecx
    xorb    %dl, %dl
    leaq    L_.str2(%rip), %rdi
    movl    %ecx, %esi
    movb    %dl, %al
    callq   _printf               ; <- printf()
    movl    $0, -8(%rbp)
    movl    -8(%rbp), %eax
    movl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    addq    $32, %rsp
    popq    %rbp
    ret
Leh_func_end1:

    .section    __TEXT,__cstring,cstring_literals
L_.str:
    .asciz   "a"

L_.str1:
    .asciz   "d"

L_.str2:
    .asciz   "%d\n"

    .section    __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame0:
Lsection_eh_frame:
Leh_frame_common:
Lset0 = Leh_frame_common_end-Leh_frame_common_begin
    .long   Lset0
Leh_frame_common_begin:
    .long   0
    .byte   1
    .asciz   "zR"
    .byte   1
    .byte   120
    .byte   16
    .byte   1
    .byte   16
    .byte   12
    .byte   7
    .byte   8
    .byte   144
    .byte   1
    .align  3
Leh_frame_common_end:
    .globl  _main.eh
_main.eh:
Lset1 = Leh_frame_end1-Leh_frame_begin1
    .long   Lset1
Leh_frame_begin1:
Lset2 = Leh_frame_begin1-Leh_frame_common
    .long   Lset2
Ltmp3:
    .quad   Leh_func_begin1-Ltmp3
Lset3 = Leh_func_end1-Leh_func_begin1
    .quad   Lset3
    .byte   0
    .byte   4
Lset4 = Ltmp0-Leh_func_begin1
    .long   Lset4
    .byte   14
    .byte   16
    .byte   134
    .byte   2
    .byte   4
Lset5 = Ltmp1-Ltmp0
    .long   Lset5
    .byte   13
    .byte   6
    .align  3
Leh_frame_end1:


.subsections_via_symbols

原始源代码如下:

#include <stdio.h>
#include <string.h>

int main()
{
    const char *a = "a";
    const char *d = "d";
    printf("%d\n", strcmp("a", "d"));
    printf("%d\n", strcmp(a, "d"));
    printf("%d\n", strcmp(a, d));

    return 0;
}

以下是它生成的输出(为了更好的证明,附有屏幕截图):

enter image description here


1
好的,“strcmp”是一个非常简单的函数。它所做的任何事情都应该很容易理解。(是的,我在谈论实现细节)。我实际上怀疑OP的实验出了问题。 - AnT stands with Russia
1
@AndreyT 对于我在Mac OS X上使用GCC 4.2,这个程序输出了“-1 -3 -3”,并且我查看了生成的汇编代码,证明了我的想法是正确的(真是个惊喜)。 - user529758
1
@d0rmLife 优化并不添加-3。优化是移除-3(在这种特定情况下是strcmp()的实际返回值),并将其替换为-1(一个相当随意的负数)。 - user529758
1
@d0rmLife 我怀疑这不是交替的问题...这些表达式是不同的;改变它们的顺序,结果也会随之改变。为了知道为什么在某些实现中,strcmp(a, "d") 返回 -3 而另外两个返回 -1,你需要检查优化器。 - Jim Balter
1
“bitter”--从那个来源说出来很有趣。我所说的是“没有我知道的glibc strcmp实现”...显然我不知道这个。即使它来自一个非常晚到派对并且发表了许多错误言论并且通常是一个PITA的人,但新信息和更正仍然受欢迎。 - Jim Balter
显示剩余22条评论

4
C标准允许实现返回任何负值。只要结果符合标准,实现可以对库函数调用进行优化... 因此,实现可以通过生成内联机器指令而不是调用函数来优化像strcmp这样的函数。当参数是常数时,可以实现额外的优化。因此,结果不同的原因是优化器恰好为某些情况生成了不同的代码。符合标准的程序不允许关心返回哪个负值。
编辑:
目前在我的系统上,输出结果为:
-1
-3
-3

以下是编译器生成的代码,这些代码产生了上述结果(使用gcc -S获得):
    movl    $-1, 4(%esp)
    movl    $LC2, (%esp)
    call    _printf
    movl    $LC1, 4(%esp)
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    _strcmp
    movl    %eax, 4(%esp)
    movl    $LC2, (%esp)
    call    _printf
    movl    24(%esp), %eax
    movl    %eax, 4(%esp)
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    _strcmp
    movl    %eax, 4(%esp)

正如你所看到的,只有两个strcmp调用。第一个比较的结果-1是在编译时产生的,因为编译器知道"a"小于"d"。如果我使用-O,它会产生以下代码:

    movl    $-1, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    movl    $-1, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    movl    $-1, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf

2

我遇到了问题

 -1
 -3
 -1

以下是在Linux上使用GCC 4.1.2进行优化编译(-O4)的输出结果。这里是编译器为main生成的代码。

main:
.LFB25:
        subq    $8, %rsp
.LCFI0:
        movl    $-1, %esi
        xorl    %eax, %eax
        movl    $.LC0, %edi
        call    printf
        movzbl  .LC1(%rip), %edx
        movzbl  .LC2(%rip), %eax
        movl    %edx, %esi
        subl    %eax, %esi
        jne     .L2
        movzbl  .LC1+1(%rip), %esi
        movzbl  .LC2+1(%rip), %eax
        subl    %eax, %esi
.L2:
        movl    $.LC0, %edi
        xorl    %eax, %eax
        call    printf
        movl    $-1, %esi
        movl    $.LC0, %edi
        xorl    %eax, %eax
        call    printf
        xorl    %eax, %eax
        addq    $8, %rsp
        ret

这意味着第一个和最后一个比较实际上被优化掉了,而中间的比较实际上是通过减法内在实现的(这就是为什么它产生了-3)。我没有看到这种选择性行为中的任何逻辑,所以它可能只是优化器的怪癖。
顺便说一下,没有优化的情况下,同样的GCC 4.1.2会产生
 -1
 -1
 -1

由于它调用了strcmp,因此产生了输出。在这个标准库中,strcmp是这样实现的:

<strcmp>           mov    (%rdi),%al
<strcmp+2>         cmp    (%rsi),%al
<strcmp+4>         jne    <strcmp+19>
<strcmp+6>         inc    %rdi
<strcmp+9>         inc    %rsi
<strcmp+12>        test   %al,%al
<strcmp+14>        jne    <strcmp>
<strcmp+16>        xor    %eax,%eax
<strcmp+18>        retq
<strcmp+19>        mov    $0x1,%eax
<strcmp+24>        mov    $0xffffffff,%ecx
<strcmp+29>        cmovb  %ecx,%eax
<strcmp+32>        retq

这意味着它是有意实现为返回-10+1,即使可能被视为次优。

这表明可能有三种不同的strcmp()实现在起作用:编译器内实现并在编译时完全计算的比较;由编译器提供但在运行时评估的内置函数;以及在运行时评估的标准库函数。你看到的确切输出取决于每个实现对测试输入生成的输出,以及每个比较使用的实现(这可能取决于优化设置)。 - caf

1

strcmp返回< 0表示字符串不相等。
这表明第二个字符串在第一个不匹配的字符上具有更高的值。确切的值是未指定的
唯一定义的是输出是否为:

  • 零或
  • 正数或
  • 负数

2
那有些偏离问题的重点。这个问题是关于为什么相同的输入字符串会产生不同的结果。虽然结果在形式上是“正确”的,但问题是:为什么strcmp的行为看起来是如此的不确定性? - AnT stands with Russia
2
这没问题,但这是一个现实中的问题。如果一个现实中的编译器对于相同的字符串返回不同的结果,唯一的原因就是它有意地随机化了结果。这在某些“压力测试”编译器中可能是有意义的,但在“正常”的编译器中则不然。所以问题依旧存在。你的纯理论回答是正确的,但在现实生活中没有意义。或者,如果你认为有意义,那么请证明它。 - AnT stands with Russia
1
@AndreyT “如果真实的编译器在处理相同的字符串时返回不同的结果,唯一的原因是它故意随机化结果。”--你是错误的。请看我的回答。 - Jim Balter
1
这是一个现实生活中的问题 - 真正的答案是:生成汇编代码并查看它。它可能因不同版本的gcc和不同的平台而有所不同。 - Jim Balter
3
@JimBalter:确切地说,这个练习是无用的,因为即使在同一个编译器和平台上,你也不能保证值是相同的,从技术上讲(因此实际上),该值可以是任何值,只要它为负数。 - Alok Save
显示剩余9条评论

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