在 for 循环内的 strsep() 之后调用 printf() 会导致段错误

Calling printf() after strsep() inside for loop causes a segfault

我正在用 C 语言编写自己的 UNIX shell,我正在尝试添加对在引号内传递多词参数的支持(即 echo "This is a test")。在您可以在下面看到的我当前的函数 (parseCommandWords) 中,我成功地分离了通过输入参数传递给函数的单词,并通过 strsep() 适当地更新了输入。但是,一旦 printf() 调用 运行s 并打印 wordinput 的正确值,就会抛出分段错误。它永远不会到达 printf 下面的任何 if 语句,在它下面添加任何东西,根本就不会 运行。我看不出是什么导致了这个问题。例如用 input = ls 测试它(简单命令),它会打印出 word = ls | input = (null) 正如你所期望的那样。

parsedWords 参数最初是 NULL 字符串数组,参数在传递给函数之前也经过验证。

更新 #1: 问题几乎肯定与 strcpy(parsedWords[i],word) 有关。将其更改为 parsedWords[i] = word 不会导致段错误,但当然,一旦我们退出该函数,它就会失去其价值。当它通知我非法 read/write.

时,我能够使用 Valgrind 查明这一点

更新 2: 我认为问题在于我在 parseInput 中初始化 args char* 数组的方式。使用 NULL 初始化每个 char* 然后尝试使用 strcpy 在该位置写入应该是导致问题的原因,对吗?像这样为每个字符串动态分配内存解决了这个问题:

char *args[MAX_NUM_OF_COMMAND_WORDS];
int i;
for(i=0; i < MAX_NUM_OF_COMMAND_WORDS; i++) {
    args[i] = (char *)malloc(50*sizeof(char));
}

完整代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include "cs345sh.h"

/**
 * Counts how many times the given char is present 
 * in the given string.
 * @param input The string in which to look for
 * @param lookupChar The char whose occurences to count
 * @return The number of occurences of the given char
 **/
int countCharOccurences(char *input, char lookupChar)
{
    char *str = input;
    int count = 0;
    int i;
    for (i = 0; str[i]; i++)
    {
        if (str[i] == lookupChar)
            count++;
    }
    return count;
}

/**
 * Parses the available command words in the given command and places
 * them in the given array.
 * @param input The initial string to split that contains the command.
 * @param parsedWords The final parsed commands.
 **/
void parseCommandWords(char *input, char **parsedWords)
{
    int i;
    for (i = 0; i < MAX_NUM_OF_COMMAND_WORDS; i++)
    {
        char *word = (char *)malloc(100 * sizeof(char)); // max 100 chars
        if (!word)
        {
            perror("Failed to allocate memory!\n");
            exit(EXIT_FAILURE);
        }
        if (input[0] == '\"')
        {
            char *inptPtr = input;
            int charCnt = 0;
            do
            {
                inptPtr++;
                charCnt++;
            } while (inptPtr[0] != '\"');
            charCnt++; // include final "
            strncpy(word, input, charCnt);
            // check if there are chars left to parse or not
            if (++inptPtr != NULL)
            {
                input = ++inptPtr; // start after the ending "
            }
            else
            {
                input = "";
            }
            printf("word after loop = %s\ninput = %s\n", word, input);
            strcpy(parsedWords[i],word);
            free(word);
            continue;
        }
        word = strsep(&input, " ");
        printf("word = %s | input = %s\n",word,input);
        if (word == NULL)
        {
            free(word);
            break; // there was nothing to split
        }
        if (strlen(word) == 0)
        {
            free(word);
            i--; // read an empty command, re-iterate
            continue;
        }
        printf("before cpy");
        strcpy(parsedWords[i],word);
        printf("word = %s | parsedwords[i] = %s\n",word,parsedWords[i]);
        free(word);

        if(input == NULL) break;
    }
    printf("exiting parser");
}

/**
 * Parses the available commands in the given string and places
 * them in the given array.
 * @param input The initial string to split that contains the commands.
 * @param parsedWords The final parsed commands.
 **/
