Изучая C, буду признателен за информацию о том, почему это решение работает - PullRequest
7 голосов
/ 09 июня 2010

Это буквально первая вещь, которую я когда-либо писал на C, поэтому, пожалуйста, не стесняйтесь указывать на все ее недостатки. :) Моя проблема, однако, заключается в следующем: если я пишу программу так, как мне кажется, она самая чистая, я получаю испорченную программу:

#include <sys/queue.h> 

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

/* Removed prototypes and non related code for brevity */

int
main()
{
    char    *cmd = NULL; 
    unsigned int acct = 0; 
    int amount = 0; 
    int done = 0; 

    while (done==0) {
        scanf ("%s %u %i", cmd, &acct, &amount);

        if (strcmp (cmd, "exit") == 0)
            done = 1;
        else if ((strcmp (cmd, "dep") == 0) || (strcmp (cmd, "deb") == 0))
            debit (acct, amount);
        else if ((strcmp (cmd, "wd") == 0) || (strcmp (cmd, "cred") == 0))
            credit (acct, amount);
        else if (strcmp (cmd, "fee") == 0)
            service_fee(acct, amount);
        else
            printf("Invalid input!\n");
    }
    return(0);
}

void
credit(unsigned int acct, int amount)
{
}

void
debit(unsigned int acct, int amount)
{
}

void
service_fee(unsigned int acct, int amount)
{
}

В нынешнем виде вышеизложенное не генерирует ошибок при компиляции, но дает мне ошибку при запуске. Я могу исправить это, изменив программу на передачу cmd по ссылке при вызове scanf и strcmp. Segfault исчезает и заменяется предупреждениями для каждого использования strcmp во время компиляции. Несмотря на предупреждения, уязвимый код работает.

предупреждение: передача аргумента 1 из 'strcmp' из несовместимого типа указателя

В качестве дополнительного бонуса, изменение вызовов scanf и strcmp позволяет программе продвинуться достаточно далеко, чтобы выполнить return (0), и в этот момент происходит сбой с прерыванием Abort. Если я заменю return (0) на exit (0), тогда все будет работать как положено.

Это оставляет мне два вопроса: почему оригинальная программа была неправильной? Как я могу исправить это лучше, чем у меня?

Меня немного сбивает с толку необходимость использовать выход вместо возврата.

Ответы [ 9 ]

11 голосов
/ 09 июня 2010

Это происходит из-за оператора scanf.

Посмотрите, как cmd указывает на NULL.Когда запускается scanf, он пишет по адресу cmd, который равен NULL, и, таким образом, генерирует segfault.

Решение состоит в том, чтобы создать буфер для cmd, например:

char cmd[20];

Теперь ваш буфер может содержать 20 символов.Однако теперь вам нужно беспокоиться о переполнении буфера, если пользователь вводит более 20 символов.

Добро пожаловать в C.

РЕДАКТИРОВАТЬ. Также обратите внимание, что ваш кредит, дебет и плата за обслуживание функционируютне будет работать, как ожидалось, как вы их написали.Это связано с тем, что параметры передаются по значению , а не по ссылке.Это означает, что после возврата метода любые изменения будут отменены.Если вы хотите, чтобы они изменили аргументы, которые вы задаете, попробуйте изменить методы на:

void credit (unsigned int * acct, int * amount)

И затем вызывайте их как:

credit(&acct, &amt);

В этом случае параметры будут передаваться по ссылке, что означает, что любые изменения, внесенные в функцию кредитования, будут влиять на параметры даже после ее возврата.

7 голосов
/ 09 июня 2010

Вы не выделяете память для cmd, поэтому NULL.

Попробуйте объявить его с пробелом:

char cmd[1000];
5 голосов
/ 09 июня 2010

Как уже отмечали другие, вы ничего не выделили для сканирования. Но вы также должны проверить возвращаемое значение scanf:

if ( scanf ("%s %u %i", cmd, &acct, &amount) != 3 ) {
   // do some error handling
}

Функция scanf возвращает количество успешных преобразований, поэтому, если кто-то вводит XXXX, когда вы ожидаете целое число, вы захотите его обнаружить и обработать. Но, честно говоря, код пользовательского интерфейса, который использует scanf (), никогда не будет доказательством против такого рода вещей. На самом деле функция scanf () была предназначена для чтения отформатированных файлов, а не случайного ввода от людей.

4 голосов
/ 09 июня 2010

