Базовый шаблон для начало программы C


Я разработал базовый шаблон для начало программы C. Он пытается реализовать следующее:

  1. Обработка выделения памяти и ошибки
  2. Ведение журнала сообщений
  3. Безопасного копирования строки
  4. Безопаснее конкатенации строк
  5. Среда тестирования
  6. Обработка ошибок

Как это выглядит?

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

/*Macros for constants*/
#define INIT_BUF_SIZE 10000
#define MAX_INT_STR_LEN 20

/*Function-like macros*/
#define MALLOC(newbuf, bufsize, logbuf) if ((rc = str_malloc(newbuf, bufsize, \
        logbuf)) != SUCCESS) return rc
#define CAT(dest, source, logbuf) if ((rc = str_cat(dest, source, \
        logbuf)) != SUCCESS) return rc
#define TESTCASE(func, logbuf, testcount) if ((rc = func(logbuf)) != SUCCESS) \
                                                    return rc; testcount++
/*Return codes*/
typedef enum {
  SUCCESS,
  ERR_STR_COPY,
  ERR_STR_CAT,
  ERR_STR_MALLOC,
  ERR_RC_NOT_SET } td_rc;

/*Dynamically allocated string with allocated size */
struct strbuff{
  size_t size;
  char *str;
};

/*Function prototypes*/

/*Convenience wrapper around malloc with optional log parameter*/
td_rc str_malloc(struct strbuff *newbuf, size_t bufsize, 
    struct strbuff *logbuf);
/*Safer replacement for strcpy with optional log parameter*/
td_rc str_copy(struct strbuff *dest, char *source, struct strbuff *logbuf);
/*Safer replacement for strcat with optional log parameter*/
td_rc str_cat(struct strbuff *dest, char *source, struct strbuff *logbuf);
/*Test runner*/
td_rc test_all(struct strbuff *logbuf);
/*Test cases*/
td_rc test_str_malloc(struct strbuff *logbuf);
td_rc test_str_copy(struct strbuff *logbuf);
td_rc test_str_cat(struct strbuff *logbuf);

/*Output HTTP header and log messages*/
int main(int argc, char** argv){
  td_rc rc = ERR_RC_NOT_SET;
  struct strbuff logbuf;
  MALLOC(&logbuf, INIT_BUF_SIZE, NULL);
  logbuf.size = INIT_BUF_SIZE;
  if ((rc = str_copy(&logbuf, "content-type: text/html\n\n", NULL)) 
      == SUCCESS) {
    if (argc > 1)
      if (strcmp(argv[1], "test") == 0)
        /*If the first parameter is "test", run all test cases*/
        rc = test_all(&logbuf);
    printf("%s", logbuf.str);
  }
  free(logbuf.str);
  return rc;
}

td_rc str_malloc(struct strbuff *newbuf, size_t bufsize, 
    struct strbuff *logbuf){
  td_rc rc = ERR_RC_NOT_SET;
  newbuf->str = malloc(sizeof *newbuf->str * bufsize);
  if (!newbuf->str) {
    /*If malloc fails, write error to log if supplied
      otherwise write to standard output*/
    char errmes[100];
    sprintf(errmes, "malloc failed : size requested %lu \n", 
        (unsigned long)bufsize); 
    if (!logbuf)
      printf("%s", errmes);
    else CAT(logbuf, errmes, NULL);
    return ERR_STR_MALLOC;
  }
  /*Terminate the new empty string*/
  *newbuf->str = '\0';
  newbuf->size = bufsize;
  return SUCCESS;
} 

td_rc str_copy(struct strbuff *dest, char *source, struct strbuff *logbuf){
  size_t sourcelen = strlen(source);
  td_rc rc = ERR_RC_NOT_SET;
  /**dest should have been initialized with str_malloc*/
  if (sourcelen > dest->size) {
    /*If if the source string is longer than the destion buffer, 
     * write error to log if supplied otherwise write to standard output*/
    char errmes[100];
    sprintf(errmes, "string copy failed : source length %lu > \
        dest allocated space %lu\n", (unsigned long)sourcelen, \
        (unsigned long)dest->size); 
    if (!logbuf)
      printf("%s", errmes);
    else CAT(logbuf, errmes, NULL);
    return ERR_STR_COPY;
  }
  memcpy(dest->str, source, sourcelen+1);
  return SUCCESS;
} 