void parseMultipleCommands(char *input, char **parsedCommands)
{
    int numOfSemicolons = countCharOccurences(input, ';');
    int i;
    for (i = 0; i < numOfSemicolons + 1; i++)
    {
        char *word = strsep(&input, ";");
        if (word == NULL)
            break;
        if (strlen(word) == 0)
        {
            i--;
            continue;
        }
        parsedCommands[i] = word;
    }
}

char *removeLeadingWhitespace(char *input)
{
    while (*input == ' ')
    {
        input++;
    }
    return input;
}

/**
 * Splits the given string at each pipe char occurance and places
 * each command in the given array.
 * @param input The initial string to split
 * @param inptParsed The final parsed commands split at the pipe chars
 * @return Returns 0 if no pipe chars were found or 1 if the operatio was successful.
 **/
int splitAtPipe(char *input, char **inptParsed)
{
    int numOfPipes = countCharOccurences(input, '|');
    int i;
    // create a copy of the given input in order to preserver the original
    char *inpt = (char *)malloc(MAX_INPUT_SIZE * sizeof(char));
    strcpy(inpt, input);
    for (i = 0; i < numOfPipes + 1; i++)
    {
        char *word = strsep(&inpt, "|");
        if (word == NULL)
            break;
        if (strlen(word) == 0)
        {
            i--;
            continue;
        }

        word = removeLeadingWhitespace(word);
        inptParsed[i] = word;
    }
    return 1;
}

/**
 * Handles the execution of custom commands (i.e. cd, exit).
 * @param cmdInfo An array containing the command to execute in the first position, and the arguments
 * to execute with in the rest of the array.
 * @return Returns 0 if the command couldn't be executed, or 1 otherwise.
 **/
int handleCustomCommands(char **cmdInfo)
{
    int numOfCustomCommands = 2;
    char *customCommands[numOfCustomCommands];
    customCommands[0] = "cd";
    customCommands[1] = "exit";
    int i;
    for (i = 0; i < numOfCustomCommands; i++)
    {
        // find the command to execute
        if (strcmp(cmdInfo[0], customCommands[i]) == 0)
            break;
    }

    switch (i)
    {
    case 0:
        if (chdir(cmdInfo[1]) == -1)
            return 0;
        else
            return 1;
    case 1:
        exit(0);
        return 1;
    default:
        break;
    }
    return 0;
}

/**
 * Displays the shell prompt in the following format:
 * <user>@cs345sh/<dir>$
 **/
void displayPrompt()
{
    char *user = getlogin();
    char cwd[512]; // support up to 512 chars long dir paths
    if (getcwd(cwd, sizeof(cwd)) == NULL)
    {
        perror("error retrieving current working directory.");
        exit(-1);
    }
    else if (user == NULL)
    {
        perror("error getting currently logged in user.");
        exit(-1);
    }
    else
    {
        printf("%s@cs345%s$ ", user, cwd);
    }
}

void execSystemCommand(char **args)
{
    // create an identical child process
    pid_t pid = fork();

    if (pid == -1)
    {
        perror("\nFailed to fork child..");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        if (execvp(args[0], args) < 0)
        {
            perror("Could not execute given command..");
        }
        exit(EXIT_FAILURE);
    }
    else
    {
        // wait for the child process to finish
        wait(NULL);
        return;
    }
}

