我正在用C语言为一个赋值编写一个基本的shell程序。我目前在分配用于存储命令行参数的内存时遇到问题,这些参数将被传递到要解析的程序中。
如果输入大小大于或等于4(即"这是一个测试"将在程序终止时产生分段故障,而"这是个测试"则不会),则我得到分段故障。
我想我的问题在于如何分配内存,但程序会捕获我输入的每个令牌并将其打印到屏幕上。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
char cap[] = "";
char* cmd;
const int MAX_ARGS = 16;
const int MAX_ARG_SIZE = 256;
char** arglst = (char**)malloc(MAX_ARGS * sizeof(char*));
for(int i = 0; i < MAX_ARGS; i++)
{
arglst[i] = (char*)malloc(MAX_ARG_SIZE * sizeof(char));
}
pid_t cpid;
//implement ls, cd, mkdir, chdir, rm, rmdir
while(1)
{
printf("Enter a command: ");
scanf("%[^n]s", cap);
int index = 0;
cmd = strtok(cap, " ");
while( cmd != NULL )
{
strcpy(arglst[index], cmd);
cmd = strtok(NULL, " ");
index++;
}
for(int i = 0; i < index; i++)
{
printf("%sn", arglst[i]);
}
printf("%dn", index);
/*
if(strcmp(cap, "quit") == 0) exit(EXIT_SUCCESS);
if( (cpid = fork()) == -1) perror("fork()");
else if(cpid == 0)
{
if( execvp(cmd, arglst) == -1 )
{
errorp("cmd error");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
else
{
cpid = wait(NULL);
strcpy(cmd, "/bin/");
}
*/
for(int i = 0; i < index; i++)
{
free(arglst[i]);
}
free(arglst);
return 0;
}
}
存在许多错误。
这不会编译(例如errorp
而不是perror
)。
cap
太小,无法包含一行。最好使用(例如)char cap[1000];
在主循环之前为每个arglst[i]
预分配一次是有问题的。其中一个单元格必须获得NULL
值,才能与execvp
一起工作。但是,这样做会导致内存泄漏。解决方案是在strtok
循环中使用strdup
,以便仅在需要时分配单元。
此外,由于arglst[i]
在初始化期间仅设置一次,因此在底部附近使用free
进行循环会导致UB[在释放后访问缓冲区]。这是通过使用下面的strdup
来修复的。
注释掉的代码引用了不应依赖的变量(例如cmd
和cap
)。此时,cmd
将成为NULL
,从而导致segfault。
return 0;
放置不正确。将只执行一次迭代(因此只执行一个命令)。
arglst
(例如free(arglst)
)的最终释放是在外循环内部完成的,因此在第二次迭代中引用它是UB。
还有一些问题[注释如下]
这是一个重构版本。它修复了错误,并进行了大量注释。
我已经使用预处理器来显示旧的/原始的代码与新的/固定的代码:
#if 0
// old code
#else
// new code
#endif
同样,对于纯新代码使用#if 1
。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#if 1
#include <sys/wait.h>
#endif
int
main(void)
{
// NOTE/BUG: this must be large enough to contain a command line
#if 0
char cap[] = "";
#else
char cap[1000];
#endif
char *cmd;
const int MAX_ARGS = 16;
char **arglst = malloc(MAX_ARGS * sizeof(*arglst));
// NOTE/BUG: because we need to add a NULL terminator, don't preallocate the
// elements -- we'll leak memory
#if 0
const int MAX_ARG_SIZE = 256;
for (int i = 0; i < MAX_ARGS; i++) {
arglst[i] = (char *) malloc(MAX_ARG_SIZE * sizeof(char));
}
#endif
pid_t cpid;
// implement ls, cd, mkdir, chdir, rm, rmdir
while (1) {
printf("Enter a command: ");
// NOTE/BUG: this didn't work too well
#if 0
scanf("%[^n]s", cap);
#else
fgets(cap,sizeof(cap),stdin);
cap[strcspn(cap,"n")] = 0;
#endif
int index = 0;
cmd = strtok(cap, " ");
while (cmd != NULL) {
// NOTE/BUG: we should strdup dynamically rather than preallocate -- otherwise,
// we leak memory when we set the necessary NULL pointer below
#if 0
strcpy(arglst[index], cmd);
#else
arglst[index] = strdup(cmd);
#endif
cmd = strtok(NULL, " ");
index++;
}
// NOTE/FIX: we have to add a NULL terminator before passing to execvp
#if 1
arglst[index] = NULL;
#endif
for (int i = 0; i < index; i++) {
printf("%sn", arglst[i]);
}
printf("%dn", index);
// NOTE/BUG: we can't [shouldn't] rely on cap here
#if 0
if (strcmp(cap, "quit") == 0)
exit(EXIT_SUCCESS);
#else
if (strcmp(arglst[0], "quit") == 0)
exit(EXIT_SUCCESS);
#endif
if ((cpid = fork()) == -1)
perror("fork()");
else if (cpid == 0) {
// NOTE/BUG: cmd will be NULL here
#if 0
if (execvp(cmd, arglst) == -1) {
errorp("cmd error");
exit(EXIT_FAILURE);
}
#else
if (execvp(arglst[0], arglst) == -1) {
perror("cmd error");
exit(EXIT_FAILURE);
}
#endif
// NOTE/BUG: this will never be executed
#if 0
exit(EXIT_SUCCESS);
#endif
}
else {
cpid = wait(NULL);
// NOTE/BUG -- cmd is NULL and this serves no purpose
#if 0
strcpy(cmd, "/bin/");
#endif
}
// NOTE/BUG: in the _old_ code that did a single preallocate of these cells
// _before_ the loop, freeing them here is wrong -- they would never be
// reallocated because -- the fix using strdup alleviates the issue
for (int i = 0; i < index; i++) {
free(arglst[i]);
}
// NOTE/BUG: freeing this is wrong because we do the allocation only _once_
// above the outer loop
#if 0
free(arglst);
#endif
// NOTE/BUG -- this should be placed at the end to allow multiple commands --
// here it stops after the first command is input
#if 0
return 0;
#endif
}
// NOTE/FIX: correct placement for the above
#if 1
free(arglst);
return 0;
#endif
}
这是清理过的版本,只剩下固定的代码:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int
main(void)
{
char cap[1000];
char *cmd;
const int MAX_ARGS = 16;
char **arglst = malloc(MAX_ARGS * sizeof(*arglst));
pid_t cpid;
// implement ls, cd, mkdir, chdir, rm, rmdir
while (1) {
printf("Enter a command: ");
fgets(cap,sizeof(cap),stdin);
cap[strcspn(cap,"n")] = 0;
int index = 0;
cmd = strtok(cap, " ");
while (cmd != NULL) {
arglst[index] = strdup(cmd);
cmd = strtok(NULL, " ");
index++;
}
arglst[index] = NULL;
for (int i = 0; i < index; i++) {
printf("%sn", arglst[i]);
}
printf("%dn", index);
if (strcmp(arglst[0], "quit") == 0)
exit(EXIT_SUCCESS);
if ((cpid = fork()) == -1)
perror("fork()");
else if (cpid == 0) {
if (execvp(arglst[0], arglst) == -1) {
perror("cmd error");
exit(EXIT_FAILURE);
}
}
else {
cpid = wait(NULL);
}
for (int i = 0; i < index; i++) {
free(arglst[i]);
}
}
free(arglst);
return 0;
}
请注意,上述操作不会检查超过MAX_ARGS
的实际参数数量。
虽然我们可以添加该检查,但更好的方法是在arglst
上使用realloc
来动态增加它,因此不需要对参数数量进行任意限制
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int
main(void)
{
char cap[1000];
char *cmd;
char **arglst = NULL;
int argmax = 0;
pid_t cpid;
// implement ls, cd, mkdir, chdir, rm, rmdir
while (1) {
printf("Enter a command: ");
fgets(cap,sizeof(cap),stdin);
cap[strcspn(cap,"n")] = 0;
int index = 0;
cmd = strtok(cap, " ");
while (cmd != NULL) {
if (index >= argmax) {
argmax += 10;
arglst = realloc(arglst,sizeof(*arglst) * (argmax + 1));
}
arglst[index] = strdup(cmd);
cmd = strtok(NULL, " ");
index++;
}
arglst[index] = NULL;
for (int i = 0; i < index; i++) {
printf("%sn", arglst[i]);
}
printf("%dn", index);
if (strcmp(arglst[0], "quit") == 0)
exit(EXIT_SUCCESS);
if ((cpid = fork()) == -1)
perror("fork()");
else if (cpid == 0) {
if (execvp(arglst[0], arglst) == -1) {
perror("cmd error");
exit(EXIT_FAILURE);
}
}
else {
cpid = wait(NULL);
}
for (int i = 0; i < index; i++) {
free(arglst[i]);
}
}
free(arglst);
return 0;
}
原始代码在arglst
(例如arglst[i]
)的各个元素上使用malloc
和/或strdup
。
这使得代码足够通用,可以在更复杂的场景中使用。但是,在编写代码时,单个元素的malloc/strdup
实际上并不是必需的。
这是因为单元格在主循环的底部已经完全用完了,所以我们不需要来保存它们。
我们可以在每次循环迭代中重用cap
缓冲区空间,因为我们不需要任何令牌来在一次又一次的迭代中生存。
我们可以简单地存储strtok
的返回值,并简化代码:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int
main(void)
{
char cap[1000];
char *cmd;
char **arglst = NULL;
int argmax = 0;
pid_t cpid;
// implement ls, cd, mkdir, chdir, rm, rmdir
while (1) {
printf("Enter a command: ");
fgets(cap,sizeof(cap),stdin);
cap[strcspn(cap,"n")] = 0;
int index = 0;
cmd = strtok(cap, " ");
while (cmd != NULL) {
if (index >= argmax) {
argmax += 10;
arglst = realloc(arglst,sizeof(*arglst) * (argmax + 1));
}
arglst[index] = cmd;
cmd = strtok(NULL, " ");
index++;
}
arglst[index] = NULL;
for (int i = 0; i < index; i++) {
printf("%sn", arglst[i]);
}
printf("%dn", index);
if (strcmp(arglst[0], "quit") == 0)
exit(EXIT_SUCCESS);
if ((cpid = fork()) == -1)
perror("fork()");
else if (cpid == 0) {
if (execvp(arglst[0], arglst) == -1) {
perror("cmd error");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
else {
cpid = wait(NULL);
}
}
free(arglst);
return 0;
}