【问题标题】:strcat with char pointer to a string literalstrcat 带有指向字符串文字的 char 指针
【发布时间】:2019-01-15 16:25:48
【问题描述】:

只是试图理解最近一次采访中询问的以下代码。

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

int main() {
    char *ptr = "Linux";
    char a[] = "Solaris";
    strcat(a, ptr);
    printf("%s\n", ptr);
    printf("%s\n", a);
    return 0;
}

执行跟踪:

gcc -Wall -g prog.c
gdb a.out

(gdb) p ptr
$15 = 0x400624 "Linux"
(gdb) p a+1
$20 = 0x7fffffffe7f1 "olarisLinux"
**(gdb) p a
$21 = "SolarisL"**
**(gdb) p a+0
$22 = 0x7fffffffe7f0 "SolarisLinux"**
(gdb)
$23 = 0x7fffffffe7f0 "SolarisLinux"
**(gdb) p ptr
$24 = 0x78756e69 <error: Cannot access memory at address 0x78756e69>
(gdb)**

我有几个问题:

  1. strcat 是否会从原始位置删除字符串文字,因为访问 ptr 会导致分段错误?

  2. 为什么 gdb 中的 p a 没有给出正确的输出,而 p a+0 显示 "SolarisLinux"

【问题讨论】:

  • char a[] = "Solaris"; 更改为例如char a[50] = "Solaris";,否则附加字符串将没有空间,您将获得UB。
  • 感谢您的 cmets,基本上知道如何使 prog 工作,但只是在寻找它为什么会这样。
  • 这是未定义的行为,因此它没有以特定方式表现。这次它用“inux”破坏了ptr 的值。下次它可能会导致你的宠物爆炸。最好避免编写不起作用的代码,而专注于编写起作用的代码。
  • 每个帖子只问一个问题。为什么一个程序以某种方式运行与为什么 GDB 显示一些输出是完全不同的问题。

标签: c string-literals strcat


【解决方案1】:

如果我理解您的问题是正确的,您就会知道由于a 无法保存与“Linux”连接的字符串“Solaris”,该程序具有未定义的行为。

因此,您要寻找的答案不是“这是未定义的行为”,而是:

为什么会这样

在处理未定义的行为时,我们无法对正在发生的事情给出一般解释。它可能在不同的系统上做不同的事情,或者为不同的编译器(或编译器版本)做不同的事情等等。

因此,人们常说,试图解释具有未定义行为的程序中正在发生的事情是没有意义的。嗯 - 这是正确的。

但是 - 有时您可以找到特定系统的解释 - 请记住,它是特定于您的系统的,绝不是通用的。

所以我更改了您的代码以添加一些调试打印:

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

int main()
{
    char *ptr = "Linux";
    char a[] = "Solaris";
    printf("   a = %p\n", (void*)a);
    printf("&ptr = %p\n", (void*)&ptr);
    printf(" ptr = %p\n", (void*)ptr);

    // Print the data that ptr holds
    unsigned char* p = (unsigned char*)&ptr;

    printf("\nBefore strcat\n");
    printf("  a:\n");
    for (int i = 0; i < 8; ++i) printf("%02x ", *(a+i));
    printf("\n");

    printf("  ptr:\n");
    for (int i = 0; i < 8; ++i) printf("%02x ", *(p+i));
    printf("\n");

    strcat(a,ptr);

    printf("\nAfter strcat\n");
    printf("  a:\n");
    for (int i = 0; i < 8; ++i) printf("%02x ", *(a+i));
    printf("\n");

    printf("  ptr:\n");
    for (int i = 0; i < 8; ++i) printf("%02x ", *(p+i));
    printf("\n\n");

    printf("%s\n", a);

    printf("%s\n", ptr);

    return 0;
}

在我的系统上,这会生成:

   a = 0x7ffff3ce5050
&ptr = 0x7ffff3ce5058
 ptr = 0x400820

Before strcat
  a:
53 6f 6c 61 72 69 73 00
  ptr:
20 08 40 00 00 00 00 00

After strcat
  a:
53 6f 6c 61 72 69 73 4c
  ptr:
69 6e 75 78 00 00 00 00

SolarisLinux
Segmentation fault

这里的输出是添加了一些 cmets:

   a = 0x7ffff3ce5050   // The address where the array a istored