void execPipedCommands(char *input, char **commands)
{
    int numOfPipes = countCharOccurences(input, '|');
    int fds[2 * numOfPipes]; // two file descriptors per pipe needed for interprocess communication
    int i;
    pid_t cpid;

    // initialize all pipes and store their respective fds in the appropriate place in the array
    for (i = 0; i < numOfPipes; i++)
    {
        if (pipe(fds + 2 * i) == -1)
        {
            perror("Failed to create file descriptors for pipe commands!\n");
            exit(EXIT_FAILURE);
        }
    }

    for (i = 0; i < numOfPipes + 1; i++)
    {
        if (commands[i] == NULL)
            break;
        char *args[MAX_NUM_OF_COMMAND_WORDS] = {
            NULL,
        };
        parseCommandWords(commands[i], args);
        cpid = fork(); // start a child process
        if (cpid == -1)
        {
            perror("Failed to fork..\n");
            exit(EXIT_FAILURE);
        }

        if (cpid == 0)
        { // child process is executing
            if (i != 0)
            { // if this is not the first command in the chain
                // duplicate the file descriptor to read from the previous command's output
                if (dup2(fds[(i - 1) * 2], STDIN_FILENO) < 0)
                {
                    perror("Failed to read input from previous command..\n");
                    exit(EXIT_FAILURE);
                }
            }

            // if this is not the last command in the chain
            if (i != numOfPipes && commands[i + 1] != NULL)
            {
                // duplicate write file descriptor in order to output to the next command
                if (dup2(fds[(i * 2 + 1)], STDOUT_FILENO) < 0)
                {
                    perror("Failed to write output for the next command..\n");
                    exit(EXIT_FAILURE);
                }
            }

            // close the pipes
            int j;
            for (j = 0; j < numOfPipes + 1; j++)
            { // close all copies of the file descriptors
                close(fds[j]);
            }

            // execute command
            if (execvp(args[0], args) < 0)
            {
                perror("Failed to execute given piped command");
                return;
            }
        }
    }
    // parent closes all original file descriptors
    for (i = 0; i < numOfPipes + 1; i++)
    {
        close(fds[i]);
    }

    // parent waits for all child processes to finish
    for (i = 0; i < numOfPipes + 1; i++)
        wait(0);
}

void parseInput(char *input)
{
    if (strchr(input, '|') != NULL)
    { // possibly piped command(s)
        char *commands[MAX_NUM_OF_COMMANDS] = {
            NULL,
        };
        splitAtPipe(input, commands);
        execPipedCommands(input, commands);
    }
    else if (strchr(input, ';') != NULL)
    { // possibly multiple command(s)
        char *commands[MAX_NUM_OF_COMMANDS] = {
            NULL,
        };
        parseMultipleCommands(input, commands);
        int i;
        for (i = 0; i < MAX_NUM_OF_COMMANDS; i++)
        {
            if (commands[i] == NULL)
                break;
            // single command
            char *args[MAX_NUM_OF_COMMAND_WORDS] = {
                NULL,
            };
            parseCommandWords(commands[i], args);
            if (handleCustomCommands(args) == 0)
            {
                execSystemCommand(args);
            }
        }
    }
    else
    {
        // single command
        char *args[MAX_NUM_OF_COMMAND_WORDS] = {
            NULL,
        };
        parseCommandWords(input, args);
        printf("parsed! arg[0] = %s\n",args[0]);
        if (handleCustomCommands(args) == 0)
        {
            execSystemCommand(args);
        }
    }
}

int main()
{
    char *inputBuf = NULL; // getline will allocate the buffer
    size_t inputLen = 0;
    while (1)
    {
        displayPrompt();
        if (getline(&inputBuf, &inputLen, stdin) == -1)
        {
            perror("Error reading input.");
            exit(EXIT_FAILURE);
        }
        if (*inputBuf == '\n')
            continue;
        else
        {
            // remove the \n at the end of the read line ()
            inputBuf[strcspn(inputBuf, "\n")] = '[=11=]';
            parseInput(inputBuf);
        }
    }
    return 0;
}

这是最小的可重现示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include "cs345sh.h"

/**
 * Counts how many times the given char is present 
 * in the given string.
 * @param input The string in which to look for
 * @param lookupChar The char whose occurences to count
 * @return The number of occurences of the given char
 **/
int countCharOccurences(char *input, char lookupChar)
{
    char *str = input;
    int count = 0;
    int i;
    for (i = 0; str[i]; i++)
    {
        if (str[i] == lookupChar)
            count++;
    }
    return count;
}

/**
 * Parses the available command words in the given command and places
 * them in the given array.
 * @param input The initial string to split that contains the command.
 * @param parsedWords The final parsed commands.
 **/