Это:

char    *cmd = NULL; 


Должно быть:

char cmd[100]; 



Обратите внимание: Вы должны убедиться, что строка, введенная пользователем в cmd, имеет длину меньше 100 или n

2 голосов
/ 09 июня 2010

cmd инициализируется нулевым указателем, который никогда не указывает на какую-либо память.scanf не проверяет допустимость cmd, прежде чем пытаться записать, на что указывает cmd.

Предварительное решение вместо этого создает некоторое пространство для указания cmd:

char cmd[30]; /* DANGEROUS! */

, ноэто очень опасное движение, потому что вы все равно можете получить ошибки по умолчанию, если ввод длиннее, чем ожидалось, и scanf пытается записать в cmd [30] и далее.

По этой причине scanfсчитается небезопасным и не должен использоваться в производственном коде.Более безопасные альтернативы включают использование fgets для чтения строки ввода и sscanf для ее обработки.

К сожалению, CI / O очень трудно сделать правильно, не представляя возможности переполнения буфера в вашей программе.Вы всегда должны думать о том, сколько памяти у вас есть, и будет ли этого достаточно для хранения максимально длинного ввода, который вы можете получить.Вам также необходимо проверить возвращаемые значения большинства функций ввода / вывода на наличие ошибок.

2 голосов
/ 09 июня 2010

В вашем примере, scanf() передается нулевой указатель.

char    *cmd = NULL; 

scanf () не будет выделять место для строки - вам нужно будет выделить место для строки.

char   cmd[80];
...
scanf ("%s",cmd);

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

1 голос
/ 09 июня 2010

Ваша основная проблема в том, что вы не выделили память для своей строки. В C вы несете ответственность за все управление памятью. Если вы объявляете переменные в стеке, это легко. С указателями это немного сложнее. Поскольку у вас есть строка char* str = NULL, когда вы пытаетесь ввести в нее scanf, вы записываете байты в NULL, что недопустимо. Спецификатор %s записывает то, на что указывает str; он не может изменить str, так как параметры передаются по значению. Вот почему вы должны передать &acct вместо acct.

Так как вы это исправите? Вы должны предоставить память, где может жить читаемая строка. Что-то вроде char str[5] = "". Это делает str массивом из пяти элементов, достаточно большим, чтобы содержать «выход» и его завершающий нулевой байт. (Массивы распадаются на указатели при малейшей провокации, поэтому у нас все в порядке.) Однако это опасно. Если пользователь вводит строку malicious, вы собираетесь записать "malic" в str и байты для "icious\0" во все, что идет после этого в памяти. Это переполнение буфера, и это классическая ошибка. Самый простой способ исправить это - потребовать от пользователя ввести команду, состоящую не более чем из N букв, где N - самая длинная команда, которую вы имеете; в этом случае N = 4. Затем вы можете указать scanf, что нужно прочитать не более четырех символов: scanf("%4s %u %i", cmd, &acct, &amt). %4s гласит «читать максимум четыре символа», поэтому вы не можете испортить другую память. Однако обратите внимание, что если пользователь введет malformed 3 4, вы не сможете найти 3 и 4, так как вы будете смотреть на ormed.

Причина, по которой вы можете сделать scanf("%s %u %i", &cmd, &acct, &amount), заключается в том, что C не является типобезопасным. Когда вы дали ему &cmd, вы дали ему char**; тем не менее, он был счастлив рассматривать это как char*. Таким образом, он записал байты сверх cmd, поэтому, если вы передали строку exit, cmd может (если бы она была шириной в четыре байта и имела соответствующий порядковый номер) быть равной 0x65786974 (0x65 = e, 0x78 = x, 0x69 = i, 0x74 = t). И затем нулевой байт или любые другие байты, которые вы передали, вы начинаете записывать поверх случайной памяти. Если вы также измените его на strcmp, то также будет обрабатывать значение из str как строку, и все будет согласованно. Что касается того, почему return 0; терпит неудачу, но exit(0) работает, я не уверен, но у меня есть предположение: вы, возможно, писали по обратному адресу main. Он также хранится в стеке, и если он окажется после cmd в макете стека, вы можете обнулить его или записать его. Теперь exit должен выполнить очистку вручную, перепрыгивая в нужные места и т. Д. Однако, если (как я думаю, дело обстоит, хотя я не уверен), main ведет себя как любая другая функция, ее return переходит на место в стеке, хранящемся в качестве адреса возврата (что, вероятно, является какой-то процедурой очистки). Однако, так как вы набросались на это, вы получаете прерывание.