&ptr = 0x7ffff3ce5058   // The address where ptr is stored. Notice 8 higher than a
 ptr = 0x400820         // The value of ptr

Before strcat
  a:
53 6f 6c 61 72 69 73 00 // Hex dump of a gives Solaris\0
  ptr:
20 08 40 00 00 00 00 00 // Hex dump of ptr is the value 0x0000000000400820 (little endian system)

// Here strcat is executed

After strcat
  a:
53 6f 6c 61 72 69 73 4c // Hex dump of a gives SolarisL
  ptr:
69 6e 75 78 00 00 00 00 // Ups.. ptr has changed! It's not a valid pointer value anymore
                        // As a string it is inux\0

SolarisLinux            // print a
Segmentation fault      // print ptr crashes because ptr doesn't hold a valid pointer value

所以在我的系统上的解释是:

a 位于内存中 ptr 之前,因此当strcat 写入超出a 的范围时,它实际上会覆盖ptr 的值。因此,当尝试使用ptr 作为有效指针时,程序会崩溃。

所以对于您的具体问题:

1) strcat 是否会从原始位置删除字符串文字,因为访问 ptr 会导致分段错误。

没有。 ptr 的值已被覆盖。 sring 字面量很可能未被触及

2)为什么 gdb 中的 p a 没有给出正确的 o/p 而 p a+0 显示“SolarisLinux”。

这是一个猜测 - 仅此而已。我的猜测是 gdb 知道 a 是 8 个字节,所以直接打印 a 只打印 8 个字节。在打印 a + 0 时,我的猜测是 gdb 将 a + 0 视为指针(因此无法知道对象大小),因此 gdb 会继续打印,直到看到零终止。