void parseCommandWords(char *input, char **parsedWords)
{
    int i;
    for (i = 0; i < MAX_NUM_OF_COMMAND_WORDS; i++)
    {
        char *word = (char *)malloc(100 * sizeof(char)); // max 100 chars
        if (!word)
        {
            perror("Failed to allocate memory!\n");
            exit(EXIT_FAILURE);
        }
        if (input[0] == '\"')
        {
            char *inptPtr = input;
            int charCnt = 0;
            do
            {
                inptPtr++;
                charCnt++;
            } while (inptPtr[0] != '\"');
            charCnt++; // include final "
            strncpy(word, input, charCnt);
            // check if there are chars left to parse or not
            if (++inptPtr != NULL)
            {
                input = ++inptPtr; // start after the ending "
            }
            else
            {
                input = "";
            }
            printf("word after loop = %s\ninput = %s\n", word, input);
            strcpy(parsedWords[i],word);
            free(word);
            continue;
        }
        word = strsep(&input, " ");
        printf("word = %s | input = %s\n",word,input);
        if (word == NULL)
        {
            free(word);
            break; // there was nothing to split
        }
        if (strlen(word) == 0)
        {
            free(word);
            i--; // read an empty command, re-iterate
            continue;
        }
        printf("before cpy");
        strcpy(parsedWords[i],word);
        printf("word = %s | parsedwords[i] = %s\n",word,parsedWords[i]);
        free(word);

        if(input == NULL) break;
    }
    printf("exiting parser");
}
 

/**
 * Handles the execution of custom commands (i.e. cd, exit).
 * @param cmdInfo An array containing the command to execute in the first position, and the arguments
 * to execute with in the rest of the array.
 * @return Returns 0 if the command couldn't be executed, or 1 otherwise.
 **/
int handleCustomCommands(char **cmdInfo)
{
    int numOfCustomCommands = 2;
    char *customCommands[numOfCustomCommands];
    customCommands[0] = "cd";
    customCommands[1] = "exit";
    int i;
    for (i = 0; i < numOfCustomCommands; i++)
    {
        // find the command to execute
        if (strcmp(cmdInfo[0], customCommands[i]) == 0)
            break;
    }

    switch (i)
    {
    case 0:
        if (chdir(cmdInfo[1]) == -1)
            return 0;
        else
            return 1;
    case 1:
        exit(0);
        return 1;
    default:
        break;
    }
    return 0;
}

/**
 * Displays the shell prompt in the following format:
 * <user>@cs345sh/<dir>$
 **/
void displayPrompt()
{
    char *user = getlogin();
    char cwd[512]; // support up to 512 chars long dir paths
    if (getcwd(cwd, sizeof(cwd)) == NULL)
    {
        perror("error retrieving current working directory.");
        exit(-1);
    }
    else if (user == NULL)
    {
        perror("error getting currently logged in user.");
        exit(-1);
    }
    else
    {
        printf("%s@cs345%s$ ", user, cwd);
    }
}

void execSystemCommand(char **args)
{
    // create an identical child process
    pid_t pid = fork();

    if (pid == -1)
    {
        perror("\nFailed to fork child..");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        if (execvp(args[0], args) < 0)
        {
            perror("Could not execute given command..");
        }
        exit(EXIT_FAILURE);
    }
    else
    {
        // wait for the child process to finish
        wait(NULL);
        return;
    }
}

void parseInput(char *input)
{
        // single command
        char *args[MAX_NUM_OF_COMMAND_WORDS] = {
            NULL,
        };
        parseCommandWords(input, args);
        printf("parsed! arg[0] = %s\n",args[0]);
        if (handleCustomCommands(args) == 0)
        {
            execSystemCommand(args);
        }
}

int main()
{
    char *inputBuf = NULL; // getline will allocate the buffer
    size_t inputLen = 0;
    while (1)
    {
        displayPrompt();
        if (getline(&inputBuf, &inputLen, stdin) == -1)
        {
            perror("Error reading input.");
            exit(EXIT_FAILURE);
        }
        if (*inputBuf == '\n')
            continue;
        else
        {
            // remove the \n at the end of the read line ()
            inputBuf[strcspn(inputBuf, "\n")] = '[=12=]';
            parseInput(inputBuf);
        }
    }
    return 0;
}

头文件:

#define MAX_NUM_OF_COMMAND_WORDS 50 // usual num of maximum command arguments is 9 (but is system dependent)
#define MAX_NUM_OF_COMMANDS 20 // what could it be hmm
#define MAX_INPUT_SIZE 1000 // num of max chars to read

/**
 * Counts how many times the given char is present 
 * in the given string.
 * @param input The string in which to look for
 * @param lookupChar The char whose occurences to count
 * @return The number of occurences of the given char
 **/
