Не так давно у со мной произошел довольно-таки интересный инцидент, в котором был замешан один из преподавателей одного колледжа информатики.
Разговор о программировании под Linux медленно перешел к тому, что этот человек стал утверждать, что сложность системного программирования на самом деле сильно преувеличена. Что язык Си прост как спичка, собственно как и ядро Linux (с его слов).
У меня был с собой ноутбук с Linux, на котором присутствовал джентльменский набор утилит для разработки на языке Си (gcc, vim, make, valgrind, gdb). Я уже не помню, какую цель мы тогда перед собой поставили, но через пару минут мой оппонент оказался за этим ноутбуком, полностью готовый решать задачу.
И буквально на первых же строках он допустил серьезную ошибку при аллоцировании памяти под… строку.
char *str = (char *)malloc(sizeof(char) * strlen(buffer));
buffer — стековая переменная, в которую заносились данные с клавиатуры.
Я думаю, определенно найдутся люди, которые спросят: «Разве что-то тут может быть не так?».
Поверьте, может.
А что именно — читайте по катом.
Немного теории — своеобразный ЛикБез.
Если знаете — листайте до следующего хэдера.
Строка в C — это массив символов, который по-хорошему всегда должен заканчиваться '\0' — символом конца строки. Строки на стеке (статичные) объявляются вот так:
char str[n] = { 0 };
n — размер массива символов, то же, что и длина строки.
Присваивание { 0 } — «зануление» строки (опционально, объявлять можно и без него). Результат такой же, как у выполнения функций memset(str, 0, sizeof(str)) и bzero(str, sizeof(str)). Используется, чтобы в неинициализированных переменных не валялся мусор.
Так же на стеке можно сразу проинициализировать строку:
char buf[BUFSIZE] = "default buffer text\n";
Помимо этого строку можно объявить указателем и выделить под нее память на куче (heap):
char *str = malloc(size);
size — количество байт, которые мы выделяем под строку. Такие строки называются динамическими (вследствие того, что нужный размер вычисляется динамически + выделенный размер памяти можно в любой момент увеличить с помощью функции realloc() ).
В случае со стековой переменной, для определения размера массива я использовал обозначение n, в случае с переменной на куче — я использовал обозначение size. И это прекрасно отражает истинную суть отличия объявления на стеке от объявление с аллоцированием памяти на куче, ведь n как правило используется тогда, когда говорят о количестве элементов. А size — это уже совсем другая история…
Думаю. пока хватит. Идем дальше.
Нам поможет valgrind
В своей предыдущей статье я также упоминал о нем. Valgrind (раз — вики-статья, два — небольшой how-to) — очень полезная программа, которая помогает программисту отслеживать утечки памяти и ошибки контекста — как раз те вещи, которые чаще всего всплывают при работе со строками.
Давайте рассмотрим небольшой листинг, в котором реализовано что-то похожее на упомянутую мной программу, и прогоним ее через valgrind:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HELLO_STRING "Hello, Habr!\n"
void main() {
char *str = malloc(sizeof(char) * strlen(HELLO_STRING));
strcpy(str, HELLO_STRING);
printf("->\t%s", str);
free(str);
}
И, собственно, результат работы программы:
[indever@localhost public]$ gcc main.c
[indever@localhost public]$ ./a.out
-> Hello, Habr!
Пока ничего необычного. А теперь давайте запустим эту программу с valgrind!
[indever@localhost public]$ valgrind --tool=memcheck ./a.out
==3892== Memcheck, a memory error detector
==3892== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==3892== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==3892== Command: ./a.out
==3892==
==3892== Invalid write of size 2
==3892== at 0x4005B4: main (in /home/indever/prg/C/public/a.out)
==3892== Address 0x520004c is 12 bytes inside a block of size 13 alloc'd
==3892== at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==3892== by 0x400597: main (in /home/indever/prg/C/public/a.out)
==3892==
==3892== Invalid read of size 1
==3892== at 0x4C30BC4: strlen (vg_replace_strmem.c:454)
==3892== by 0x4E89AD0: vfprintf (in /usr/lib64/libc-2.24.so)
==3892== by 0x4E90718: printf (in /usr/lib64/libc-2.24.so)
==3892== by 0x4005CF: main (in /home/indever/prg/C/public/a.out)
==3892== Address 0x520004d is 0 bytes after a block of size 13 alloc'd
==3892== at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==3892== by 0x400597: main (in /home/indever/prg/C/public/a.out)
==3892==
-> Hello, Habr!
==3892==
==3892== HEAP SUMMARY:
==3892== in use at exit: 0 bytes in 0 blocks
==3892== total heap usage: 2 allocs, 2 frees, 1,037 bytes allocated
==3892==
==3892== All heap blocks were freed -- no leaks are possible
==3892==
==3892== For counts of detected and suppressed errors, rerun with: -v
==3892== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)
==3892== All heap blocks were freed — no leaks are possible — утечек нет, и это радует. Но стоит опустить глаза чуть пониже (хотя, хочу заметить, это лишь итог, основная информация немного в другом месте):
==3892== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)
3 ошибки. В 2х контекстах. В такой простой программе. Как!?
Да очень просто. Весь «прикол» в том, что функция strlen не учитывает символ конца строки — '\0'. Даже если его явно указать во входящей строке (#define HELLO_STRING «Hello, Habr!\n\0»), он будет проигнорирован.
Чуть выше результата исполнения программы, строки -> Hello, Habr! есть подробный отчет, что и где не понравилось нашему драгоценному valgrind. Предлагаю самостоятельно посмотреть эти строчки и сделать выводы.
Собственно, правильная версия программы будет выглядеть так:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HELLO_STRING "Hello, Habr!\n"
void main() {
char *str = malloc(sizeof(char) * (strlen(HELLO_STRING) + 1));
strcpy(str, HELLO_STRING);
printf("->\t%s", str);
free(str);
}
Пропускаем через valgrind:
[indever@localhost public]$ valgrind --tool=memcheck ./a.out
-> Hello, Habr!
==3435==
==3435== HEAP SUMMARY:
==3435== in use at exit: 0 bytes in 0 blocks
==3435== total heap usage: 2 allocs, 2 frees, 1,038 bytes allocated
==3435==
==3435== All heap blocks were freed -- no leaks are possible
==3435==
==3435== For counts of detected and suppressed errors, rerun with: -v
==3435== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Отлично. Ошибок нет, +1 байт выделяемой памяти помог решить проблему.
Что интересно, в большинстве случаев и первая и вторая программа будут работать одинаково, но если память, выделенная под строку, в которую не влез символ окончания, не была занулена, то функция printf(), при выводе такой строки, выведет и весь мусор после этой строки — будет выведено все, пока на пути printf() не встанет символ окончания строки.
Однако, знаете, (strlen(str) + 1) — такое себе решение. Перед нами встают 2 проблемы:
- А если нам надо выделить память под формируемую с помощью, например, s(n)printf(..) строку? Аргументы мы не поддерживаем.
- Внешний вид. Строка с объявлением переменной выглядит просто ужасно. Некоторые ребята к malloc еще и (char *) умудряются прикручивать, будто под плюсами пишут. В программе где регулярно требуется обрабатывать строки есть смысл найти более изящное решение.
Давайте придумаем такое решение, которое удовлетворит и нас, и valgrind.
snprintf()
int snprintf(char *str, size_t size, const char *format, ...);
— функция — расширение sprintf, которая форматирует строку и записывает ее по указателю, переданному в качестве первого аргумента. От sprintf() она отличается тем, что в str не будет записано байт больше, чем указано в size.Функция имеет одну интересную особенность — она в любом случае возвращает размер формируемой строки (без учета символа конца строки). Если строка пустая, то возвращается 0.
Одна из описанных мною проблем использования strlen связана с функциями sprintf() и snprintf(). Предположим, что нам надо что-то записать в строку str. Конечная строка содержит значения других переменных. Наша запись должна быть примерно такой:
char * str = /* тут аллоцируем память */;
sprintf(str, "Hello, %s\n", "Habr!");
Встает вопрос: как определить, сколько памяти надо выделить под строку str?
char * str = malloc(sizeof(char) * (strlen(str, "Hello, %s\n", "Habr!") + 1));
— не прокатит. Прототип функции strlen() выглядит так:#include <string.h>
size_t strlen(const char *s);
const char *s не подразумевает, что передаваемая в s строка может быть строкой формата с переменным количеством аргументов.
Тут нам поможет то полезное свойство функции snprintf(), о котором я говорил выше. Давайте посмотрим на код следующей программы:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main() {
/* Т.к. snprintf() не учитывает символ конца строки, прибавляем его размер к результату */
size_t needed_mem = snprintf(NULL, 0, "Hello, %s!\n", "Habr") + sizeof('\0');
char *str = malloc(needed_mem);
snprintf(str, needed_mem, "Hello, %s!\n", "Habr");
printf("->\t%s", str);
free(str);
}
Запускаем программу в valgrind:
[indever@localhost public]$ valgrind --tool=memcheck ./a.out
-> Hello, Habr!
==4132==
==4132== HEAP SUMMARY:
==4132== in use at exit: 0 bytes in 0 blocks
==4132== total heap usage: 2 allocs, 2 frees, 1,041 bytes allocated
==4132==
==4132== All heap blocks were freed -- no leaks are possible
==4132==
==4132== For counts of detected and suppressed errors, rerun with: -v
==4132== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
[indever@localhost public]$
Отлично. Поддержка аргументов у нас есть. Благодаря тому, что мы в качестве второго аргумента в функцию snprintf() передаем ноль, запись по нулевому указателю никогда не приведет к Seagfault. Однако, несмотря на это функция все равно вернет необходимый под строку размер.
Но с другой стороны, нам пришлось завести дополнительную переменную, да и конструкция
size_t needed_mem = snprintf(NULL, 0, "Hello, %s!\n", "Habr") + sizeof('\0');
выглядит еще хуже, чем в случае с strlen().
Вообще, + sizeof('\0') можно убрать, если в конце строки формата явно указать '\0' (size_t needed_mem = snprintf(NULL, 0, «Hello, %s!\n\0», «Habr»);), но это возможно отнюдь не всегда (в зависимости от механизма обработки строк мы можем выделить лишний байт).
Надо что-то сделать. Я немного подумал и решил, что сейчас настал час воззвать к мудрости древних. Опишем макрофункцию, которая будет вызывать snprintf() с нулевым указателем в качестве первого аргумента, и нулем, в качестве второго. Да и про конец строки не забудем!
#define strsize(args...) snprintf(NULL, 0, args) + sizeof('\0')
Да, возможно, для кого-то будет новостью, но макросы в си поддерживают переменное количество аргументов, и троеточие говорит препроцессору о том, что указанному аргументу макрофункции (в нашем случае это args) соответствует несколько реальных аргументов.
Проверим наше решение на практике:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define strsize(args...) snprintf(NULL, 0, args) + sizeof('\0')
void main() {
char *str = malloc(strsize("Hello, %s\n", "Habr!"));
sprintf(str, "Hello, %s\n", "Habr!");
printf("->\t%s", str);
free(str);
}
Запускаем с valgrund:
[indever@localhost public]$ valgrind --tool=memcheck ./a.out
-> Hello, Habr!
==6432==
==6432== HEAP SUMMARY:
==6432== in use at exit: 0 bytes in 0 blocks
==6432== total heap usage: 2 allocs, 2 frees, 1,041 bytes allocated
==6432==
==6432== All heap blocks were freed -- no leaks are possible
==6432==
==6432== For counts of detected and suppressed errors, rerun with: -v
==6432== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Да, ошибок нет. Все корректно. И valgrind доволен, и программист наконец может пойти поспать.
Но, напоследок, скажу еще кое-что. В случае, если нам надо выделить память под какую-либо строку (даже с аргументами) есть уже полностью рабочее готовое решение.
Речь идет о функции asprintf:
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <stdio.h>
int asprintf(char **strp, const char *fmt, ...);
В качестве первого аргумента она принимает указатель на строку (**strp) и аллоцирует память по разыменованному указателю.
Наша программа, написанная с использованием asprintf() будет выглядеть так:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main() {
char *str;
asprintf(&str, "Hello, %s!\n", "Habr");
printf("->\t%s", str);
free(str);
}
И, собственно, в valgrind:
[indever@localhost public]$ valgrind --tool=memcheck ./a.out
-> Hello, Habr!
==6674==
==6674== HEAP SUMMARY:
==6674== in use at exit: 0 bytes in 0 blocks
==6674== total heap usage: 3 allocs, 3 frees, 1,138 bytes allocated
==6674==
==6674== All heap blocks were freed -- no leaks are possible
==6674==
==6674== For counts of detected and suppressed errors, rerun with: -v
==6674== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Все отлично, но, как видите, памяти всего было выделено больше, да и alloc'ов теперь три, а не два. На слабых встраиваемых системах использование это функции нежелательно.
К тому же, если мы напишем в консоли man asprintf, то увидим:
CONFORMING TO
These functions are GNU extensions, not in C or POSIX. They are also available under *BSD. The FreeBSD implementation sets strp to NULL on error.
Отсюда ясно, что данная функция доступна только в исходниках GNU.
Заключение
В заключение я хочу сказать, что работа со строками в C — это очень сложная тема, которая имеет ряд нюансов. Например, для написания «безопасного» кода при динамическом выделении памяти рекомендуется все же использовать функцию calloc() вместо malloc() — calloc забивает выделяемую память нулями. Ну или после выделения памяти использовать функцию memset(). Иначе мусор, который изначально лежал на выделяемом участке памяти, может вызвать вопросы при дебаге, а иногда и при работе со строкой.
Больше половины моих знакомых си-программистов (большинство из них — начинающие), решивших по моей просьбе задачу с выделением памяти под строки, сделали это так, что в конечном итоге это привело к ошибкам контекста. В одном случае — даже к утечке памяти (ну, забыл человек сделать free(str), с кем не бывает). Собственно говоря, это и сподвигло меня на создание сего творения, которое вы только что прочитали.
Я надеюсь, кому-то эта статья будет полезной. К чему я это все городил — никакой язык не бывает прост. Везде есть свои тонкости. И чем больше тонкостей языка вы знаете, тем лучше ваш код.
Я верю, что после прочтения этой статьи ваш код станет чуточку лучше :)
Удачи, Хабр!
Комментарии (118)
ServPonomarev
12.04.2017 16:13+3char *str = (char *)malloc(sizeof(char) * strlen(buffer));
И в чём проблема? Строка не обязательно должна заканчиваться нулём, если её длина известна и неизменна. Например, если мы меняем одни символы на другие, а потом сливаем результат в файл. Фиговатенько у автора с С.Indever2
12.04.2017 16:32Проблема в том, что подавляющее большинство строк в си, обработанных стандартными функциями, содержит в конце символ '\0'.
Хотите работать без него — тогда придется озаботиться использованием везде функций, которые ограничивают размер перемещаемой памяти (strncpy, snprintf, strncat), а в случае, когда у нас строки формируются из аргументов их использование (по назначению) не всегда возможно (только если не использовать подобие макроса strnsize, описанного в статье).
Это очень усложнит как процесс написания, так и процесс восприятия кода + количество функциональных вызовов на строку увеличится. Да и сам я не встречал проектов, где подобная практика используется :)Zibx
12.04.2017 22:13+3А между тем это очень правильная практика. Строка оканчивающаяся нулём пришла к нам из 60 годов. Великий замысел был в возможности сэкономить драгоценные байты памяти. За эту экономию мы до сих пор расплачиваемся стринг билдерами и переполнениями строк. Внезапно, сложение десяти строк через strcat даст экспоненциальную сложность, в то время как строки со счётчиком количества символов будут сложены с линейной (и та уйдёт на копирование байт). И никакого переполнения — циклы не будут выходить за допустимые рамки если они будут сразу знать длину строки.
grossws
13.04.2017 01:47Внезапно, сложение десяти строк через strcat даст экспоненциальную сложность, в то время как строки со счётчиком количества символов будут сложены с линейной (и та уйдёт на копирование байт).
Ну никто не мешает не использовать в цикле strcat, а посчитать и просуммировать длины, выделить один раз и сделать memcpy.
И никакого переполнения — циклы не будут выходить за допустимые рамки если они будут сразу знать длину строки.
Тоже не гарантирует. Куча багов во всяких парсерах (протоколов, бинарных контейнеров и т. п.) показывает, что зачастую несмотря на наличия явной длины в каком-нибудь поле всё равно умудряются совершать ошибки из этого класса.
kemm
13.04.2017 02:45-1А зачем вы конкатенируете строки через strcat(), я стесняюсь спросить, если вам хоть в какой-то мере интересна скорость? И откуда вы получаете строку, не узнавая там же её длину? Я только два места знаю: аргументы командной строки и переменные окружения. Оба случая не про скорость. Зато ASCIIZ строки безумно быстрые, если уметь читать доки. И, в отличие от «строк с длиной» позволяют избегать лишних выделений памяти или копирования.
iig
13.04.2017 10:38+1Зато ASCIIZ строки безумно быстрые, если уметь читать доки. И, в отличие от «строк с длиной» позволяют избегать лишних выделений памяти или копирования.
1. За счет чего более быстрые? копирование в любом случае происходит более-менее одинаково, а в zero-terminated строках длину надо дополнительно пересчитывать. А в вариантах, когда 1 символ >= 1 байт (UTF-8 если) — это реально долго.
2. Откуда лишние выделения памяти? Сосчитали, сколько памяти нужно — выделили. В обоих случаях одна операция, только в zero-terminated строках считать размер нужно дольше.grossws
13.04.2017 13:19+2А в вариантах, когда 1 символ >= 1 байт (UTF-8 если) — это реально долго.
С какого перепугу? Длина строки — это количество байт, а не количество codepoint'ов или графемных кластеров в ней.
iig
13.04.2017 13:38Пардону прошу, для копирования действительно не важно, сколько там символов. Для других действий со строками — важно. А бывает и критично. Вот, например, в некоторых языках есть буквы, в UPPERCASE и lowercase занимающие разное количество байт.
grossws
13.04.2017 13:52Для других действий со строками — важно. А бывает и критично.
В каких операциях вам важно/критично количество codepoint'ов в строке? В большинстве случаев вы будете итерироваться по codepoint'ам, порождать и записывать codepoint'ы в массив.
Вот, например, в некоторых языках есть буквы, в UPPERCASE и lowercase занимающие разное количество байт.
Не припомню такого, но даже если так, то что это меняет? Один и тот же графемный кластер
й
может занимать в utf8 и 2 байта, и 4.iig
13.04.2017 14:01+2Ну, сколько нужно памяти, чтобы записать строку в UPPER case? В общем случае — нужно считать. Если выделить sizeof(char)*strlen(src)+1 — рано или поздно оно выстрелит.
grossws
13.04.2017 14:08+1Считайте, что вы не можете этого сделать. Т. к. to_upper_case сложная локалезависимая операция, которая может производить частичную композицию/декомпозицию codepoint'ов.
А как основной подход к решению — выделять чуть больше и использовать функции, принимающие саму строку и максимальный размер (как snprintf). Ну и в случае, если не получилось — делать realloc.
Интересно, как сделано в icu-project, надо будет потом глянуть.
kemm
13.04.2017 13:321. За счёт того, что в ASCIIZ строках копирование не всегда нужно. Например, надо разбить строку по разделителю — мы можем просто менять разделитель на '\0' и дальше мы, во-первых, уже знаем длину (если нормально разбор пишем), во-вторых, так же можем использовать функции, которые принимают ASCIIZ, а не массив байт с длиной.
1.1. Чем utf-8 отличается? Длина считается так же (нам ж длина массива байт нужна, а не кол-во символов — во втором варианте всё вообще по-другому)
2. Либо мы выделяем место под {длина строки, строка} — выделение + копрование, либо под {длина строки, указатель на первый байт} — выделение + как реализовать c_str(), который обязан завершаться нулём? Зачастую это всё равно надо, но всё-таки не всегда в случае ASCIIZ. А размер считать не надо. Про подсчёт длины: строки не берутся божественным промыслом, длину мы почти всегда знаем, когда получаем строку откуда-нибудь (будь то возвращаемые значения из функций, данные протоколов или что угодно ещё). Исключений мало — либо по определению некритичные для производительности вещи (аргументы CL, environment), либо криво спроектированные библиотеки.MacIn
13.04.2017 14:17Можно использовать комбинированный подход, как в Delphi: есть и длина строки (и refcount еще для меньшего кол-ва операций копирования) и терминальный 0. Поэтому можно и всегда быстро длину узнать, и передать в C, где нужен ноль на конце. Можно ту же самую замену разделителей на 0 провести, с выделением указателей на каждую подстроку, формальная длина строки при этом останется той же.
iig
13.04.2017 14:32Надо постоянно следить за соответствием длины строки данным внутри. Как быть, например, когда мы при разборе строки пишем в разные места \0? Нужно тогда делать свои функции на все случаи жизни, с поддержкой длины и \0. Или перейти на С++ ;)
MacIn
13.04.2017 18:29-1Не вижу никакой проблемы. Длина строки остается прежней, т.е. равна длине буфера.
А уж если мы начинаем играть в упомянутые выше ASCIIZ игры, то и пользоваться будем, соответственно, указателями и функциями типа strlen.iig
13.04.2017 18:45+1Ну, не знаю…
1. Наверное, нужно для начала определить новый тип данных, раз в С строках нет места под длину. Не проблема
struct { unsigned int len; char * data; }
— где-то так.
2. Как это инициализировать константной строкой?
— уже не катит (пора писать макрос для инициализации этих полей)char * x; x = "the string";
3. Как прочитать туда строку из файла? fread (и прочие функции, выводящие данные в буфер) с таким не работает — натягиваем другой макрос. Или потихоньку переписываем все функции для строк.
А так особых проблем нет ;)ZyXI
13.04.2017 19:32fread()
вообще строки не читает, он читает бинарные данные. Там и в аргументахvoid *
. Так что измени?те добавление NUL байта на сохранение длины, вот и вся разница. Про другие функции вы правы, и часть придётся именно переписать (практически все, если вам нужны NUL байты внутри строки). Макрос для литерала этой новой строки довольно прост: к примеру, Neovim использует такой.
Кстати, под длину есть стандартный тип:
size_t
.
Ещё напомню, что строки с длиной хотят много кто. В большинстве современных языков с GC (типа Python), чьи внутренности я приблизительно знаю они именно такие: строка + длина. Вроде где?то есть и C’шная библиотека под этот вариант, но я её не трогал.
ZyXI
13.04.2017 19:36А, и структура ещё может выглядеть как
typedef struct { size_t len; char data[]; } String;
(память выделяется так:xmalloc(offsetof(String, data) + len)
). В зависимости от ситуации, так может быть оптимальнее или наоборот.
MacIn
13.04.2017 19:38Вы решаете вопрос «как средствами языка Си достичь xyz», в то время, как я просто отметил, что комбинированный подход — удобен, потому что наметился спор (см. выше) на тему, какие строки лучше — asciiz или паскалевского типа.
iig
13.04.2017 22:50Статья про строки в Си, так что почему бы и не поспорить ;)
Достоинство строк с нулем — они уже есть ;) то есть изобретать ничего не нужно. Строки с длиной могут иногда обрабатываться быстрее, хотя бы потому, что длину считать нужно не более 1 раза. Но в Си нужно стоить велосипед, или искать готовый, или переходить на С++ ;).
kloppspb
14.04.2017 00:26Или потихоньку переписываем все функции для строк.
/* ... */
Но в Си нужно стоить велосипед, или искать готовый, или переходить на С++ ;).
:-) IMHO, эти велосипеды строили все, особенно если в истории болезни пациент ловил C до C++ (осложнения в виде BASIC, или досовско-ассемблерных выкрутасов в рассчёт не берём).
Но, в конце концов, мечтам натянуть сову на глобус (то бишь сделать в одном языке как в другом) — меньше, чем время жизни C, а все эти откровения про строки смотрятся уже анахронизмом, ибо пережёваны мильярды раз ещё с прошлого века.
kemm
14.04.2017 13:27-2Вот именно поэтому ASCIIZ строки быстрые, а pascal-like — нет.
const char str[] = "1,2,3,4,5"; // внезапно строка const char *p = str + 4; // внезапно тоже строка в ASCIIZ char *endp = NULL; long val = strtol(p, &endp, 10); // и endp тоже не менее внезапно строка // check errors ... if (endp - str >= sizeof(str) - 1) // strlen()? А шо ента и, главное, зачем? fprintf(stderr, "unexpected EOS\n");
Этот момент очень важен, когда нужно быстро разбирать какую-нибудь простыню околотекстовых данных (например, pdf). Да, возникает такая необходимость редко, да, это неудобно и требует крайней аккуратности (собственно, а чего вы хотели-то от условно-переносимого ассемблера?), но я не могу придумать ни одного другого варианта, который предоставлял бы такую же гибкость, скорость и относительное удобство в тех случаях, когда нам эта гибкость и скорость не нужна (я про семейство str*() функций).iig
14.04.2017 16:26+2То, что на С можно быстро и эффективно стрелять себе по ногам — это известно давно ;)
А если вспомнить про паскалевские строки и работу с ними в том самом Паскале — там начудить с указателями невозможно.
MacIn
14.04.2017 20:09+2Вы, собственно, прочитали комментарий, на который отвечали? Судя по всему, нет.
В смешанном подходе вы сможете все сделать точно так же, за исключением инициализации константой (но это именно языкозависимая фишка, конкретно относящаяся к упомянутому мною Delphi, из которого я притащил пример смешанного подхода).
const char *p = str + 4; // внезапно тоже строка в ASCIIZ
Будет
var s: string; p: pchar; s := '1,2,3,4,5'; p := pchar(str) + 4;
Здесь мы можем использовать p как ASCIIZ строку.
char *endp = NULL;
long val = strtol(p, &endp, 10); // и endp тоже не менее внезапно строка
То же самое, если мпортируем strtol откуда-нибудь
var val: integer; endp: pchar; val := strtol(p, @endp, 10);
if (endp — str >= sizeof(str) — 1) // strlen()? А шо ента и, главное, зачем?
А здесь вместо sizeof я буду использовать обычный стандартный length:
if cardinal(endp) - cardinal(str) >= length(str) - 1 then
И этот length имеет сложность O(1).MacIn
14.04.2017 20:14А все потому, что встроенная в язык строка выглядит как:
refcount (4 bytes) | length(4 bytes) | string data itself | 0
и поле типа string — это указатель на «string itself», который напрямую cast'ится в ASCIIZ строку. И нам достпуна вся та же магия с указателями, мы можем так же понавставлять нулей и работать с созданными подстроками функциями сишной библиотеки, если очень хочется.
kemm
14.04.2017 20:26-2> Здесь мы можем использовать p как ASCIIZ строку.
Только у вас нет ASCIIZ строк, они плохие, медленные и вообще, их выкинули. Упс.
> То же самое, если мпортируем strtol откуда-нибудь
strtol тоже нет, он с гадостью работал. val := StrToInt(p) — упс, а чего это у нас компилятор ругается? Копируем в string, жуём кактус, громко радуемся, что у нас быстрые строки, а не ужасный медленный ASCIIZ. 8))
> И этот length имеет сложность O(1).
Как и арифметика. Удивительно, не правда ли?MacIn
15.04.2017 15:17Только у вас нет ASCIIZ строк, они плохие, медленные и вообще, их выкинули. Упс.
Неправда, и ровно об этом я и написал. Но кое у кого дислексия, судя по всему.
strtol тоже нет
Простите, у вас русский язык не родной? Я русским по белому написал: "если импортируем strtol откуда-нибудь"
Как и арифметика. Удивительно, не правда ли?
Ничего удивительного. Комбинированный подход, о котором я написал, берет лучшее из обоих миров — в нем можно работать и так и так.
Siemargl
13.04.2017 12:08+2Внезапно, сложение десяти строк через strcat даст экспоненциальную сложность
Внезапно, нет.
Подсказать, где посмотреть исходники strcat или понятие экспоненты?
lpre
12.04.2017 16:31+4(strlen(str) + 1) — такое себе решение. Перед нами встают 2 проблемы:
А если нам надо выделить память под формируемую с помощью, например, s(n)printf(..) строку?А если нам это не надо? Зачем решать несуществующую "проблему"? Вот когда нам в каком-либо конкретном случае это действительно потребуется, тогда и сделаем по-другому.
Внешний вид.
Нормальный внешний вид. А если не нравится — сделайте макрос (особенно для программы "где регулярно требуется обрабатывать строки").
работа со строками в C — это очень сложная тема
Ничего сложного. Это вы всё слишком усложняете. Вместо того, чтобы сразу написать "под катом" (или даже перед ним) вот это:
Весь «прикол» в том, что функция strlen не учитывает символ конца строки
вы зачем-то затеяли целое "исследование" с дампами из valgrind, а в конце привели решение, избыточное для простого копирования.
И чем больше тонкостей языка вы знаете, тем лучше ваш код.
Одна из "тонкостей" заключается в том, что вот это:
char *str = malloc(sizeof(char) * (strlen(BUF) + 1)); strcpy(str, BUF);
является нормальным и безопасным рабочим решенем, если вам нужно скопировать одну строку в другую (динамически выделенную).
для написания «безопасного» кода при динамическом выделении памяти рекомендуется все же использовать функцию calloc() вместо malloc() — calloc забивает выделяемую память нулями. Ну или после выделения памяти использовать функцию memset(). Иначе мусор, который изначально лежал на выделяемом участке памяти, может вызвать вопросы при дебаге, а иногда и при работе со строкой.
Зачем такие "сложности"? Вы же используете strcpy(), а она копирует всю строку, включая терминальный 0.
avbochagov
12.04.2017 16:33+5А мне одному глаза режет от sizeof(char)?
Indever2
12.04.2017 16:34-9sizeof(char) при выделение памяти необходим, так как на некоторых системах размер char равен 1 байту, на других же — 2м.
gbg
12.04.2017 16:39+8Идите и учите Стандарт C99, раздел 6.5.3.4:
When applied to an operand that has type char, unsigned char, or signed char, (or a qualified version thereof) the result is 1.
Indever2
12.04.2017 16:43-4В общем случае размер типа char на конкретной платформе регулируется значением константы CHAR_BITS, определённой в заголовочном файле limits.h. По умолчанию и на платформах x86 она равна 8
— википедия. Хотя там так же есть и про стандарт.gbg
12.04.2017 16:46+8Из этого не следует, что в стандартном компиляторе C, sizeof(char) вернет не 1. Даже если машина работает с char, который в памяти занимает 32 бита, sizeof(char) будет равен 1. Это прям так в стандарте английским по белому написано.
avbochagov
12.04.2017 16:39+3Вообще-то по стандарту С sizeof(char) = 1. В стандарте С++ — ровно точно также.
Indever2
12.04.2017 16:43-1Тут ответил.
avbochagov
12.04.2017 17:00+2Я бы ещё тогда проверил разницу между sizeof('\0') на С компиляторе, и на С++ компиляторе.
Вот тут хорошо описано.
tandzan
12.04.2017 19:49+2В живом проекте там будет скорее всего sizeof(wchar_t), размер которого может быть и 2 байта, и 4.
grossws
12.04.2017 21:45Использование NTWS (null-terminated wide strings) вообще боль и страдание, не говоря уже о том, что они часто неудобны (например, при использовании UTF-8). Часто предпочтительнее использовать NTMBS (null-terminated multibyte strings), которые представляются
char *
.TrueBers
13.04.2017 13:11А никто и не просит хранить в них UTF-8. В wide-строках хранится, обычно, UTF-32, если компилятор соответствует стандарту.
TrueBers
13.04.2017 13:10На самом деле, по стандарту, размер wchar_t должен быть таким, чтобы в него помещались все кодпоинты юникода. Сейчас их 0x10FFFF, в 2 байта их помещает только винда с помощью суррогатных пар, это означает только то, что винда не следует стандарту.
grossws
13.04.2017 13:30Ткните на пункт в стандарте C99, где говорится, что wchar_t должен вмещать все codepoint'ы unicode'а, я видел только про то, что он должен быть способен вместить любые символы в текущей локали, про unicode вообще не говорится.
Tujh
20.04.2017 13:46«Винда» с зари своих юникодных времён использует UCS-2 и (wchar_t == 2), так что тут всё следует стандарту, вот только ни кто не обещал для wchar_t использовать UTF32
TrueBers
20.04.2017 16:48+1«Винда» с зари своих юникодных времён использует UCS-2
Возможно, на «заре» так оно и было. Но эта заря закончилась в 96 году, когда вышел стандарт Unicode 2.0, в котором понятие UCS-2 вообще объявили obsolete, т. к. были добавлены UTF-16 и суррогатные пары.
так что тут всё следует стандарту
Только если, стандарту Unicode 1.1 1993 года.
Согласно MSDN, поддержку UTF-16 добавили начиная с Windows 2000, и вплоть до настоящего времени, широкие символы у винды реализованы через UTF-16, а не UCS-2.
На дворе 2017 год, на носу Unicode 10.0, все, уважающие себя (и других), средства работы с кодировками поддерживают суррогатные пары, забудьте о понятии UCS-2.
Другое дело — то, как винды поддерживают этот UTF-16. А глюков с этим там — более, чем хватает. Но это проблема уже тех, кто объявил, что «мы поддерживаем», а на самом деле забили на это.
вот только ни кто не обещал для wchar_t использовать UTF32
Да, в стандарте указано, что «implementation defined». Что тупо ломает бинарную совместимость в зависимости от локали. При этом, ещё с версии C99, существует макрос, который гарантирует некое множество символов определённой минимальной версии Юникода, которое должно быть представлено единым широким символом.
MSVC не парится по поводу этого макроса вообще, в отличие от той же glibc, которая в текущей стабильной версии обещает реализацию юникода 8.0. А меньше, чем в UTF-32 целиком множество кодпоинтов 8-й версии юникода, не входит.
Я к тому, что лучше бы следовать, какому-никакому, более-менее допускающему вольности, пункту стандарта, чем костылить свои хотелки, как в голову взбредёт.Tujh
21.04.2017 10:29+2Вот именно поэтому я и говорю, что, не смотря на все заявления, MSWin поддерживает UCS-2, а не UTF-16. Так как реализация полноценного юникода хромает на обе ноги.
Только если, стандарту Unicode 1.1 1993 года.
Это же Микрософт, поддержка legacy у них на уровне :)
При этом, ещё с версии C99, существует макрос...MSVC не парится по поводу этого макроса вообще
А зачем им париться если у них полная поддержка только С90 и частично С94, о С99 и С11 они даже не задумываются? :)
Если хочется использовать юникод в своих проектах — придётся отказаться от нативного WinAPI. Но нужно так же помнить, что в NT системах (а сейчас они все такие) в основе лежит вызов именно «юникодовых» функций, те которые заканчиваются на W и принимают в качестве параметров wchar_t*. При вызове функций с char* и <имя_функции>A будет производится конвертация символов. Или шашечки, или ехать… в этом весь микрософт.
Indever2
12.04.2017 20:03-2Во, чего откопал:
Оказывается, при выделении памяти под указатель на char, sizeof() является одним из способов дать понять, зачем нам нужна эта переменная.
То есть.
Тот же char * иногда используют, например, когда упаковывают пакеты перед отправкой на RAW-сокет, и тогда выделение памяти будет выглядеть так:
char *packet = malloc(sizeof(ether_header) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(payload));
Тому, кто будет сопровождать проект будет ясно, что это переменная под пакет. (заголовки и данные)
А если мы напишем sizeof(char) * n, это указание, что эта переменная будет использована как строка.
Я не знаю, зачем так делать (буду благодарен тому, кто объяснит), но тем не менее — в определенных кругах считается аж правилом хорошего тона :)avbochagov
12.04.2017 20:53+2О каких кругах идет речь? Можно хоть ссылочку, чтобы почитать объяснение такому замусориванию кода?
Когда-то прочитал, что код надо писать так, как будто его будет читать неуравновешенный психопат, который знает где ты живешь.
Когда я вижу конструкцию sizeof(char)*n — то сразу вижу, что человек не понимает смысла этой конструкции, а потому его код надо проверять с двойным вниманием и усердием. А это расход моего времени… и вот я уже незаметно превращаюсь в того психопата...
015z
13.04.2017 02:44В качестве оправдания я лично использую: «Самодокументированный код». packet — слишком общо, если пример вырван из контекста. А когда я ковыряюсь в своих старых программах, пример как правило именно вырван из общего контеста. Коллега, правивший мой код не задал ни одного вопроса. Это и его заслуга, и моя.
arcman
13.04.2017 12:17+1В таком случае лучше использовать uint8_t * и сразу будет понятно, что речь не о строках идет.
Zolushok
12.04.2017 16:38+3поправьте меня, если я что-то путаю, но предварительное вычисление длины результата с помощью "snprintf(NULL, ..." — в итоге выполняет одну и ту же работу по форматированию два раза?
shukan
12.04.2017 17:55+4Проблема препода, что он не знает про
char* strdup (const char *src);
Функция появилась в BSD, включена в POSIX, но не является частью стандартов ANSI/ISO, хотя поддерживается почти всеми компиляторами.
Andrey2008
12.04.2017 23:14+2Кстати, чтобы найти ошибку в первом фрагменте кода, вовсе не обязательно запускать программу под присмотром valgrind. Подобные простые случаи сходу выявляет и статический анализатор кода. Например, PVS-Studio выдаёт 2 предупреждения:
- V512: A call of the 'strcpy' function will lead to overflow of the buffer 'str'.
- V575: The potential null pointer is passed into 'strcpy' function. Inspect the first argument.
Первое по делу. Второе про то, что указатель, выделенный через malloc() хорошо-бы проверить перед использованием.saterenko
13.04.2017 11:49+2Valgrind бесплатный, PVS-Studio платный…
gbg
13.04.2017 11:58+2Это совершенно разные инструменты, решающие разные задачи. Не хотите платить, есть CLANG Static Analyzer
Можно выделить три стадии развития кода и существования ошибки
1) Этап написания
2) Этап компиляции
3) Этап выполнения
VALGRIND, как динамический анализатор, работает на третьем, статический анализатор работает на первом.
На более раннем этапе поиск ошибки более надежен. На этапе написания у вас есть код программы с одной стороны и спецификации на код, библиотеки и компилятор с другой стороны.
На этапе выполнения у вас добавляется внешняя среда, которая может существенно влиять на поиск ошибки.
Чем больше ошибок удается отловить на первых двух этапах, тем лучше.
JIghtuse
14.04.2017 10:18+2Как и Cppcheck:
(error) Buffer is accessed out of bounds.
, как и AddressSanitizer компилятора:
Скрытый текст=================================================================
==21454==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000effd at pc 0x7f7182fd729b bp 0x7ffe039c1940 sp 0x7ffe039c10e8
WRITE of size 14 at 0x60200000effd thread T0
#0 0x7f7182fd729a (/lib64/libasan.so.3+0x5f29a)
#1 0x4008d1 in main /tmp/c.c:9
#2 0x7f7182bd2400 in __libc_start_main (/lib64/libc.so.6+0x20400)
#3 0x4007d9 in _start (/tmp/a.out+0x4007d9)
0x60200000effd is located 0 bytes to the right of 13-byte region [0x60200000eff0,0x60200000effd)
allocated by thread T0 here:
#0 0x7f718303ee60 in malloc (/lib64/libasan.so.3+0xc6e60)
#1 0x4008b7 in main /tmp/c.c:8
#2 0x7f7182bd2400 in __libc_start_main (/lib64/libc.so.6+0x20400)
SUMMARY: AddressSanitizer: heap-buffer-overflow (/lib64/libasan.so.3+0x5f29a)
Shadow bytes around the buggy address:
0x0c047fff9da0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9db0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9dc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9dd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9de0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c047fff9df0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa 00[05]
0x0c047fff9e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9e20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9e30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9e40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==21454==ABORTING
LynXzp
13.04.2017 01:13Да очень просто. Весь «прикол» в том, что функция strlen не учитывает символ конца строки — '\0'. Даже если его явно указать во входящей строке (#define HELLO_STRING «Hello, Habr!\n\0»), он будет проигнорирован.
«Hello, Habr!\n\0» будет на один байт длиннее «Hello, Habr!\n», и естественно последний байт проигнорируется точно так же (хотя один завершающий ноль уж будет точно). Строго говоря строковый литерал содержит в себе все указанные символы + завершающий нулевой. Причем вне зависимости указаны ли нулевые символы внутри и тем более от их позиций. Нулевые символы внутри строкового литерала хоть и необычны, но вполне легитимные и иногда полезные. Например, я так экономил память: «Hello\0Привет», сдвигая строку перед печатью в зависимости от выбранного языка.
ZyXI
13.04.2017 01:41+4sizeof('\0')
совершенно точно делает не то, что вы просили. Согласно C99sizeof('\0') == sizeof(int)
, если ваш компилятор выдаёт что?либо иное, то в компиляторе ошибка. 6.4.4.4, параграф 10: «An integer character constant has type int.»
ZyXI
13.04.2017 01:53+2Ещё относительно
snprintf(NULL, …)
: оно не всегда работает: http://demin.ws/blog/russian/2013/01/28/use-snprintf-on-different-platforms/. Статья довольно старая, но она говорит в том числе о Windows 7, которая ещё актуальна. Я не знаю, починили ли что?то в более новых версиях или вам нужно делать#define snprintf _snprintf_s
.monah_tuk
13.04.2017 06:58+2Хотел написать про это, но вы уже сделали :)
Поэтому только про макросы с переменным числом параметров.
В той форме, что они применены в статье:
#define macro(args...) foo(args)
это расширение препроцессора Gcc: https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html
Легальная форма:
#define macro(...) foo(__VA_ARGS__)
Макросы с переменным числом параметров доступны с C99.
mbait
13.04.2017 02:16-1Не ведитесь на поролон — предыдущая статьи автора, опубликованная буквально недавно, написана в том же духе. Это какая-то дикая смесь троллинга и эффекта Даннинга — Крюгера.
saterenko
13.04.2017 11:43Вызывать snprintf для расчёта длины строки — это какое-то особенное извращение, функция совсем не дешёвая… Если вам надо писать скроку неизвестной длины, есть vsnprintf. Вот кусок кода (вырезал всё лишнее):
cor_log_put(const char *format, ...) { /* пропущена часть кода... */ char buf[COR_LOG_STR_SIZE]; char *p = buf; char *begin = p; char *end = p + COR_LOG_STR_SIZE; va_list args; while (1) { /* пишем сколько влезет */ va_start(args, format); int rc = vsnprintf(p, end - p, format, args); va_end(args); if (rc < 0) { if (begin != buf) { free(begin); } return; } /* проверяем, не мало ли места */ if (rc >= end - p) { size_t size = COR_LOG_STR_SIZE; while (size <= rc) { size = (size << 1) - (size >> 1); } char *nb = (char *) malloc(size); if (!nb) { if (begin != buf) { free(begin); } return; } memcpy(nb, begin, p - begin); p = nb + (p - begin); end = nb + size; if (begin != buf) { free(begin); } begin = nb; continue; } p += rc; break; } /* пропущена часть кода... */ }
Идея в том, что на стеке выделяется строка размером COR_LOG_STR_SIZE байт, которой обычно хватает для записи строк. А если надо будет записать больше, выделяем нужное количество памяти на куче.
alexoron
13.04.2017 12:10-5Тот момент когда студенты намного умней всяких там преподавателей и профессоров!
У нас в универе был препод который постоянно всем твердил — «Да я вам говорю что вы тупой».
Всем студентам.
OlegKozlov
14.04.2017 09:36-3Тысяча и одна вариация функции xxxprintf отпугивает от C++ злых духов гораздо круче святой воды и чеснока.
safinaskar
14.04.2017 14:19+1#define strsize(args...) snprintf(NULL, 0, args) + sizeof('\0')
Нужно скобки писать, вот так:
#define strsize(args...) (snprintf(NULL, 0, args) + sizeof('\0'))
Иначе всякое тамstrsize("abc") * 2
неправильно будет работать. Решили поучить других о C, а сами оплошали
gbg
После совета лепить везде calloc вместо malloc некоторый критический к производительности код станет чуточку ХУЖЕ. Предварительное забивание памяти нулем из соображений «как бы чего не вышло» гораздо хуже программирования без ошибок.
А такие низкоуровневые системные места нужно формально доказывать, а не проверять при помощи VALGRIND.
terrier
Покажите, какой-нибудь интересный низкоуровневый код, правильность которого вы формально доказали
gbg
Если вы хотите знать метод доказательства, или подход к доказательству, то он описан Дейкстрой еще в 70е годы.
1. Берем все системные функции за аксиомы, то есть считаем, что они работают в соответствии со своей спецификацией.
2. Предполагаем, что компилятор работает строго в соответствии со стандартом языка.
3. Исходя из этого, формально доказываем, что для всех входных данных программа работает в соответствии со своей спецификацией.
Если удалось доказать — ура, новых багов мы не внесли.
terrier
Нет, хочу посмотреть на интересный низкоуровневый код, правильность которого вы формально доказали
gbg
Это такой странный формат аргумента «сперва добейся»? Извините, я в таких переходах на личности участия не принимаю. Спасибо.
terrier
Спасибо и вам — это вот яркая характеристика любителей разного рода «формальных доказательств» и эффектных фраз типа «да выкиньте вы свои валгринды, а лучше смотрите, какую я клевую книжку прочитал». Прямо хоть на стенку вешай.
lain8dono
Есть мнение, что микроядро seL4 вертифицировали. Мнение где-то тут http://sel4.systems/ код где-то там https://github.com/seL4
terrier
Очень интересная штука!
Понятно, что по существу верификации сложно что-то сказать, но насколько я понимаю ради верифицируемости запрещено DMA, драйверы выселены в user-mode ну и функциональность, понятное дело минимальна ( однако есть треды! ). При этом предполагать корректность работы компилятора не надо, корректность трансляции в машинные коды тоже доказывается.
Ну и, конечно же, утверждать что в такой системе «нет багов» или она «невзламываемая» можно только в рекламных целях.
gbg
Почему вы не доверяете формальному доказательству?
terrier
Нет, я как раз к тому, что проверить формальное доказательство у меня и близко нет возможности и умения и поэтому можно только поверить им, что оно верно.
mihaild
Проверить формальное доказательство относительно несложно (было бы, если бы они дали на него ссылку). Гораздо сложнее проверять неформализуемую часть — а именно формализацию (что они действительно доказывают «отсутствие утечек», например).
gbg
Итого вы утверждаете, что «сложно найти изъян в чьем-то определении условия отсутствия утечек»?
lain8dono
Нет. Это фишка микроядерной архитектуры самой по себе. Повышенная безопасность в ущерб перформансу.
Почему же? На уровне software вполне себе можно. Но кроме собственно ядра есть ещё hardware и userspace. И от физического доступа это очевидно не защищает. Только отсеивает некоторый класс атак. В целом даже такой уровень безопасности часто излишен.
terrier
Но позвольте:
The formal verification of seL4 on the ARM platform assumes that the MMU has complete control over memory, which means the proof assumes that DMA is off.
https://wiki.sel4.systems/FrequentlyAskedQuestions#What_about_DMA.3F
Здесь именно сказано, что доказательство предполагает, что DMA выключен.
gimntut
Другими словами, сами вы этого ни когда не делали. Или я не правильно понял, и вы сможете (за определённую плату) выполнять формальное доказательство, для каждого изменения кода программы. Если можете, то хотелось бы подробностей, как это делается на практике.
gbg
На практике это делается так — новый алгоритм публикуется в рецензируемых научных журналах, а его авторы выступают на научных конференциях. В частности, в сфере вычислительной гидродинамики, действуют именно так.
Indever2
Алгоритм — последовательность действий. Динамическая память — инструмент реализации.
Я не говорю, что вы не правы, просто я немного не могу понять суть.
rg_software
С алгоритмами тут проблем нет, проблемы в невнимательности при программировании или в незнании тонкостей языка, так что доказывать тут нечего, не та ситуация.
bogolt
Простите, а как публикация алгоритма и выступления автора докажут, что алгоритм правильно работает ( для всех случаев и без багов )?
gbg
Извините, но математика именно так примерно развивается с 16 века. Тогда было популярно искать, например всякие корни многочленов, и устраивались состязания между математиками. Тогда начал формироваться подход к строгому математическому рассуждению (в математике перестали верить на слово джентельменам).
Так вот, метод формального доказательства корректности программного текста, он ничем не отличается от методов, которые применяются в математике. Этот метод нельзя автоматизировать для любой программы (теорема об остановке не даст), однако под любую готовую программу можно построить способ доказательства того, что она соответствует спецификации.
Это доказательство будет опираться на следующие посылки:
1) Все библиотеки работают по спецификации.
2) Компилятор работает в соответствии со стандартом языка
3) Вероятность аппаратного сбоя пренебрежимо мала.
Теоретически, никаких фундаментальных запретов на это нет.
Однако, на практике это требует определенных дополнительных усилий со стороны разработчиков программы, и их более высокой математической подготовки. Под математической подготовкой здесь понимается не знание хитрых формул, а именно формат мышления.
К счастью, сейчас имеются средства, позволяющие выполнять верификацию полуавтоматически — это статические анализаторы кода, о котором здесь упомянул Andrey2008.
RomanArzumanyan
Корректность алгоритма не тождественна корректности реализации.
Если алгоритм представим в виде одного ДКА (на бумажке), то его реализация на компьютере будет сменой состояний уже второго ДКА (в кремнии). Как правило, алгоритм смены состояний большого ДКА (программа) содержит баги, поскольку этот ДКА большой и сложный (и тоже с багами!). Для того и тестируют.
Indever2
Речь шла о безопасном коде, а не производительном в данном случае.
gbg
Как забивка нулями повышает безопасность? Мусор вылезет сразу, а нули будут маскировать реальную ошибку.
Indever2
Отнюдь.
Рассмотрим ситуацию, когда кусок памяти, выделяемой под строку (допустим, тот же «Hello, Habr!\n»), выглядит вот так:
0x602010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Тогда после записи туда строки «Hello, Habr!\n» (без символа конца строки), этот кусок будет выглядеть так:
0x602010: 0x48 0x65 0x6c 0x6c 0x6f 0x2c 0x20 0x48
0x602018: 0x61 0x62 0x72 0x21 0x0a 0x00 0x00 0x00
Проблем нет, если мы попробуем распечатать эту строку функцией printf(), которая выводит все символы до тех пор, пока не встретить нулевой символ (конец строки), то она дойдет до первого куска памяти, в котором есть 0x00 (14 символ), и интерпретирует его как конец строки.
Теперь история с мусором:
0x602010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602018: 0x00 0x00 0x00 0x00 0x65 0x65 0x65 0x00
Собственно, после записи:
0x602010: 0x48 0x65 0x6c 0x6c 0x6f 0x2c 0x20 0x48
0x602018: 0x61 0x62 0x72 0x21 0x0a 0x65 0x65 0x00
Вывод программы:
-> Hello, Habr!
ee
Примерно так :)
gbg
Запись в буфер строки без символа конца строки — это явная логическая ошибка в программе.
Indever2
Хорошо, давайте посмотрим с другой стороны.
Если нам придется дебажить программу в рантайме, как мы отличим инициализированные переменные от неинициализированных? Ну, отловили мы контекст, начинаем смотреть память.
А мусор по адресу бывает нередко «похож на правду». Очень часто это может запутать.
По поводу вашего предыдущего комментария — я, очевидно, не так прочел. Я соглашусь, что мусор вылезет сразу, а нули могут замаскировать ошибку. Тем не менее, зануление памяти в Си — очень полезная штука. Это не C# и не Java, здесь занулением никто, кроме программиста заниматься не будет.
Проверка на пустоту массива, например, когда в нем лежит мусор будет некорректной и посчитает только что аллоцированный массив заполненным :)
gbg
Мы можем поставить точку останова на операцию записи по определенным адресам в памяти. И если точка останова не сработала — значит, программа туда не писала.
Zolushok
Так точно. А заметание под ковер ошибок с неверно посчитанной длиной и недописанными терминаторами с помощью предварительной очистки всё равно рано или поздно вылезет в ещё более труднодиагностируемых местах.
MacIn
Здесь есть другой аспект, с которым иногда приходится встречаться. Если по некоему адресу хранится указатель, или блок указателей, его лучше инициализировать, обнулять, потому что проверка валидности указателя, которая ставится в ifах и assertах, может выстрелить только при нулевом указателе, дав нам стек для отладки, а в ином случае там может оказаться мусор, который при этом является валидным адресом памяти, и ошибка уплывет дальше, где ее фиг поймаешь.
0xd34df00d
nullptr, кстати, не обязан представляться комбинацией из лишь нулевых байт, и были архитектуры, где это не так.
MacIn
Разумеется. Память при выделении по идее должна обnilиваться, а не обнуляться, но я не проверял.
iMADik
В системном программировании бывает невозможно использовать точки останова и другие прелести отладки, для примера написание встраиваемой ОС для какого нибудь специфичного МК, с ограниченным набором инструментов для разработки. В таких случаях для дебага только какая то запись в гарантированно неиспользуемую область памяти и её анализ после принудительной остановки.
maaGames
Для безопасности не сильно важно, чем заполнена память при выделении. Для безопасности важно обнулить (или заполнить мусором) память перед освобождением.
arcman
Автор трактует «безопасность» в контексте fail safe. Т.е. речь идёт не о затирании приватных данных после окончания работы сними, а о сокрытии ошибок в ПО.