td_rc str_cat(struct strbuff *dest, char *source, struct strbuff *logbuf){
  td_rc rc = ERR_RC_NOT_SET;
  /**dest should have been initialized with str_malloc*/
  size_t destlen = strlen(dest->str);
  size_t destfree = dest->size - destlen;
  size_t sourcelen = strlen(source);
  if (sourcelen > destfree) {
    /*If the source string is longer than the free space in the 
     * destination buffer, write error to log if supplied otherwise write 
     * to standard output*/
    char errmes[100];
    sprintf(errmes, "string concatenation failed : source length %lu > \
        dest free space %lu\n", (unsigned long)sourcelen, \
        (unsigned long)destfree); 
    rc = ERR_STR_CAT;
    if (!logbuf)
      printf("%s", errmes);
    else 
      rc = str_cat(logbuf, errmes, NULL);
  } else { 
    memcpy(dest->str+destlen, source, sourcelen+1);
    rc = SUCCESS;
  }
  return rc;
} 

td_rc test_all(struct strbuff *logbuf){
  td_rc rc = ERR_RC_NOT_SET;
  int testcount = 0;
  char rc_str[MAX_INT_STR_LEN];
  /*If a test case fails, stop further processing and output the log, 
   * otherwise output a test summary*/
  TESTCASE(test_str_malloc, logbuf, testcount);
  TESTCASE(test_str_copy, logbuf, testcount);
  TESTCASE(test_str_cat, logbuf, testcount);
  sprintf(rc_str, "%d", testcount);
  CAT(logbuf, "\n", NULL);
  CAT(logbuf, rc_str, NULL);
  CAT(logbuf, " tests completed successfully\n", NULL);
  return SUCCESS;
}

td_rc test_str_malloc(struct strbuff* logbuf){
  td_rc rc = ERR_RC_NOT_SET;
  struct strbuff teststr;
  MALLOC(&teststr, 5, logbuf);
  if (teststr.size == 5)
    rc = SUCCESS;
  else rc = ERR_STR_MALLOC;
  free(teststr.str);
  return rc;
}

td_rc test_str_copy(struct strbuff* logbuf){
  td_rc rc = ERR_RC_NOT_SET;
  struct strbuff teststr;
  MALLOC(&teststr, 7, logbuf);
  if ((rc = str_copy(&teststr, "foo", logbuf)) == SUCCESS) {
    if (strcmp(teststr.str, "foo")) 
      rc = ERR_STR_COPY;
    else {
      if ((rc = str_copy(&teststr, "bar", logbuf)) == SUCCESS) {
        if (strcmp(teststr.str, "bar")) 
          rc = ERR_STR_COPY;
      }
    }
  }
  free(teststr.str);
  return rc;
}

td_rc test_str_cat(struct strbuff* logbuf){
  td_rc rc = ERR_RC_NOT_SET;
  struct strbuff teststr;
  MALLOC(&teststr, 7, logbuf);
  if ((rc = str_cat(&teststr, "foo", logbuf)) == SUCCESS) {
    if (strcmp(teststr.str, "foo")) 
      rc = ERR_STR_CAT;
    else {
      if ((rc = str_cat(&teststr, "bar", logbuf)) == SUCCESS) {
        if (strcmp(teststr.str, "foobar")) 
          rc = ERR_STR_CAT;
      }
    }
  }
  free(teststr.str);
  return rc;
}


288
4
задан 28 января 2018 в 11:01 Источник Поделиться
Комментарии
2 ответа

Я бы сказал, что у тебя дела "решением в поисках проблемы" здесь — и ваши решения являются в основном неисправен!


#define MALLOC(newbuf, bufsize, logbuf) if ((rc = str_malloc(newbuf, bufsize, \
logbuf)) != SUCCESS) return rc

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

if (useSmallBuffer)
MALLOC(mybuf, 100, logbuf);
else
MALLOC(mybuf, 200, logbuf);

(Разверните его. Вы понимаете, почему поток управления идет не так?)

Всякий раз, когда ты пишешь "заявление-как" макросы в C или C++, вы должны обернуть его в do { ... } while (0) для гигиены.

Также, рассмотрим, что происходит с

int ra = 1, rb = 2, rc = 3;
MALLOC(mybuf, 3, logbuf);
mybuf[0] = ra;
mybuf[1] = rb;
mybuf[2] = rc;

Макросы, как правило, не должен принимать "скрытые" или "неявное" параметров.


/*Macros for constants*/
#define INIT_BUF_SIZE 10000
#define MAX_INT_STR_LEN 20

Эти макросы с именами излишне загадочные, и их целью является неясным. Зачем они вам нужны вообще?

Единственное место, где вы использовать INIT_BUF_SIZE в main

