Strtok () иногда (??) вызывает разрушение стека?

Используя Kubuntu 22.04 LTS, Kate v22.04.3 и gcc v11.3.0, я разработал небольшую программу для исследования использования strtok() для токенизации строк, которая показана ниже.

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

int main(void)
{
   char inString[] = "";         // string read in from keyboard.
   char * token    = "";         // A word (token) from the input string.
   char delimiters[] = " ,";     // Items that separate words (tokens).
   
   // explain nature of program.
   printf("This program reads in a string from the keyboard"
          "\nand breaks it into separate words (tokens) which"
          "\nare then output one token per line.\n");
   printf("\nEnter a string: ");
   scanf("%s", inString);
   
   /* get the first token */
   token = strtok(inString, delimiters);
   
   /* Walk through other tokens. */
   while (token != NULL)
   {
      printf("%s", token);
      printf("\n");
      
      // Get next token.
      token = strtok(NULL, delimiters);
   }
   return 0;
}

Из различных веб-страниц, которые я просматривал, кажется, что я правильно отформатировал вызов функции strtok(). При первом запуске программа выдает следующий результат.

$ ./ex6_2
This program reads in a string from the keyboard
and breaks it into separate words (tokens) which
are then output one token per line.

Enter a string: fred ,  steve ,   nick
f
ed

При втором запуске он выдал следующий результат.

$ ./ex6_2
This program reads in a string from the keyboard
and brakes it into separate words (tokens) which
are then output one token per line.

Enter a string: steve ,  barney ,   nick
s
eve
*** stack smashing detected ***: terminated
Aborted (core dumped)

Последующие запуски показали, что программа как бы запускается, как и в первом случае выше, если первое слово/токен содержит только четыре символа. Однако, если первое слово/токен содержало пять или более символов, происходило разрушение стека.

Учитывая, что для доступа к токенам используется «char *», почему:

а) разделяется ли первый токен (в каждом случае) на втором символе?

б) последующие токены (в каждом случае) не выводятся?

c) приводит ли первое слово/токен из более чем четырех символов к разрушению стека?

Стюарт

char inString[] = ""; вызывает переполнение стека. Вы не указали длину строки, поэтому она не может содержать строку длиннее 0.
Marco 19.04.2023 15:29
scanf("%s", inString); не будет вводить строки с пробелами.
n. m. 19.04.2023 15:36

OT: char * token = ""; странно, скорее должно быть char * token = NULL;, а тут вообще не надо его инициализировать. Самым идиоматичным способом было бы удалить char * token = ""; все вместе и оставить только char *token = strtok(inString, delimiters);

Jabberwocky 19.04.2023 15:37

@Marco: переполнение стека сильно отличается от переполнения буфера в стеке. Вы неправильно используете термин «переполнение стека» для последнего.

Andreas Wenzel 19.04.2023 15:42

@Stuart Когда вы написали char inString[], вы думали, что это означало «выделить inString как строку, которая настолько велика, насколько это необходимо для всего, что в нее читается?» C не имеет строк, которые работают таким образом.

Steve Summit 19.04.2023 16:05

@AndreasWenzel Я не такой. ОП сказал, что получил следующее сообщение: *** stack smashing detected ***: что означает, что стек поврежден.

Marco 19.04.2023 16:11

@AndreasWenzel Если вы погуглите stack smashing, первый результат будет A cyberattack that causes a stack buffer overflow. .

Marco 19.04.2023 16:12

@Marco: В своем первом комментарии вы использовали термин «переполнение стека», а не «переполнение буфера стека». переполнение стека сильно отличается от переполнения буфера стека. Вы путаете эти два термина.

Andreas Wenzel 19.04.2023 16:26

@AndreasWenzel Вы не правы. См. Переполнение стека (значения): Переполнение стека может также означать: переполнение буфера стека, когда программа записывает в адрес памяти в стеке вызовов программы за пределами предполагаемой структуры данных; обычно буфер фиксированной длины.

Marco 19.04.2023 17:22

@Marco: Цель страниц значений неоднозначности в Википедии — помочь вам найти статью, которую вы ищете. Поэтому, когда, например, на странице утверждается, что термин А «может относиться» к термину Б, не обязательно правильно делать вывод, что термин А является подходящим способом ссылки на термин Б. Например, термин А также может быть распространенным неверным способом. ссылки на термин B. Я предлагаю вам прочитать сами статьи, чтобы определить, какие термины подходят.

