Всем начинающим программистам всегда рассказывают о важности правильного формирования сообщения об ошибках. Всегда говорят о том, что если программа что-то не смогла сделать, то она должна ясно и недвусмысленно рассказать почему это произошло. Рассказывают о важности контроля возвращаемого значения вызываемых функций. При этом даже компиляторы научились выдавать предупреждения если возвращаемое определенными функциями значение игнорируется. Надо сказать что важность обработки ошибок современными программистами принята. Временами это приводит к занятным казусам, как на КДПВ (взята здесь). Мне в реальной жизни несколько раз приходилось сталкиваться с подобными странными диагностическими сообщениями. О последнем случае и методах преодоления такой вот диагностики я и хочу рассказать. Если стало интересно милости прошу под кат. Опытные программисты наверняка не откроют для себя ничего нового, но пофилософствовать о разработке ПО точно смогут.
- правильно обрабатывать возвращаемые функцией read() значения важно
- не забываем проверять актуальность используемого софта (а OpenSource особенно)
- многофункциональные программы-комбайны в большинстве случаев одинаково неудобны для решения любой из допустимых задач. С другой стороны плодить «москитный флот» не лучшая идея
А вообще у меня грустная новость. Не будет больше никаких картинок. Мы опустимся на уровень системной консоли Linux и будем жить там. При этом будем радоваться. Ибо проект, с которым предстоит работать — достаточно известный загрузчик U-Boot. Проект с открытым исходным кодом, поддерживаемый компанией DENX Software Engineering. Потому порадуемся тому, что у нас есть консоль, есть системное окружение и вообще жизнь кипит. Потому как при работе с этим проектом, как правило, нет ничего подобного — сплошные области памяти, пересылки байт из одного места в другое да ожидание готовности периферии. Но, к частью, эта часть уже выполнена и есть вполне себе рабочий загрузчик для железки. Пора заняться украшениями, которые позволят прикладным программистам как-то влиять на процесс загрузки системы. Ничто не предвещает проблем. Задача давно решена и активно используется таким популярным проектами как OpenWRT и множеством других, чуть менее известных.
Суть очень проста. U-Boot корректирует свое поведение в зависимости от переменных среды. Переменные среды между перезагрузками могут быть сохранены в энергонезависимой памяти. Утилиты командной строки fw_printenv и fw_setenv позволяют соответственно выводить и менять их значение прямо из Linux. Все. В принципе большего и не требуется. Как всегда инструкцию мы будем читать «когда дым рассеется». Да и откуда здесь взяться дыму? Дым весь был выпущен когда загрузчик под эту плату адаптировали. Потому смело набираем команду «fw_printenv», потому как она-то точно ничего сломать не может.
localhost ~ # fw_printenv
Cannot open /dev/mtd1: No such file or directory
localhost ~ # fw_printenv --help
Usage: fw_printenv [OPTIONS]... [VARIABLE]...
Print variables from U-Boot environment
-h, --help print this help.
-v, --version display version
-c, --config configuration file, default:/etc/fw_env.config
-n, --noheader do not repeat variable name in output
-l, --lock lock node, default:/var/lock
Ну ожидаемо. Конечно. Мы не указали где именно хранятся переменные среды. А «быстрая помощь» однозначно говорит о том, что указать в командной строке и не получится. Надо править конфигурационный файл /etc/fw_env.config. Формат файла довольно простой и интуитивно понятный. Для того, чтоб не создавать самому себе (и окружающим) трудностей я разместил переменные среды U-Boot в самом доступном месте, которое только можно себе придумать. Конкретно в файле uboot.env первого раздела основного накопителя, отформатированного реально переносимой файловой системой vfat (она же FAT-32). И проверил. Из консоли U-Boot переменные сохраняются в файл, при старте из него читаются. Красота. Осталось только дать возможность их править из Linux. Раздел c файлом uboot.env, а еще ядром, файлом дерева устройств, и некоторым дополнительным наполнением критичным для работы системы совершенно логично монтируется к /boot. Потому совершенно не сомневаясь комментирую строчки 11 и 12 (/dev/mtd1 и /dev/mdt2 соответственно) и убираю комментарий со строчки 30 (/boot/uboot.env) в конфигурационном файле.
# VFAT example
/boot/uboot.env 0x0000 0x4000
Все. Вроде все подготовительные операции выполнены. Дубль два.
localhost ~ # fw_printenv
Read error on /boot/uboot.env: Success
Ну здравствуй, КДПВ. Первая разумная мысль, которая посещает любого Linux’оидника в такой ситуации — а что с правами. Впрочем, наш лозунг «Слабоумие и отвага» — мы работаем от root’а. Логично. Чего бояться человеку, который для железки делает загрузчик и имеет самый что ни на есть физический (с паяльником) доступ к плате? А может файла просто нет? Забыл в консоли U-Boot’а сказать «saveenv»? Проверим…
localhost ~ # ls -l /boot/uboot.env
-rwxr-xr-x 1 root root 8192 Dec 2 13:22 /boot/uboot.env
Нет, есть он. И даже читаться может всем миром (ай, как не хорошо). Интересно, а если его не будет?
localhost ~ # mv /boot/uboot.env /boot/uboot.env.bak
localhost ~ # fw_printenv
Cannot open /boot/uboot.env: No such file or directory
localhost ~ # mv /boot/uboot.env.bak /boot/uboot.env
Логично. Тут все правильно. Ладно, тяжело вздохнули и… Это наш кактус, нам его и грызть. Хорошо хоть исходники есть. Надо глянуть, что там у нас происходит. Может мысли какие появятся? Очень быстро находим строку 950 в файле tools/env/fw_env.c:
lseek(fd, blockstart + block_seek, SEEK_SET);
rc = read(fd, buf + processed, readlen);
if (rc == -1) {
fprintf(stderr, "Read error on %s: %s\n",
DEVNAME(dev), strerror(errno));
return -1;
}
if (rc != readlen) {
fprintf(stderr,
"Read error on %s: Attempted to read %zd bytes but got %d\n",
DEVNAME(dev), readlen, rc);
return -1;
}
Нет. Тут вполне себе классическая обертка над функцией read(). Практически прямиком из учебника. И, судя по поведению итоговой программы не остается сомнений в том, что read() возвращает -1, но при этом errno остается нулевым. Подождите. Что это за скрежет раздается? А, это мозги шевелиться начали… Хорошо…
Ну что, можно почитать мануал на read? Не, ерунда… Уж вроде про read-то все читано-перечитано. Все мыслимые и немыслимые варианты ошибок с функцией read() давно известны. Не должно быть такого. Что делаем дальше? Правильно, раз исходники не дают ответ — пусть его даст сама система.
localhost ~ # strace fw_printenv
execve("/usr/bin/fw_printenv", ["fw_printenv"], 0x7ebf2400 /* 28 vars */) = 0
brk(NULL) = 0x2118000
uname({sysname="Linux", nodename="localhost", ...}) = 0
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=42265, ...}) = 0
mmap2(NULL, 42265, PROT_READ, MAP_PRIVATE, 3, 0) = 0x76f14000
close(3) = 0
openat(AT_FDCWD, "/lib/libc.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0(\0\1\0\0\0\f~\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1286448, ...}) = 0
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x76f12000
mmap2(NULL, 1356160, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x76da1000
mprotect(0x76ed7000, 65536, PROT_NONE) = 0
mmap2(0x76ee7000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x136000) = 0x76ee7000
mmap2(0x76eea000, 8576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x76eea000
close(3) = 0
set_tls(0x76f12ca0) = 0
mprotect(0x76ee7000, 8192, PROT_READ) = 0
mprotect(0x4a9000, 4096, PROT_READ) = 0
mprotect(0x76f1f000, 4096, PROT_READ) = 0
munmap(0x76f14000, 42265) = 0
openat(AT_FDCWD, "/var/lock/fw_printenv.lock", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
flock(3, LOCK_EX) = 0
brk(NULL) = 0x2118000
brk(0x2139000) = 0x2139000
openat(AT_FDCWD, "/etc/fw_env.config", O_RDONLY) = 4
fstat64(4, {st_mode=S_IFREG|0644, st_size=1342, ...}) = 0
read(4, "# Configuration file for fw_(pri"..., 4096) = 1342
read(4, "", 4096) = 0
close(4) = 0
openat(AT_FDCWD, "/boot/uboot.env", O_RDONLY) = 4
fstat64(4, {st_mode=S_IFREG|0755, st_size=8192, ...}) = 0
close(4) = 0
openat(AT_FDCWD, "/boot/uboot.env", O_RDONLY) = 4
_llseek(4, 0, [0], SEEK_SET) = 0
read(4, "n.'\202__INF0__=Ravion-V2 I.MX6 CPU"..., 16384) = 8192
write(2, "Read error on /boot/uboot.env: S"..., 39Read error on /boot/uboot.env: Success
) = 39
close(4) = 0
flock(3, LOCK_UN) = 0
close(3) = 0
exit_group(1) = ?
+++ exited with 1 +++
localhost ~ #
Люблю Linux. Ай какая красота. Все сразу встало на свои места. Ладно, согласен — не все. Но уже что-то. Самое интересное здесь:
openat(AT_FDCWD, "/boot/uboot.env", O_RDONLY) = 4
_llseek(4, 0, [0], SEEK_SET) = 0
read(4, "n.'\202__INF0__=Ravion-V2 I.MX6 CPU"..., 16384) = 8192
write(2, "Read error on /boot/uboot.env: S"..., 39Read error on /boot/uboot.env: Success
) = 39
Пытаемся прочитать 16384 (16K), а можем только 8192 (8K). В принципе всё. Бинго. Поднимаемся выше и смотрим размер файла. Да, он действительно 8192 байта. Поднимаемся еще выше и смотрим строку в конфиге. Смещение 0, длина 0x4000 или 16384. Исправляем на 0x2000
# VFAT example
/boot/uboot.env 0x0000 0x2000
Да, черт возьми я очень стар. По мне U-Boot’у для переменных среды и килобайта хватит. Еще тут драгоценную память просто так расходовать. Что вы хотите. Коренной житель Питера. Нам с детства вбили что хлеб (ресурсы) надо беречь. А выбрасывать его — это не ценить память погибших из-за его отсутствия в блокаду. И да, мы такие. Спасибо реальным ветеранам, спасибо городским музеям. Которые не смотря ни на что сохраняют память о тех страшных временах. Надеюсь и мои дети этому научатся.
Так вот — о переменных среды для U-Boot. Ну два килобайта. Ну, ладно — четыре. Куда больше? Что там можно писать в таких количествах (и главное зачем)? Потому действительно — был момент когда выделенные по умолчанию 16К урезал до 8. Еще думал — куда столько? Ладно, лирику в сторону — проверяем.
localhost ~ # fw_printenv
__INF0__=Ravion-V2 I.MX6 CPU Module BSP package
__INF1__=Created: Alex A. Mihaylov AKA MinimumLaw, MinimumLaw@Rambler.Ru
[…]
boot_os=1
localhost ~ #
Работает. И даже fw_setenv работает.
localhost ~ # fw_setenv boot_os 0; fw_printenv boot_os
boot_os=0
Можно ставить точку? Думаю нет. Остался один важный вопрос, который уважающий себя программист не имеет права оставить без внимания. Как думаете, какой вопрос?
Правильно думаете. Если посмотреть на код, который лежит в репозитарии U-Boot, то можно легко заметить, что такая ситуация должна корректно отрабатываться. Я выше специально не стал вырезать этот кусок. Больше того, strace совершенно честно и открыто говорит что read возвращает значение 8192. Так почему же мы оказываемся в ветке с ошибкой чтения? Ведь 8192 никак не может равняться -1.
Давайте разбираться. Первая мысль, которая приходит в голову — подождите, но ведь Das U-Boot это динамично развивающийся проект. Может быть мы смотрим репозитарий с последним релизом загрузчика. Но та часть, которую используем мы совсем не обязана быть последней. Она часть пользовательского окружения операционной системы. Это я адаптирую последнюю версию загрузчика, чтоб пульс проекта ощущать. А авторы сборок прикладного ПО скорее за стабильность ратуют. Потому она наверняка последней и не будет. Проверяем.
localhost ~ # fw_printenv --version
Compiled with U-Boot 2019.10
localhost ~ #
Ага! А у меня в работе последняя стабильная (2020.10). Разница в год. Огромная дистанция для динамично развивающегося OpenSource проекта. А давайте посмотрим .
lseek(fd, blockstart + block_seek, SEEK_SET);
rc = read(fd, buf + processed, readlen);
if (rc != readlen) {
fprintf(stderr, "Read error on %s: %s\n",
DEVNAME(dev), strerror(errno));
return -1;
}
Ну да. Так и есть. Уже все исправили. Обидно. Такой красивый баг был. Ладно, на нашу жизнь багов еще припасено. Только успевай разбирать.
Но ведь и это ещё не все. А давайте заглянем в файл «uboot.env»
localhost ~ # hexdump -C /boot/uboot.env
00000000 0a 43 62 eb 5f 5f 49 4e 46 30 5f 5f 3d 52 61 76 |.Cb.__INF0__=Rav|
00000010 69 6f 6e 2d 56 32 20 49 2e 4d 58 36 20 43 50 55 |ion-V2 I.MX6 CPU|
00000020 20 4d 6f 64 75 6c 65 20 42 53 50 20 70 61 63 6b | Module BSP pack|
00000030 61 67 65 00 5f 5f 49 4e 46 31 5f 5f 3d 43 72 65 |age.__INF1__=Cre|
[...]
00000720 3d 71 70 00 76 65 6e 64 6f 72 3d 72 61 76 69 6f |=qp.vendor=ravio|
00000730 6e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |n...............|
00000740 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00002000
localhost ~ #
Вполне очевиден факт — оценка размера блока достаточного для хранения переменных среды, которая была дана мною выше, вполне справедлива. На данный момент использовано 1837 байт (0x7031 – 4) и формат блока довольно простой. Первые 4 байта CRC32, а дальше разделенные нулем переменные в формате переменная=значение. Другими словами поведение утилиты все равно вызывает вопросы. Ну напишет она что размер файла меньше ожидаемого и завершится с ошибкой. Но ведь это не правда. Все значимые и важные данные в него (даже в двухкилобайтный!) вполне бы влезли. Может все же стоит поправить?
Увы нет. И причина этому вполне банальна. Переменные в U-Boot могут храниться в самых разных местах. Файл на vfat разделе это самое приятное место. За это его и выбрал. Но в том же OpenWRT нет таких удобных накопителей. Там SPI-flash. И под переменные среды выделяется целый сектор. Но и тут все может быть не так плохо. Сектор целиком надо стирать. Писать можно частями. Беда с системами, которые используют dataflash или некоторые варианты raw-NAND накопителей. Т.е. с теми системами, которым помимо данных нужна еще и контрольная информация для контроля целостности и исправности. Вот они обязаны писать весь блок целиком.
Получается интересная альтернатива. Или мы для разных накопителей делаем разные утилиты для работы с переменными среды или… Делаем одинаково неудобно для всех. Посмотрите на код утилиты. Посмотрите на формат сохраняемых данных. К сожалению почти всегда так и получается. Хотели, чтоб всем было одинаково хорошо. На выходе всем получилось одинаково плохо. Впрочем, свою функциональность решение вполне обеспечивает. Пусть так и остается.
Вот так неожиданно и получилось легонькое пятничное чтиво. Было бы интересно посмотреть сколько времени суммарно прожила эта ошибка, но… Уже не настолько интересно, чтоб тратить на это время. Как говаривал классик: «Сказка ложь, да в ней намек. Добрым молодцам урок.» Спасибо за то, что дочитали.
P.S.
Пользуясь случаем передаю привет CodeRush Еще раз благодарю за приглашение на Habr. И да, всегда хочется писать о серьезном — о компиляторах, о безопасном программировании непосредственно по железу. А сил хватает только на легкое пятничное чтиво. Ладно, будем считать, что начало положено. Большое путешествие всегда начинается с маленького шага.
CodeRush
Пожалуйста. Вообще статья получилась плюс-минус «обо всем и не о чем», но в качестве иллюстрации к тому, что надо бы сначала версию выяснить, а потом идти код смотреть — пойдет.
Error: success, особенно если это был errno — весьма популярная ситуация даже у тех, кто на C давно пишет, да и сама по себе идея errno довольно глупая (потому что одна глобальная переменная без каких-либо метаданных о том, кто ее выставил и когда — это, скажем так, недостаточно), но теперь уже никто исправлять стандартную библиотеку не станет, поэтому приходится жить с этим всем.