int countCharOccurences(char* input, char lookupChar);
/**
 * Parses the available command words in the given command and places
 * them in the given array.
 * @param input The initial string to split that contains the command.
 * @param parsedWords The final parsed commands.
 **/
void parseCommandWords(char *input, char** parsedWords);

/**
 * Parses the available commands in the given string and places
 * them in the given array.
 * @param input The initial string to split that contains the commands.
 * @param parsedWords The final parsed commands.
 **/
void parseMultipleCommands(char *input, char **parsedCommands);

/**
 * Splits the given string at each pipe char and places
 * each command in the given array.
 * @param input The initial string to split
 * @param inptParsed The final parsed commands split at the pipe chars
 * @return Returns 0 if no pipe chars were found or 1 if the operation was successful.
 **/
int splitAtPipe(char *input, char** inptParsed);

/**
 * Handles the execution of custom commands (i.e. cd, exit).
 * @param cmdInfo An array containing the command to execute in the first position, and the arguments
 * to execute with in the rest of the array.
 * @return Returns 0 if the command couldn't be executed, or 1 otherwise.
 **/ 
int handleCustomCommands(char **command);

/**
 * Displays the shell prompt in the following format:
 * <user>@cs345sh/<dir>$
 **/
void displayPrompt();

void execPipedCommands(char*, char**);

/**
 * Removes any trailing whitespace from the given string
 * and returns a pointer at the beginning of the new string.
 * @param input The string to remove whitespace from
 */
char* removeLeadingWhitespace(char *input) ;

strcpy相反,函数strncpy不一定会向目标数组添加终止空字符。

由于 charCnt 似乎小于或等于 strlen(input),因此行

strncpy(word, input, charCnt);

不会将终止空字符写入word。因此,行

printf("word after loop = %s\ninput = %s\n", word, input);

将调用未定义的行为,因为 %s 格式说明符需要一个以 null 结尾的字符串。

缺少初始化