Andreas Wenzel 19.04.2023 20:45

@AndreasWenzel Я не согласен. Устранение неоднозначности - это процесс определения того, какое значение слова используется в контексте. В этом контексте должно быть совершенно ясно, что имелось в виду «переполнение буфера стека». Если достаточное количество людей называют переполнение буфера стека «переполнением стека» [в контексте], вам придется принять это. Вы можете быть технически правы, но опять же, это не имеет значения. Единственное, что вы могли бы отметить, это то, что мое заявление было двусмысленным, но не ошибочным. Это не «неправильно» ссылаться на переполнение буфера стека переполнением стека, иначе не существовало бы неоднозначности в Википедии.

Marco 19.04.2023 21:26

@AndreasWenzel Однако я благодарен, что вы указали на разницу. Я знаю, что переполнение стека вызовов отличается от переполнения буфера в стеке. Я не знал, что людей так волнует разница.

Marco 19.04.2023 21:28

Единственное, что имеет значение, это то, что я правильно назвал переполнение буфера стека «переполнением стека».

Marco 19.04.2023 21:30

@Марко: Ты прав. Я знаю, что допустимо не устанавливать размер массива, если для инициализации массива предоставляется фактическая строка. Я предполагаю, что мои мысли были неясными.

Stuart 22.04.2023 11:12

@n.m.: Спасибо за это. Я не знал об этом.

Stuart 22.04.2023 11:13

@Jabberwocky: Вы правы в том, что указатель должен был быть инициализирован с помощью NULL. С каких это пор указатель(!) инициализируется строкой?!! Шиш. Что касается вашего предложения инициализировать указатель вызовом strtok(), это кажется лучшей идеей. С вашего позволения, я воспользуюсь этим.

Stuart 22.04.2023 11:16

@Andreas Wenzel: Если вы перечитаете мою исходную публикацию, вы увидите, что я никогда не упоминал ни о переполнении стека, ни о переполнении буфера. Это была система, которая сообщила «*** обнаружен сбой стека ***», о чем я затем упомянул в следующем абзаце.

Stuart 22.04.2023 11:24

@Steve Summit: Не совсем так. Я думал, что выделяю место для строки нулевой длины, которая затем будет расширена в соответствии с тем, что было захвачено функцией scanf(). Да, я узнаю, что струны C определенно не действуют таким образом. Хо хм. Я думаю, что я немного устал/не ясно мыслил, когда кодировал этот бит.

Stuart 22.04.2023 11:30

@Stuart: Да, вы правы, что никогда не использовали термин «переполнение стека». Если вы перечитаете мои комментарии, вы увидите, что они были адресованы пользователю по имени «Марко», который использовал (неверный) термин «переполнение стека» в своем первом комментарии к вашему вопросу.

Andreas Wenzel 22.04.2023 14:50

Пожалуйста, примите мои извинения за то, что я забыл, кому был адресован ваш комментарий. Прочитав более дюжины ответов, я думаю, что немного запутался.

Stuart 24.04.2023 09:22
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
20
115
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это объявление массива символов

char inString[] = "";   

эквивалентно

char inString[1] = { '\0' };; 

То есть он объявляет массив только с одним элементом, который может хранить только пустую строку. Таким образом, любая попытка прочитать строку в этом массиве символов с помощью этого вызова scanf

scanf("%s", inString);

вызывает неопределенное поведение.

Вам нужно указать количество элементов намного больше. Например

enum { N = 100 };
char inString[N] = "";   

Эта инициализация указателя

char * token    = "";

не имеет большого смысла. Лучше написать например

char * token = NULL;

Этот призыв scanf

scanf("%s", inString);

может прочитать только одно слово, представляющее собой последовательность символов, разделенных пробелами.

Вместо этого напишите, например

scanf( " %99[^\n]", inString);

Имеет смысл включить символ табуляции '\t' в список разделителей

const char *delimiters = " \t,";

Вместо этих звонков printf

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

будет проще написать

puts( token );