MALLOC(&logbuf, INIT_BUF_SIZE, NULL);
logbuf.size = INIT_BUF_SIZE;

— это означает, что , по крайней мере, ее масштабы должны быть ограничены main.

const size_t initial_logbuf_size = 10000;
MALLOC(&logbuf, initial_logbuf_size, NULL);
logbuf.size = initial_logbuf_size;

Следующее, что мы замечаем об этом фрагменте заключается в том, что мы сидим logbuf.size = K сразу же после вызова MALLOC(&logbuf, k, ...). Это пахнет ужасно, как мы пытаемся сохранить инвариант здесь — например, "foo.size всегда содержит Танос объед длину буфера foo." Мы должны переместить код, который сохраняет, что инвариант в MALLOC макро.

#define MALLOC(newbuf, bufsize, logbuf) \
do { \
int rc = str_malloc(newbuf, bufsize, logbuf); \
if (rc != SUCCESS) return rc; \
(newbuf)->size = (bufsize); \
} while (0)

Заметьте, что я поместил в скобки макрос аргументы (newbuf) и (bufsize) когда я использовал их в макрос сам.


Работаю над этим макро, что-то бросилось в глаза. Теперь у нас есть какая-то логика ((newbuf)->size = (bufsize);) помещают после начала return заявление. И мы используем сырые указатели (вместо что-то вроде C++'ы unique_ptr<T>) для управления нашей памятью. Это очень похоже на рецепт утечка памяти! Но, конечно, такой же запах был там перед этим изменением; это был просто замаскирован излишней сложности кода. Рассмотрим:

void foo() {
int rc = 0;
struct strbuff buffer1, buffer2;
MALLOC(&buffer1, 1000, NULL); // possible early return
MALLOC(&buffer2, 1000, NULL); // possible early return
free(buffer1.str);
free(buffer2.str);
}

Если базовый malloc из buffer2 терпит неудачу и возвращает NULLтогда буфер, на который указывает buffer1 будет утечка!

Чтобы справиться с освобождение ресурсов в случае досрочного возврата, с программистами, как правило, используют шаблон, как goto cleanup; — смотри, например, этот вопрос так или яблока знаменитая "Гото ошибкой" ошибка.


Любой раз, когда я смотрю на код на языке C, я специально смотрю на то, как они управляют своими буферами: они правильно передавая по кругу длины буфера, чтобы избежать переполнения буфера? Это не обязательно означает, используя глупые вспомогательные функции, такие как strcpy_s- это просто означает не делать что-либо ужасно тупой.

Поэтому, когда я вижу sprintfв коде, который специально предназначен для защиты от переполнения буфера, это предупреждение колокола:

char errmes[100];
sprintf(errmes, "string concatenation failed : source length %lu > \
dest free space %lu\n", (unsigned long)sourcelen, \
(unsigned long)destfree);

Теперь, на первый взгляд, самая длинная возможная строка может быть печать в буфер errmes здесь "string concatenation failed : source length 4000000000 > dest free space 4000000000\n", который находится всего в 84 символов. Так это нормально, да? У нас есть 15 байт "ирисок" осталось! Нет проблем! Поэтому мы сопротивляемся ангел на нашем плече умоляли нас, чтобы использовать правильный переполнения буфера-доказательства строительства,

char errmes[100];
snprintf(errmes, sizeof errbuf, "string concatenation failed : source length %lu > \
dest free space %lu\n", (unsigned long)sourcelen, \
(unsigned long)destfree);

Уходи, ангел!

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

char errmes[100];
sprintf(errmes, "WARNING: string concatenation failed : source length %lu > \
dest free space %lu\n", (unsigned long)sourcelen, \
(unsigned long)destfree);

Здесь мы добавили 9 байт на строку: "WARNING: " (не забывайте, что дополнительное пространство мы добавили!). Мы добавляем это все снова и определить, что максимальная длина строки составляет сейчас 84+9 = 93 байт. Нет проблем.

За исключением того, что там, где есть одна ошибка, там обычно два. Ошибки, вызванные безалаберностью, беспечностью и не один раз. Снова посмотрим на эту строку. Сколько персонажей действительно в этой строке?

Вы заметили обратную косую черту, что означает продолжение строки? Формат строки в исходный код на самом деле выглядит так:

"string concatenation failed : source length %lu >         dest free space %lu\n"

Это 8 байт больше, чем мы думали, что это было! Так с "WARNING: " добавил, Это не переполнение буфера, и мы получаем уязвимости безопасности.

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


Наконец, давайте посмотрим на str_cat один больше времени. Я //... все детали, которые не интересуют меня сейчас...

td_rc str_cat(struct strbuff *dest, char *source, struct strbuff *logbuf){
//...
if (sourcelen > destfree) {
//...
if (!logbuf)
printf("%s", errmes);
else
rc = str_cat(logbuf, errmes, NULL);
} else {
//...
}
//...
}

Да, это верно — ты сумел написать рекурсивную конкатенации строк рутины! str_cat называет себя!

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

Ваш logbuf параметр является, по сути, попытка разрешить пользователю настраивать свои библиотеки механизмом сообщения об ошибке. Если вы действительно заинтересованы в этом маршруте, я рекомендую вам узнать про указатели на функции, которые позволит вам написать библиотеку функций такой:

typedef void (*error_reporter)(const char *, ...);

td_rc strbuff_plusequals(struct strbuff *dst, struct strbuff src, error_reporter report_error)
{
const size_t dstlen = strlen(dst->str);
const size_t srclen = strlen(src.str);
if (srclen >= dst->size - dstlen) {
report_error("string concatenation failed: source length %zu > dest free space %zu", srclen, dst->size - dstlen);
return ERR_STR_CAT;
} else {
memcpy(dst->str + dstlen, src.str, srclen + 1);
return SUCCESS;
}
}

struct strbuff strbuff_view(const char *s) {
return (struct strbuff){ strlen(s), (char *)s };
}

void report(const char *fmt, ...);

int main() {
struct strbuff s;
int err = strbuff_create(&s, 100);
if (err) exit(err);
err = strbuff_plusequals(&s, strbuff_view("hello"), report);
if (err) exit(err);
err = strbuff_plusequals(&s, strbuff_view(" world"), report);
if (err) exit(err);
assert(strcmp(s.str, "hello world") == 0);
strbuff_destroy(s);
}

Написание strbuff_create и strbuff_destroy осталось в качестве упражнения для читателя. :)