Теперь есть пара небольших улучшений, которые вы можете сделать. Во-первых, поскольку вы рассматриваете done как логическое значение, вы должны зациклить while (!done) { ... }. Во-вторых, текущая настройка требует, чтобы вы написали exit 1 1 для выхода из программы, хотя бит 1 1 не должен быть необходим. В-третьих, вы должны проверить, успешно ли вы прочитали все три аргумента, чтобы не было ошибок / несоответствий; например, если вы не исправите это, то введите

deb 1 2
deb 3 a

Вызывает debit(1,2) и debit(3,2), оставляя при этом a на входе, чтобы сбить вас с толку. Наконец, вы должны выйти из EOF аккуратно, а не зацикливаться на вечности, выполняя последнее, что вы сделали. Если мы соберем это вместе, мы получим следующий код:

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

void credit(unsigned int acct, int amount);
void debit(unsigned int acct, int amount);
void service_fee(unsigned int acct, int amount);

int main() {
  char         cmd[5] = ""; 
  unsigned int acct   = 0; 
  int          amount = 0; 
  int          done   = 0; 

  while (!done) {
    if (feof(stdin)) {
      done = 1;
    } else {
      if (scanf("%4s", cmd, &acct) != 1) {
        fprintf(stderr, "Could not read the command!\n");
        scanf(" %*s "); /* Get rid of the rest of the line */
        continue;
      }

      if (strcmp(cmd, "exit") == 0) {
        done = 1;
      } else {
        if (scanf(" %u %i", &acct, &amount) != 2) {
          fprintf(stderr, "Could not read the arguments!\n");
          scanf(" %*s "); /* Get rid of the rest of the line */
          continue;
        }

        if ((strcmp(cmd, "dep") == 0) || (strcmp(cmd, "deb") == 0))
          debit(acct, amount);
        else if ((strcmp(cmd, "wd") == 0) || (strcmp(cmd, "cred") == 0))
          credit(acct, amount);
        else if (strcmp(cmd, "fee") == 0)
          service_fee(acct, amount);
        else
          fprintf(stderr, "Invalid input!\n");
      }
    }
    /* Cleanup code ... */
  }

  return 0;
}

/* Dummy function bodies */

void credit(unsigned int acct, int amount) {
  printf("credit(%u, %d)\n", acct, amount);
}

void debit(unsigned int acct, int amount) {
  printf("debit(%u, %d)\n", acct, amount);
}

void service_fee(unsigned int acct, int amount) {
  printf("service_fee(%u, %d)\n", acct, amount);
}

Обратите внимание, что если не существует «кода очистки», вы можете заменить все ваши варианты использования done на break и удалить объявление done, что даст более приятный цикл

while (1) {
  if (feof(stdin)) break;

  if (scanf("%4s", cmd, &acct) != 1) {
    fprintf(stderr, "Could not read the command!\n");
    scanf(" %*s "); /* Get rid of the rest of the line */
    continue;
  }

  if (strcmp(cmd, "exit") == 0) break;

  if (scanf(" %u %i", &acct, &amount) != 2) {
    fprintf(stderr, "Could not read the arguments!\n");
    scanf(" %*s "); /* Get rid of the rest of the line */
    continue;
  }

  if ((strcmp(cmd, "dep") == 0) || (strcmp(cmd, "deb") == 0))
    debit(acct, amount);
  else if ((strcmp(cmd, "wd") == 0) || (strcmp(cmd, "cred") == 0))
    credit(acct, amount);
  else if (strcmp(cmd, "fee") == 0)
    service_fee(acct, amount);
  else
    fprintf(stderr, "Invalid input!\n");
}
1 голос
/ 09 июня 2010

Другие указали на ошибку в вашей программе, но для лучшего понимания указателей, так как вы только начинаете изучать C, посмотрите на этот вопрос на SO.

0 голосов
/ 09 июня 2010

Чтобы полностью понять, что здесь происходит, вам нужно понять некоторые основы о C-указателях. Я предлагаю вам взглянуть здесь, если вы действительно плохо знакомы с C:

http://www.cprogramming.com/tutorial.html#ctutorial

Наиболее распространенная причина segfaults подробно здесь:

http://www.cprogramming.com/debugging/segfaults.html

...