inputLengetline() 时间不确定。初始化它。
*lineptr 可以包含一个 指向大小为 *n 字节的 malloc(3) 分配缓冲区的指针。

  char *inputBuf;
  // size_t inputLen;
  size_t inputLen = MAX_INPUT_SIZE;
  inputBuf = (char*) malloc(MAX_INPUT_SIZE * sizeof(char));
  ...
    if (getline(&inputBuf, &inputLen, stdin) == -1) {

  // Even better
  char *inputBuf = NULL;
  size_t inputLen = 0;

  // Not needed
  // inputBuf = (char*) malloc(MAX_INPUT_SIZE * sizeof(char));
  ...
    if (getline(&inputBuf, &inputLen, stdin) == -1) {

可能是其他问题

使用 Valgrind 我能够查明问题是由于我没有为我的 args 数组正确分配内存,然后尝试将内存与 strcpy 一起使用。更具体地说:

使用时char *args[MAX_NUM_OF_COMMAND_WORDS] = { NULL,} 我不是为参数本身分配内存,而是为指针分配内存。 这会导致段错误,因为 strcpy(parsedWords[i],word); 会尝试写入无效内存(因为 parsedWords[i] 将为 NULL)。我重构了代码,以便我只为我需要的 args 分配内存,而不是在我什至不知道我是否需要那么多的时候盲目地为 50 个 args 分配内存。然后我 return 从 parseCommandWords() 函数的给定命令中找到的 args 计数,然后用于释放分配的内存。

修改后的代码(21 年 9 月 11 日更新为最终版本):

/**
 * Parses the available command words in the given command and places
 * them in the given array.
 * @param input The initial string to split that contains the command.
 * @param parsedWords An array to every command word.
 * @return The number of words in the given command
 **/
int parseCommandWords(char *input, char **parsedWords)
{
    int i;
    int cnt = 0;
    for (i = 0; i < MAX_NUM_OF_COMMAND_WORDS; i++)
    {
        char word[MAX_NUM_OF_COMMAND_WORDS];
        input = removeLeadingWhitespace(input);
        if (strlen(input) == 0)
            break;
        if (input[0] == '\"')
        {
            char *inptPtr = input + 1; // start after the beginning " char
            int charCnt = 0;
            while (inptPtr[0] != '\"')
            {
                inptPtr++;
                charCnt++;
            }
            if (charCnt >= MAX_NUM_OF_COMMAND_WORDS)
            {
                perror("Quoted argument was too long!\n");
                exit(EXIT_FAILURE);
            }
            strncpy(word, input + 1, charCnt); // input+1 : start after the beginning " and charCnt: end before the closing "
            word[charCnt] = '[=10=]';              // add null terminator
            // check if there are chars left to parse or not
            if (strlen(++inptPtr) > 0)
            {
                input = inptPtr;
            }
            else
            {
                input = NULL;
            }
            parsedWords[i] = (char *)malloc(MAX_NUM_OF_COMMAND_WORDS * sizeof(char));
            cnt++;
            strcpy(parsedWords[i], word);
            if (input == NULL || strlen(input) == 0)
                return cnt;
            else
                continue;
        }
        strcpy(word, strsep(&input, " "));
        if (word == NULL)
            break;             // nothing to split
        if (strlen(word) == 0) // read an empty command, re-iterate
        {
            i--;
            continue;
        }
        parsedWords[i] = (char *)malloc(MAX_NUM_OF_COMMAND_WORDS * sizeof(char));
        if (!parsedWords[i])
        {
            perror("Failed to allocate memory for command\n");
            exit(EXIT_FAILURE);
        }
        cnt++;
        strcpy(parsedWords[i], word);
        if (input == NULL || strlen(input) == 0)
            break;
    }
    return cnt;
}
    
/**
 * Executes the given commands after parsing them according to 
 * their type (i.e. pipes, redirection, etc.).
 * @param input A line read from the shell containing commands to execute
 * */
void parseInput(char *input)
{
    if (strchr(input, '|') != NULL)
    {
        // possibly piped command(s)
        char *commands[MAX_NUM_OF_COMMANDS] = {
            NULL,
        };
        int numOfCmds = splitAtPipe(input, commands);
        execPipedCommands(input, commands);
        int i;
        for (i = 0; i < numOfCmds; i++)
            if (commands[i] != NULL)
                free(commands[i]);
    }
    else if (strchr(input, '>') != NULL || strchr(input, '<') != NULL)
    { // no need to check for >> since we check for >
        // redirection commands
        char *commands[MAX_NUM_OF_REDIR_CMDS] = {
            NULL,
        };
        char *delim = (char *)malloc(3 * sizeof(char));

        if (strstr(input, ">>"))
            strcpy(delim, ">>");
        else if (strchr(input, '>'))
            strcpy(delim, ">");
        else
            strcpy(delim, "<");

        splitAtRedirectionDelim(input, commands, delim);
        execRedirectionCommands(input, commands, delim);

        int i;
        for (i = 0; i < MAX_NUM_OF_REDIR_CMDS; i++)
            if (commands[i] != NULL)
                free(commands[i]);
        free(delim);
    }
    else if (strchr(input, ';') != NULL)
    {
        // possibly multiple command(s)
        char *commands[MAX_NUM_OF_COMMANDS] = {
            NULL,
        };
        int numOfCmds = parseMultipleCommands(input, commands);
        int i;
        for (i = 0; i < numOfCmds; i++)
        {
            if (commands[i] == NULL)
                break;
            // single command
            char *args[MAX_NUM_OF_COMMAND_WORDS] = {
                NULL,
            };
            int numOfArgs = parseCommandWords(commands[i], args);
            if (handleCustomCommands(args,numOfArgs,input) == 0)
            {
                execSystemCommand(args);
            }
            int j;
            for (j = 0; j < numOfArgs; j++)
            {
                free(args[j]);
            }
            if (commands[i] != NULL)
                free(commands[i]);
        }
    }
    else
    {
        // single command
        char *args[MAX_NUM_OF_COMMAND_WORDS] = {
            NULL,
        };
        int numOfArgs = parseCommandWords(input, args);
        if (handleCustomCommands(args,numOfArgs,input) == 0)
        {
            execSystemCommand(args);
        }
        int i;
        for (i = 0; i < numOfArgs; i++)
        {
            free(args[i]);
        }
    }
}

正如很多人指出的那样,我的代码还包含很多其他问题(mem 问题、处理引用 args 的逻辑有问题等),所以我将听取他们的意见,采取退后一步,在继续之前尝试分段测试所有内容。