【讨论】:

    【解决方案2】:

    好吧,这里我们有一个指针错误。

    我会尽量让人理解:

    常量字符串(如"Linux""Solaris")存储在程序的特定内存区域中。对于您的程序,在其他字符串(例如错误消息)中,应该有一个区域:"Linux\0Solaris\0%s\n\0%s\n\0"

    当你这样做时:

    char *ptr = "Linux";
    char a[] = "Solaris";
    

    您将 ptr 分配给 'L' char 的地址,然后在堆栈上为您分配 8 * sizeof(char) 内存,然后将 "Solaris\0" 复制到该地址。

    当你连接这两个字符串时,因为你从来没有创建一个新的内存空间(例如mallocchar str[50]),所以要求strcat 在为你的函数使用而保留的堆栈内存结束之后写入。这是导致堆栈溢出的编程错误。

    这里 gdb 尝试最好显示字符串。

    (gdb) p ptr
    $15 = 0x400624 "Linux"
    

    指向静态字符串区域的指针,正确显示

    (gdb) p a+1
    $20 = 0x7fffffffe7f1 "olarisLinux"
    

    按预期显示的堆栈指针

    (gdb) p a
    $21 = "SolarisL"
    

    指向一个 8 字符长度区域的指针,gdb 知道大小,显示你的第一个 8 字符。

    (gdb) p a+0
    $22 = 0x7fffffffe7f0 "SolarisLinux"
    

    指向堆栈的指针(gdb 不知道大小,因为您进行指针运算)

    (gdb) p ptr
    $24 = 0x78756e69 <error: Cannot access memory at address 0x78756e69>
    

    这个很棘手。看到这里,ptr 的地址和你第一次打印的时候不一样。有可能您在某个时候覆盖了 ptr 值(因为您在堆栈上的某个地方写了不应该写的)。

    1) strcat 是否会从原始位置删除字符串文字,因为访问 ptr 会导致分段错误。

    不行,不能覆盖原来的位置。

    2)为什么 gdb 中的 p a 没有给出正确的 o/p 而 p a+0 显示“SolarisLinux”。

    这是一个调试器,它是为了避免某种类型的错误而编写的,所以当它可以时,他只读取应该是红色的。

    【讨论】:

    • 实际上,char *ptrchar a[] 是完全相同的东西 不,它们绝对不是“完全相同的东西”。 char *ptr = "Linux"; 创建一个名为 ptr 的指向字符变量的指针,并使用字符串文字 "Linux" 的地址对其进行初始化。尝试修改 ptr 指向的内存可能会失败。但是char a[] = "Solaris"; 创建了一个实际的char 数组,并使用字符"Solaris\0" 进行了初始化。这个字符串可以安全地修改,比如a[0]='s';
    • 你是对的,我编辑了我的帖子,这些不是正确的词。
    • @PierrePodevin 不,不可能更改a 的地址以使其指向另一个数组。如果您希望能够更改某物指向的位置,这就是您使用数组指针的原因。在任何情况下,a 都不是指针,说它“指向”任何地方都是不正确或严重误导的。这些都是基本点;如果您进行搜索,您会发现该问题已经在 Stackoverflow 上被问(并回答)了数百次。
    • @PierrePodevin:它不是 反正都是指针char a[] = "Solaris"; 完全等同于char a[8] = "Solaris";,一个固定大小的数组。事实上,在 C 语言中,所有数组都有固定的大小。大小可以在运行时确定,如int size = 8; char array[size];,但一旦定义,数组大小是固定的。
    【解决方案3】:

    如果问题是“我知道这是错的,但为什么会这样做那个?”,有两种回答方式。

    (1) 未定义的行为意味着任何事情都可能发生。取一个大小为 8 的数组并写入 13 个字符是一件非常错误的事情。您正在覆盖可能用于其他用途的五个字节的内存,因此覆盖它们意味着......任何事情都可能发生。 (但现在我在重复自己。)

    我知道你是真诚地问这个问题的,但我不得不说,对我来说,这些问题听起来总是像:“我跑过一个繁忙的十字路口,当标志上写着不要走路时。一辆蓝色的汽车从我身上碾过,我摔断了左腿。我不明白为什么。为什么我没有被一辆红色卡车撞到?为什么我没有摔断右臂?"

    (2) 让我们看一下为该程序分配的内存的可能布局:

                +----+----+----+----+----+----+----+----+
             a: | S  | o  | l  | a  | r  | i  | s  | \0 |
                +----+----+----+----+----+----+----+----+
    
                +----+----+----+----+
           ptr: | 78 | 56 | 34 | 12 |
                +----+----+----+----+
    
                +----+----+----+----+----+----+
    0x12345678: | L  | i  | n  | u  | x  | \0 |
                +----+----+----+----+----+----+
    

    这里我想象字符串"Linux" 存储在地址0x12345678,所以ptr 保存该值。我想象你的机器使用 32 位指针。 (不过,现在它很可能使用 64。)我想象您的机器使用“小端”字节顺序,这意味着构成指针 p 的字节以与您可能相反的顺序存储在内存中期待。

    你说调用strcat后,a打印出了你期望的拼接字符串,但是当你尝试打印ptr时程序崩溃了。我们把ptr的打印输出改成

    printf("%p: %s\n", ptr, ptr);
    

    在调用strcat 之前,会打印出类似

    0x12345678: Linux
    

    但这就是调用strcat 的实际作用:

                +----+----+----+----+----+----+----+----+
             a: | S  | o  | l  | a  | r  | i  | s  | L  |
                +----+----+----+----+----+----+----+----+
    
                +----+----+----+----+
           ptr: | i  | n  | u  | x  | \0
                +----+----+----+----+
    

    现在,ptr 的打印输出将类似于

    0x78756e69: Segmentation violation (core dumped)
    

    您覆盖了指针ptr,因此它不再指向存储字符串"Linux" 的地址0x12345678,它现在指向位置0x78756e69,这些十六进制数字来自字符i n u x .如果您没有访问地址0x78756e69 的权限,您将遇到崩溃。如果您确实有权访问位置 0x78756e69,则会打印一些垃圾字符串。

    现在,说了这么多,重要的是要注意这不是必然会发生的。我假设编译器将指针ptr 存储在内存中数组a 之后。这是一种可能性,但显然不是唯一的可能性。如果编译器碰巧将ptr 存储在其他地方,那么其他内容将被inux 覆盖,并且其他内容可能会出错。或者什么都不会出错。 (换句话说,你可能会被蓝色的汽车撞到,或者你可能会被红色的卡车撞到,或者你可能会幸运地穿过马路而完全没有被撞到。)


    附录:我刚刚更仔细地查看了您的帖子,我看到 gdb 告诉您 ptr 已更改为 0x78756e69,并且无法访问那里的内存。但现在我们知道0x78756e69 的奇怪值可能来自哪里。 :-)

    【讨论】:

    • ptr的原始值为0x400624,而不是布局中的0x12345678
    猜你喜欢
    • 2015-05-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-30
    相关资源
    最近更新 更多