posix_spawn的argv参数可以传递字符串字面量吗?

8

对于函数posix_spawn,它的原型是:

int posix_spawn(pid_t *restrict pid, const char *restrict path,
   const posix_spawn_file_actions_t *file_actions,
   const posix_spawnattr_t *restrict attrp,
   char *const argv[restrict], char *const envp[restrict]);

值得注意的是,argv参数指向一个char *指针数组(即可变字符指针)。此外,文档似乎没有给出任何保证数据不会被写入。
我的问题是:是否有任何保证可以传递字符串字面值?还是我们冒着段错误的风险?
示例代码:
char *v[] = { "foo.exe", "bar", NULL };
posix_spawn( NULL, "foo.exe", NULL, NULL, v, NULL );

你不需要任何明确的保证数据不会被写入。除非函数被指定修改某个对象,否则它不会这样做。 - R.. GitHub STOP HELPING ICE
@R.. 如果明确说明会更好。我无法想象为什么posix_spawn要修改参数,但肯定有一些情况下指针可以被视为const(即,目标将被写入,但在返回时更改将被还原)。我经常使用这样的指针(将它们注释为as-if-const)来处理字符串,在需要临时插入\0的情况下。 - Petr Skocik
@PSkocik: 不可能存在这种情况。任何这样的实现都会有数据竞争,从而不具备线程安全性。 - R.. GitHub STOP HELPING ICE
3个回答

3

在这里使用字符串字面量完全没有问题。

指针参数(或由参数指向的指针数据)是否指向const限定类型与函数是否可以修改所指向的对象无关。这纯粹是有关该函数的契约的问题。通常情况下,当对象不会被修改时,使用const限定指针作为参数更可取:

  1. 允许传递指向const限定对象的指针而不需要转换;
  2. 表示对象不会被修改。

但在C语言中并没有强制要求这样做。对于在其接口中使用双指针类型的函数来说,通常存在折衷。由于T *和const T *不能别名彼此,接口必须选择最有可能由调用方想要的形式;如果调用方想要另一个形式,则必须创建临时副本以传递给函数。这就是posix_spawn的情况。

一般来说,对于标准函数(C或POSIX),它们除特别指定外不应具有任何可观察的副作用。除非函数的描述文档说明它将修改应用程序“属于”或应用程序可以访问的对象,否则它不能修改它;这样做是不符合规范的。这就是为什么返回指向静态存储的指针的函数明确记录它的文档。例如,POSIX为strerror记录:

由strerror()返回的字符串指针可能无效,或者字符串内容可能被随后的调用覆盖。

除此之外,应用程序可以假定strerror返回的字符串永远不会被实现修改。

由于posix_spawn没有记录修改其argv数组所指向的字符串,因此它不会修改它们。

此外,请注意,posix_spawn需要具有线程安全性,并且不会对并发访问argv字符串的应用程序施加任何显式限制。因此,任何修改都会引入数据竞争,从而使posix_spawn不符合线程安全性规范。


关于标准函数的推理是正确的。然而,它没有详细说明posix_spawn()文档缺少副作用。相反,根据我的阅读,“argv”和“envp”参数指定在子进程中执行的程序的参数列表和环境,就像posix_spawn中的execve一样,posix_spawn()处理argv... - chux - Reinstate Monica
类似于execve,它有“参数向量和环境可以被调用程序的主函数访问,当它被定义为:int main(int argc, char *argv[], char *envp[])”。由于main()中的argv可以通过修改argv数组指向的字符串来进行修改,因此可以合理地认为posix_spawn()argv也会产生相同的可能性。 - chux - Reinstate Monica
@chux:对于execve,这是在调用exec并且一切都已经停止存在之后运行进程映像完全被替换的情况。此时它是否可变是无关紧要的,因为原始对象不再存在。 - R.. GitHub STOP HELPING ICE
posix_spawn 的情况下,与 execve 不同的是,调用者并不会停止存在;一个子进程被创建,并且其参数从传递给 posix_spawnargv 中复制。这是一个完全不同的进程;修改新进程中由 main 接收到的 argv 数组对传递给 posix_spawnargv 没有影响。你之所以错误地混淆它们,只是因为它们有相同的名称。 - R.. GitHub STOP HELPING ICE

1
我很确定这种类型被选择是为了与mainexecvechar **argv参数兼容。(尽管在具有适当进程分离的传统实现中,内核最终必须进行复制。)
POSIX似乎没有说这些数组会被修改,但我非常确信没有现有的实现会修改它们。可能有一些使用不同参数(和可执行文件名)的原因,但那些将会更长,所以posix_spawn必须为副本分配内存,并且无法就地执行修改。

1
给定的情况下,传递字符串字面值存在技术上的段错误风险,因为argv期望一个非constchar*数组,并提供一个字符串字面值可能会导致未定义行为。通过一个模拟main(int argc, char *argv[])的函数,代码可以写入argv[0]
“int main(int argc, char *argv[])”的参数和argv数组所指向的字符串应该被程序修改,并在程序启动和终止之间保留它们的最后存储值。C11 §5.1.2.2.1 2
int foo(......., char *const argv[restrict], char *const envp[restrict]);

char *v[] = { "foo.exe", "bar", NULL };
foo( NULL, "foo.exe", NULL, NULL, v, NULL );

替代方案

虽然使用posix_spawn(),我怀疑不会发生写入,但是使用C99的解决方案采用了复合字面量而不是字符串字面量,因此避免了UB潜在风险。

// char *v[] = { "foo.exe", "bar", NULL };
char *v2[] = { (char [8]){"foo.exe"}, (char [4]){"bar"}, NULL };
posix_spawn( NULL, "foo.exe", NULL, NULL, v2, NULL );

现在,posix_spawn()可以写入v2[0]

@chux char* argv[] = {"arg1", "arg2", NULL}; 触发了警告。我刚意识到我在使用C++,而OP在使用C。修复方法是改为 char arg1[22] = {"arg1"}; char arg2[11] = {"arg2"}; char* argv[] = {arg1, arg2, NULL};,虽然不太美观。 - Nate Glenn
@chux 是的,它会生成上面提到的警告两次,每个字符串一次。使用C++17。 - Nate Glenn
@Nate Glenn 你用C编译器会收到这些警告吗?这篇文章标记了C。 - chux - Reinstate Monica
@chux 正如我在第二条评论中提到的那样,不需要,所以您可以忽略这个。我碰巧在搜索C++中相同问题时发现了这个问题,因此在此发布解决方法将对下一个使用C++的人有用。 - Nate Glenn
@NateGlenn 考虑发布一个涉及此问题的 C++ 问题。 - chux - Reinstate Monica
显示剩余5条评论

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