Comments 122
А такие низкоуровневые системные места нужно формально доказывать, а не проверять при помощи VALGRIND.
А такие низкоуровневые системные места нужно формально доказывать, а не проверять при помощи VALGRIND.
Покажите, какой-нибудь интересный низкоуровневый код, правильность которого вы формально доказали
1. Берем все системные функции за аксиомы, то есть считаем, что они работают в соответствии со своей спецификацией.
2. Предполагаем, что компилятор работает строго в соответствии со стандартом языка.
3. Исходя из этого, формально доказываем, что для всех входных данных программа работает в соответствии со своей спецификацией.
Если удалось доказать — ура, новых багов мы не внесли.
Если вы хотите знать метод доказательства, или подход к доказательству
Нет, хочу посмотреть на интересный низкоуровневый код, правильность которого вы формально доказали
Есть мнение, что микроядро seL4 вертифицировали. Мнение где-то тут http://sel4.systems/ код где-то там https://github.com/seL4
Понятно, что по существу верификации сложно что-то сказать, но насколько я понимаю ради верифицируемости запрещено DMA, драйверы выселены в user-mode ну и функциональность, понятное дело минимальна ( однако есть треды! ). При этом предполагать корректность работы компилятора не надо, корректность трансляции в машинные коды тоже доказывается.
Ну и, конечно же, утверждать что в такой системе «нет багов» или она «невзламываемая» можно только в рекламных целях.
насколько я понимаю ради верифицируемости запрещено DMA, драйверы выселены в user-mode ну и функциональность, понятное дело минимальна
Нет. Это фишка микроядерной архитектуры самой по себе. Повышенная безопасность в ущерб перформансу.
Ну и, конечно же, утверждать что в такой системе «нет багов» или она «невзламываемая» можно только в рекламных целях.
Почему же? На уровне software вполне себе можно. Но кроме собственно ядра есть ещё hardware и userspace. И от физического доступа это очевидно не защищает. Только отсеивает некоторый класс атак. В целом даже такой уровень безопасности часто излишен.
Нет. Это фишка микроядерной архитектуры самой по себе.
Но позвольте:
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 выключен.
Я не говорю, что вы не правы, просто я немного не могу понять суть.
С алгоритмами тут проблем нет, проблемы в невнимательности при программировании или в незнании тонкостей языка, так что доказывать тут нечего, не та ситуация.
Так вот, метод формального доказательства корректности программного текста, он ничем не отличается от методов, которые применяются в математике. Этот метод нельзя автоматизировать для любой программы (теорема об остановке не даст), однако под любую готовую программу можно построить способ доказательства того, что она соответствует спецификации.
Это доказательство будет опираться на следующие посылки:
1) Все библиотеки работают по спецификации.
2) Компилятор работает в соответствии со стандартом языка
3) Вероятность аппаратного сбоя пренебрежимо мала.
Теоретически, никаких фундаментальных запретов на это нет.
Однако, на практике это требует определенных дополнительных усилий со стороны разработчиков программы, и их более высокой математической подготовки. Под математической подготовкой здесь понимается не знание хитрых формул, а именно формат мышления.
К счастью, сейчас имеются средства, позволяющие выполнять верификацию полуавтоматически — это статические анализаторы кода, о котором здесь упомянул Andrey2008.
Если алгоритм представим в виде одного ДКА (на бумажке), то его реализация на компьютере будет сменой состояний уже второго ДКА (в кремнии). Как правило, алгоритм смены состояний большого ДКА (программа) содержит баги, поскольку этот ДКА большой и сложный (и тоже с багами!). Для того и тестируют.
Например, для написания «безопасного» кода при динамическом выделении памяти рекомендуется все же использовать функцию calloc() вместо malloc().
Речь шла о безопасном коде, а не производительном в данном случае.
Рассмотрим ситуацию, когда кусок памяти, выделяемой под строку (допустим, тот же «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
Примерно так :)
Если нам придется дебажить программу в рантайме, как мы отличим инициализированные переменные от неинициализированных? Ну, отловили мы контекст, начинаем смотреть память.
А мусор по адресу бывает нередко «похож на правду». Очень часто это может запутать.
По поводу вашего предыдущего комментария — я, очевидно, не так прочел. Я соглашусь, что мусор вылезет сразу, а нули могут замаскировать ошибку. Тем не менее, зануление памяти в Си — очень полезная штука. Это не C# и не Java, здесь занулением никто, кроме программиста заниматься не будет.
Проверка на пустоту массива, например, когда в нем лежит мусор будет некорректной и посчитает только что аллоцированный массив заполненным :)
Так точно. А заметание под ковер ошибок с неверно посчитанной длиной и недописанными терминаторами с помощью предварительной очистки всё равно рано или поздно вылезет в ещё более труднодиагностируемых местах.
Я в таком случае при разработке/отладке зануляю всё (можно c командами условной компиляции, определив, что DEBUG = 1 ), а после отладки и перед выпуском компилирую c DEBUG = 0 или лишние операции по занулению руками вырезаю, если код не огромный или не были применены команды условной компиляции.
И отлаживать удобно, и производительность в конечном счёте НЕ страдает.
И в чём проблема? Строка не обязательно должна заканчиваться нулём, если её длина известна и неизменна. Например, если мы меняем одни символы на другие, а потом сливаем результат в файл. Фиговатенько у автора с С.
Хотите работать без него — тогда придется озаботиться использованием везде функций, которые ограничивают размер перемещаемой памяти (strncpy, snprintf, strncat), а в случае, когда у нас строки формируются из аргументов их использование (по назначению) не всегда возможно (только если не использовать подобие макроса strnsize, описанного в статье).
Это очень усложнит как процесс написания, так и процесс восприятия кода + количество функциональных вызовов на строку увеличится. Да и сам я не встречал проектов, где подобная практика используется :)
Внезапно, сложение десяти строк через strcat даст экспоненциальную сложность, в то время как строки со счётчиком количества символов будут сложены с линейной (и та уйдёт на копирование байт).
Ну никто не мешает не использовать в цикле strcat, а посчитать и просуммировать длины, выделить один раз и сделать memcpy.
И никакого переполнения — циклы не будут выходить за допустимые рамки если они будут сразу знать длину строки.
Тоже не гарантирует. Куча багов во всяких парсерах (протоколов, бинарных контейнеров и т. п.) показывает, что зачастую несмотря на наличия явной длины в каком-нибудь поле всё равно умудряются совершать ошибки из этого класса.
Зато ASCIIZ строки безумно быстрые, если уметь читать доки. И, в отличие от «строк с длиной» позволяют избегать лишних выделений памяти или копирования.
1. За счет чего более быстрые? копирование в любом случае происходит более-менее одинаково, а в zero-terminated строках длину надо дополнительно пересчитывать. А в вариантах, когда 1 символ >= 1 байт (UTF-8 если) — это реально долго.
2. Откуда лишние выделения памяти? Сосчитали, сколько памяти нужно — выделили. В обоих случаях одна операция, только в zero-terminated строках считать размер нужно дольше.
А в вариантах, когда 1 символ >= 1 байт (UTF-8 если) — это реально долго.
С какого перепугу? Длина строки — это количество байт, а не количество codepoint'ов или графемных кластеров в ней.
Для других действий со строками — важно. А бывает и критично.
В каких операциях вам важно/критично количество codepoint'ов в строке? В большинстве случаев вы будете итерироваться по codepoint'ам, порождать и записывать codepoint'ы в массив.
Вот, например, в некоторых языках есть буквы, в UPPERCASE и lowercase занимающие разное количество байт.
Не припомню такого, но даже если так, то что это меняет? Один и тот же графемный кластер й
может занимать в utf8 и 2 байта, и 4.
Считайте, что вы не можете этого сделать. Т. к. to_upper_case сложная локалезависимая операция, которая может производить частичную композицию/декомпозицию codepoint'ов.
А как основной подход к решению — выделять чуть больше и использовать функции, принимающие саму строку и максимальный размер (как snprintf). Ну и в случае, если не получилось — делать realloc.
Интересно, как сделано в icu-project, надо будет потом глянуть.
1.1. Чем utf-8 отличается? Длина считается так же (нам ж длина массива байт нужна, а не кол-во символов — во втором варианте всё вообще по-другому)
2. Либо мы выделяем место под {длина строки, строка} — выделение + копрование, либо под {длина строки, указатель на первый байт} — выделение + как реализовать c_str(), который обязан завершаться нулём? Зачастую это всё равно надо, но всё-таки не всегда в случае ASCIIZ. А размер считать не надо. Про подсчёт длины: строки не берутся божественным промыслом, длину мы почти всегда знаем, когда получаем строку откуда-нибудь (будь то возвращаемые значения из функций, данные протоколов или что угодно ещё). Исключений мало — либо по определению некритичные для производительности вещи (аргументы CL, environment), либо криво спроектированные библиотеки.
А уж если мы начинаем играть в упомянутые выше ASCIIZ игры, то и пользоваться будем, соответственно, указателями и функциями типа strlen.
1. Наверное, нужно для начала определить новый тип данных, раз в С строках нет места под длину. Не проблема
struct {
unsigned int len;
char * data;
}
— где-то так.
2. Как это инициализировать константной строкой?
char * x;
x = "the string";
— уже не катит (пора писать макрос для инициализации этих полей)3. Как прочитать туда строку из файла? fread (и прочие функции, выводящие данные в буфер) с таким не работает — натягиваем другой макрос. Или потихоньку переписываем все функции для строк.
А так особых проблем нет ;)
fread()
вообще строки не читает, он читает бинарные данные. Там и в аргументах void *
. Так что измени́те добавление NUL байта на сохранение длины, вот и вся разница. Про другие функции вы правы, и часть придётся именно переписать (практически все, если вам нужны NUL байты внутри строки). Макрос для литерала этой новой строки довольно прост: к примеру, Neovim использует такой.
Кстати, под длину есть стандартный тип: size_t
.
Ещё напомню, что строки с длиной хотят много кто. В большинстве современных языков с GC (типа Python), чьи внутренности я приблизительно знаю они именно такие: строка + длина. Вроде где‐то есть и C’шная библиотека под этот вариант, но я её не трогал.
А, и структура ещё может выглядеть как typedef struct { size_t len; char data[]; } String;
(память выделяется так: xmalloc(offsetof(String, data) + len)
). В зависимости от ситуации, так может быть оптимальнее или наоборот.
Достоинство строк с нулем — они уже есть ;) то есть изобретать ничего не нужно. Строки с длиной могут иногда обрабатываться быстрее, хотя бы потому, что длину считать нужно не более 1 раза. Но в Си нужно стоить велосипед, или искать готовый, или переходить на С++ ;).
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*() функций).
А если вспомнить про паскалевские строки и работу с ними в том самом Паскале — там начудить с указателями невозможно.
В смешанном подходе вы сможете все сделать точно так же, за исключением инициализации константой (но это именно языкозависимая фишка, конкретно относящаяся к упомянутому мною 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).
refcount (4 bytes) | length(4 bytes) | string data itself | 0
и поле типа string — это указатель на «string itself», который напрямую cast'ится в ASCIIZ строку. И нам достпуна вся та же магия с указателями, мы можем так же понавставлять нулей и работать с созданными подстроками функциями сишной библиотеки, если очень хочется.
Только у вас нет ASCIIZ строк, они плохие, медленные и вообще, их выкинули. Упс.
> То же самое, если мпортируем strtol откуда-нибудь
strtol тоже нет, он с гадостью работал. val := StrToInt(p) — упс, а чего это у нас компилятор ругается? Копируем в string, жуём кактус, громко радуемся, что у нас быстрые строки, а не ужасный медленный ASCIIZ. 8))
> И этот length имеет сложность O(1).
Как и арифметика. Удивительно, не правда ли?
Только у вас нет ASCIIZ строк, они плохие, медленные и вообще, их выкинули. Упс.
Неправда, и ровно об этом я и написал. Но кое у кого дислексия, судя по всему.
strtol тоже нет
Простите, у вас русский язык не родной? Я русским по белому написал: "если импортируем strtol откуда-нибудь"
Как и арифметика. Удивительно, не правда ли?
Ничего удивительного. Комбинированный подход, о котором я написал, берет лучшее из обоих миров — в нем можно работать и так и так.
Внезапно, сложение десяти строк через strcat даст экспоненциальную сложностьВнезапно, нет.
Подсказать, где посмотреть исходники strcat или понятие экспоненты?
(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.
А мне одному глаза режет от sizeof(char)?
When applied to an operand that has type char, unsigned char, or signed char, (or a qualified version thereof) the result is 1.
В общем случае размер типа char на конкретной платформе регулируется значением константы CHAR_BITS, определённой в заголовочном файле limits.h. По умолчанию и на платформах x86 она равна 8— википедия. Хотя там так же есть и про стандарт.
Вообще-то по стандарту С sizeof(char) = 1. В стандарте С++ — ровно точно также.
Я бы ещё тогда проверил разницу между sizeof('\0') на С компиляторе, и на С++ компиляторе.
Вот тут хорошо описано.
… системах размер char равен 1 байту, на других же — 2м.
Относительно размера равного 1 Вам уже объяснили, не заостряю внимание, а вот относительно слова байт, почему-то никто не написал. Просто 1, ни о каких байтах, сколько я помню, в стандарте не говорится (но, конечно же, для подавляющего числа платформ будет 1 байт или 8 бит). Если я не прав, поправьте, пожалуйста!
http://en.cppreference.com/w/cpp/language/sizeof
Странно, видимо меня слегка приглючило.
Использование NTWS (null-terminated wide strings) вообще боль и страдание, не говоря уже о том, что они часто неудобны (например, при использовании UTF-8). Часто предпочтительнее использовать NTMBS (null-terminated multibyte strings), которые представляются char *
.
Ткните на пункт в стандарте C99, где говорится, что wchar_t должен вмещать все codepoint'ы unicode'а, я видел только про то, что он должен быть способен вместить любые символы в текущей локали, про unicode вообще не говорится.
«Винда» с зари своих юникодных времён использует 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-й версии юникода, не входит.
Я к тому, что лучше бы следовать, какому-никакому, более-менее допускающему вольности, пункту стандарта, чем костылить свои хотелки, как в голову взбредёт.
Только если, стандарту Unicode 1.1 1993 года.Это же Микрософт, поддержка legacy у них на уровне :)
При этом, ещё с версии C99, существует макрос...MSVC не парится по поводу этого макроса вообщеА зачем им париться если у них полная поддержка только С90 и частично С94, о С99 и С11 они даже не задумываются? :)
Если хочется использовать юникод в своих проектах — придётся отказаться от нативного WinAPI. Но нужно так же помнить, что в NT системах (а сейчас они все такие) в основе лежит вызов именно «юникодовых» функций, те которые заканчиваются на W и принимают в качестве параметров wchar_t*. При вызове функций с char* и <имя_функции>A будет производится конвертация символов. Или шашечки, или ехать… в этом весь микрософт.
Оказывается, при выделении памяти под указатель на char, sizeof() является одним из способов дать понять, зачем нам нужна эта переменная.
То есть.
Тот же char * иногда используют, например, когда упаковывают пакеты перед отправкой на RAW-сокет, и тогда выделение памяти будет выглядеть так:
char *packet = malloc(sizeof(ether_header) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(payload));
Тому, кто будет сопровождать проект будет ясно, что это переменная под пакет. (заголовки и данные)
А если мы напишем sizeof(char) * n, это указание, что эта переменная будет использована как строка.
Я не знаю, зачем так делать (буду благодарен тому, кто объяснит), но тем не менее — в определенных кругах считается аж правилом хорошего тона :)
О каких кругах идет речь? Можно хоть ссылочку, чтобы почитать объяснение такому замусориванию кода?
Когда-то прочитал, что код надо писать так, как будто его будет читать неуравновешенный психопат, который знает где ты живешь.
Когда я вижу конструкцию sizeof(char)*n — то сразу вижу, что человек не понимает смысла этой конструкции, а потому его код надо проверять с двойным вниманием и усердием. А это расход моего времени… и вот я уже незаметно превращаюсь в того психопата...
поправьте меня, если я что-то путаю, но предварительное вычисление длины результата с помощью "snprintf(NULL, ..." — в итоге выполняет одну и ту же работу по форматированию два раза?
char* strdup (const char *src);
Функция появилась в BSD, включена в POSIX, но не является частью стандартов ANSI/ISO, хотя поддерживается почти всеми компиляторами.
IIRC, sizeof(char) == 1
по стандарту и писать его не имеет смысла.
- 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() хорошо-бы проверить перед использованием.
Можно выделить три стадии развития кода и существования ошибки
1) Этап написания
2) Этап компиляции
3) Этап выполнения
VALGRIND, как динамический анализатор, работает на третьем, статический анализатор работает на первом.
На более раннем этапе поиск ошибки более надежен. На этапе написания у вас есть код программы с одной стороны и спецификации на код, библиотеки и компилятор с другой стороны.
На этапе выполнения у вас добавляется внешняя среда, которая может существенно влиять на поиск ошибки.
Чем больше ошибок удается отловить на первых двух этапах, тем лучше.
(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
Сейчас, к счастью, много инструментов, позволяющих улучшить качество кода.
Да очень просто. Весь «прикол» в том, что функция strlen не учитывает символ конца строки — '\0'. Даже если его явно указать во входящей строке (#define HELLO_STRING «Hello, Habr!\n\0»), он будет проигнорирован.
«Hello, Habr!\n\0» будет на один байт длиннее «Hello, Habr!\n», и естественно последний байт проигнорируется точно так же (хотя один завершающий ноль уж будет точно). Строго говоря строковый литерал содержит в себе все указанные символы + завершающий нулевой. Причем вне зависимости указаны ли нулевые символы внутри и тем более от их позиций. Нулевые символы внутри строкового литерала хоть и необычны, но вполне легитимные и иногда полезные. Например, я так экономил память: «Hello\0Привет», сдвигая строку перед печатью в зависимости от выбранного языка.
sizeof('\0')
совершенно точно делает не то, что вы просили. Согласно C99 sizeof('\0') == sizeof(int)
, если ваш компилятор выдаёт что‐либо иное, то в компиляторе ошибка. 6.4.4.4, параграф 10: «An integer character constant has type int.»
Ещё относительно snprintf(NULL, …)
: оно не всегда работает: http://demin.ws/blog/russian/2013/01/28/use-snprintf-on-different-platforms/. Статья довольно старая, но она говорит в том числе о Windows 7, которая ещё актуальна. Я не знаю, починили ли что‐то в более новых версиях или вам нужно делать #define snprintf _snprintf_s
.
Хотел написать про это, но вы уже сделали :)
Поэтому только про макросы с переменным числом параметров.
В той форме, что они применены в статье:
#define macro(args...) foo(args)
это расширение препроцессора Gcc: https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html
Легальная форма:
#define macro(...) foo(__VA_ARGS__)
Макросы с переменным числом параметров доступны с C99.
Не ведитесь на поролон — предыдущая статьи автора, опубликованная буквально недавно, написана в том же духе. Это какая-то дикая смесь троллинга и эффекта Даннинга — Крюгера.
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 байт, которой обычно хватает для записи строк. А если надо будет записать больше, выделяем нужное количество памяти на куче.
У нас в универе был препод который постоянно всем твердил — «Да я вам говорю что вы тупой».
Всем студентам.
#define strsize(args...) snprintf(NULL, 0, args) + sizeof('\0')
Нужно скобки писать, вот так:
#define strsize(args...) (snprintf(NULL, 0, args) + sizeof('\0'))
Иначе всякое там
strsize("abc") * 2
неправильно будет работать. Решили поучить других о C, а сами оплошали
Немного о строках в Си, или несколько вариантов оптимизировать неоптимизируемое