Большое спасибо за разъяснения и ваши предложения. Будет ли scanf( "%99[^\n]", inString) захватывать строку, содержащую пробелы? Я только что узнал, что scan() захватывает символы до первого пробела, но не включает его.

Stuart 22.04.2023 11:54

@Stuart Этот вызов scanf считывает все символы, включая пробелы, до тех пор, пока не встретится символ новой строки. . Также безопаснее будет написать scanf( " %99[^\n]", inString); Обратите внимание на начальный пробел в строке формата. Это позволяет пропускать символы пробела, оставшиеся в буфере после ввода других данных, например, если перед этим вызовом есть вызов типа scanf( "%d", &number ); читать целое число.

Vlad from Moscow 22.04.2023 12:12

@Stuart Потому что после этого вызова символ новой строки '\n' будет оставлен в буфере, а вызов scanf, который читает строку, будет считывать пустую строку, потому что он сразу встречает символ новой строки.

Vlad from Moscow 22.04.2023 12:12

Спасибо за ваши дальнейшие разъяснения. Они очень полезны.

Stuart 22.04.2023 12:37
Ответ принят как подходящий

Декларация

char inString[] = "";

эквивалентно:

char inString[1] = "";

Это означает, что вы выделяете массив только из одного элемента, поэтому в нем есть место только для хранения одного символа.

Вызов функции

scanf("%s", inString);

требует, чтобы аргумент функции inString указывал на буфер памяти, достаточно большой для хранения согласованных входных данных. Ваша программа нарушает это требование, поскольку в буфере памяти есть место только для одного символа (конечный нулевой символ). Поэтому он может хранить только строки нулевой длины.

Нарушая требование, ваша программа вызывает неопределенное поведение , а это значит, что может произойти что угодно, включая наблюдаемое вами странное поведение. Функция scanf, вероятно, переполняет буфер inString, перезаписывая другие важные данные в стеке вашей программы, что приводит к неправильному поведению. Это называется "разгром стека".

Чтобы это исправить, вы должны дать массиву inString больше места, например, изменив строку

char inString[] = "";

к:

char inString[200] = "";

Однако в этом случае, если пользователь введет более 200 символов в качестве одного слова, у вас снова возникнет та же проблема, и ваша программа может рухнуть. Поэтому вы можете дополнительно ограничить количество символов, соответствующих scanf, до 199 символов (200, включая завершающий нулевой символ). Таким образом, вы можете гарантировать, что пользователь не сможет сломать вашу программу.

Вы можете добавить такой лимит следующим образом:

scanf("%199s", inString);

Обратите внимание, однако, что спецификатор %s будет соответствовать только одному слову. Если вы хотите прочитать всю строку ввода, вы можете использовать функцию fgets вместо scanf.

Большое спасибо за очевидно столь необходимое разъяснение. Я думаю, что буду(!) использовать fgets() вместо scanf(). Вы говорите, что «... спецификатор %s будет соответствовать только одному слову». Если это правда, какой смысл использовать «большое число» в «%s», если слова после первого пробела игнорируются функцией scanf()?

Stuart 22.04.2023 11:46

@Stuart: Вы правы в том, что в большинстве случаев пользователь, вероятно, никогда не введет слово длиннее 200 символов, поэтому использования %s вместо %199s, вероятно, будет достаточно. Однако если вы программируете, например, веб-сервер, то злоумышленник сможет вывести из строя вашу серверную программу, введя слово длиннее 200 символов. Вы можете запретить пользователю делать это, используя %199s вместо %s. Следовательно, вам решать, хотите ли вы, чтобы пользователь не мог сломать вашу программу, или это не имеет значения.

Andreas Wenzel 22.04.2023 15:00

@Stuart: Если один из ответов решил вашу проблему, вы можете рассмотреть возможность принятия одного из ответов. См. эту официальную справочную страницу для получения дополнительной информации: Что мне делать, когда кто-то отвечает на мой вопрос?

Andreas Wenzel 22.04.2023 15:30

@Stuart: Если вы решите использовать fgets, вы, вероятно, столкнетесь с проблемой fgets хранения символа \n в конце строки в строке. Поэтому вы можете прочитать это о том, как его удалить: Удаление завершающего символа новой строки из ввода fgets()

Andreas Wenzel 22.04.2023 15:36

Другие вопросы по теме