Еще одно упражнение для читателя, чтобы добавить void *cookie параметр report_error.


В любом случае, я бы сказал, что вы должны узнать о snprintf и strcpy и так далее, прежде чем пытаться изобретать свой собственный обработки строк библиотеки. Начните с инструментов, которые предоставляются для вас и вам хорошо с ними, первый.

8
ответ дан 29 января 2018 в 05:01 Источник Поделиться

Некоторые идеи лучше в качестве ответа, чем комментарий

Слабость rc = str_cat(logbuf, errmes, NULL); по крайней мере, концептуально - код вызывает сбой функции. Код в обработчик ошибок функция str_cat() и есть что-то неправильно, кто знает, что основной причиной. Звоню str_cat() опять-таки рискованно. Лучше получить сообщение об ошибке из первого.

Если snprintf() не доступен, с помощью щедрых правильного размера буфера, а не магическое число 100 является разумным, исходя из формата и это в худшем случае аргументы. Пример: с long как 64-разрядных, 111 необходимо в коде (см. ниже). ИМО, щедрой буфера в 2 раза, что вы думаете, что это может быть.

string copy failed : source length 18446744073709551615 >           dest allocated space 18446744073709551615 


А не

    char errmes[100];
sprintf(errmes, "string copy failed : source length %lu > \
dest allocated space %lu\n", (unsigned long)sourcelen, \
(unsigned long)dest->size);

Рассмотрим

    #define ERR_STR_CPY "string copy failed : source length %lu > \
dest allocated space %lu\n"
// /3 is about log10(2)
#define ULONG_DECIMAL_LEN (sizeof(unsigned long)*CHAR_BITS/3 + 2)
#define ERR_STR_CPY_N (sizeof ERR_STR_CPY + 2*ULONG_DECIMAL_LEN)
char errmes[ERR_STR_CPY_N*2];
sprintf(errmes, ERR_STR_CPY, (unsigned long)sourcelen, (unsigned long)dest->size);


Непонятно, наверное, ошибка, что ОП так много пространства в форме ниже

    // long format?
sprintf(errmes, "string copy failed : source length %lu > \
dest allocated space %lu\n", (unsigned long)sourcelen, \
(unsigned long)dest->size);

// wanted format?
sprintf(errmes, "string copy failed : source length %lu > "
"dest allocated space %lu\n", (unsigned long)sourcelen,
(unsigned long)dest->size);

Именно поэтому программист компьютерной магические числа, такие как 100 и вычисленные программой значения рискованно.

4
ответ дан 30 января 2018 в 05:01 Источник Поделиться