为什么此 C 代码片段不会导致分段错误?
Why does this C code snippet not result in a segmentation fault?
int main(int argc, char **argv) {
unsigned int ptr1 = *((unsigned int *)(argv[1]));
printf("ptr1 = 0x%x\n", ptr1);
exit(0);
}
这是视频教程中的代码片段。我不确定我是否理解为什么代码在 运行 时不会导致分段错误。
(unsigned int *)(argv[1])
这看起来像传递给程序的第一个参数被强制转换为无符号整数。所以,如果参数是 'AAAA' 那么 '0x41414141' 现在是指向内存中某个位置的指针。
然后当我们这样做时 - *((unsigned int *)(argv[1]))
,我们不是在引用地址 0x41414141
指向的值吗?据我所知,这个地址可能无法被进程访问。那么为什么没有发生分段错误呢?程序的输出是-
ptr1 = 0x41414141
我在 Linux 上用 gcc 编译了这个程序。
注意:此答案解释了代码在这种特殊情况下的工作方式,如需更完整的答案,请参阅@Lundin post。
(unsigned int *)(argv[1])
将 char*
转换为 unsigned int*
.
*((unsigned int *)(argv[1]))
取消引用将 char*
转换为 unsigned int*
。
假设,argv[1]
指向 AAAA
字符串。它在内存中存储为 41 41 41 41
(十六进制)。然后您将其解释为 unsigned int
,结果为 ptr1 = 0x41414141
。
看下图:左边的查看器将内存解释为char*
,右边的查看器将其解释为unsigned int*
(不关心little-endian / big-endian差异):
所以,没有理由出现段错误
这段代码既危险又糟糕。逐步解释代码的作用以及为什么它不好:
argv
是指向字符 (char*[]
) 的指针数组。或者,如果您愿意,指向此类字符指针数组中第一项的指针 (char**
)。
argv[0]
指向可执行文件的名称,argv[1]
是指向传递的第一个参数的指针。
(unsigned int *)(argv[1])
将指向第一个参数的指针 char*
转换为指向 int 的指针 unsigned int*
。这绝不保证是安全的转换。这里有两个主要错误:
如果这个新的整数地址未对齐,访问它会引发未定义的行为,可能导致程序崩溃。
转换违反了严格的别名规则,该规则(简单地说)表明编译器可能假定 char*
指向的内容永远不会通过其他一些访问随机指针类型。因此编译器可能会假设 argv[i]
指向的内存从未被您的程序使用过。当您调用未定义的行为时,可能会发生任何奇怪的优化。
鉴于特定编译器将指针转换的确定性行为指定为非标准扩展,因此它会尝试访问指向的字符串,就好像它是一个整数一样。例如,如果字符串是 "ABCD"
,则生成的整数(假设为 32 位)将是 0x41424344
或 0x44434241
。哪个适用取决于 CPU 字节顺序。这样的代码将是不可移植的。
但是,仅仅访问argv
数组指向的内存当然不会产生任何影响。如果你不能读取这段内存,那么你就不可能使用 argv 参数。它们的确切存储方式取决于 OS,但它必须位于允许您的进程访问的地址 space 内。
因此,只要您在分配的内存中保持访问权限,程序就不会因此而崩溃或出现段错误。如果字符串 argv[1]
只有 1 个字符长,您可能会遇到段错误。
int main(int argc, char **argv) {
unsigned int ptr1 = *((unsigned int *)(argv[1]));
printf("ptr1 = 0x%x\n", ptr1);
exit(0);
}
这是视频教程中的代码片段。我不确定我是否理解为什么代码在 运行 时不会导致分段错误。
(unsigned int *)(argv[1])
这看起来像传递给程序的第一个参数被强制转换为无符号整数。所以,如果参数是 'AAAA' 那么 '0x41414141' 现在是指向内存中某个位置的指针。
然后当我们这样做时 - *((unsigned int *)(argv[1]))
,我们不是在引用地址 0x41414141
指向的值吗?据我所知,这个地址可能无法被进程访问。那么为什么没有发生分段错误呢?程序的输出是-
ptr1 = 0x41414141
我在 Linux 上用 gcc 编译了这个程序。
注意:此答案解释了代码在这种特殊情况下的工作方式,如需更完整的答案,请参阅@Lundin post。
(unsigned int *)(argv[1])
将 char*
转换为 unsigned int*
.
*((unsigned int *)(argv[1]))
取消引用将 char*
转换为 unsigned int*
。
假设,argv[1]
指向 AAAA
字符串。它在内存中存储为 41 41 41 41
(十六进制)。然后您将其解释为 unsigned int
,结果为 ptr1 = 0x41414141
。
看下图:左边的查看器将内存解释为char*
,右边的查看器将其解释为unsigned int*
(不关心little-endian / big-endian差异):
所以,没有理由出现段错误
这段代码既危险又糟糕。逐步解释代码的作用以及为什么它不好:
argv
是指向字符 (char*[]
) 的指针数组。或者,如果您愿意,指向此类字符指针数组中第一项的指针 (char**
)。argv[0]
指向可执行文件的名称,argv[1]
是指向传递的第一个参数的指针。(unsigned int *)(argv[1])
将指向第一个参数的指针char*
转换为指向 int 的指针unsigned int*
。这绝不保证是安全的转换。这里有两个主要错误:如果这个新的整数地址未对齐,访问它会引发未定义的行为,可能导致程序崩溃。
转换违反了严格的别名规则,该规则(简单地说)表明编译器可能假定
char*
指向的内容永远不会通过其他一些访问随机指针类型。因此编译器可能会假设argv[i]
指向的内存从未被您的程序使用过。当您调用未定义的行为时,可能会发生任何奇怪的优化。
鉴于特定编译器将指针转换的确定性行为指定为非标准扩展,因此它会尝试访问指向的字符串,就好像它是一个整数一样。例如,如果字符串是
"ABCD"
,则生成的整数(假设为 32 位)将是0x41424344
或0x44434241
。哪个适用取决于 CPU 字节顺序。这样的代码将是不可移植的。
但是,仅仅访问argv
数组指向的内存当然不会产生任何影响。如果你不能读取这段内存,那么你就不可能使用 argv 参数。它们的确切存储方式取决于 OS,但它必须位于允许您的进程访问的地址 space 内。
因此,只要您在分配的内存中保持访问权限,程序就不会因此而崩溃或出现段错误。如果字符串 argv[1]
只有 1 个字符长,您可能会遇到段错误。