LINUX.ORG.RU

Программирование древнего и экзотического big-endian ARM

 , , ,


1

2

Нашёл в условном ящике стола мобильник Motorola SLVR L6, которому почти 20 лет. Протёр пыль, зарядил батарейку «лягушкой» – до сих пор рабочий чертяка. Оказалось, что под него можно кодить не только на унылом подмножестве Java – J2ME, но и на православной сишке и даже плюсцах. Скачал кросс-компилятор и SDK, доступные как под Linux, так и под Windows и решил написать пару GUI-программок и подёргать функции из прошивки телефона, но столкнулся с одной странной особенностью, про которую хочу уточнить у специалистов.

Есть у меня следующий минимально-неработающий пример и функция wtf_foo(), которую по хорошему нужно инлайнить, но похоже что в кросс-компиляторе вообще не работают инлайны:

#define SWAP_UINT32(x) (((x) >> 24) | (((x) & 0x00FF0000) >> 8) | (((x) & 0x0000FF00) << 8) | ((x) << 24))

static uint8_t bmp_header[70] = {
	0x42, 0x4D, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x38, 0x00,
	0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x10, 0x00, 0x03, 0x00,
	0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x1F, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

static void wtf_foo(uint8_t *start_address, uint32_t start_offset, uint32_t value) {
	*((uint32_t *) &start_address[start_offset]) = value;
	/* start_address[start_offset + 0x00] = (value >>  0) & 0x000000FF; */
	/* start_address[start_offset + 0x01] = (value >>  8) & 0x000000FF; */
	/* start_address[start_offset + 0x02] = (value >> 16) & 0x000000FF; */
	/* start_address[start_offset + 0x03] = (value >> 24) & 0x000000FF; */
}

void main(void) {
	print_hex();
	wtf_foo(bmp_header, 0x02, SWAP_UINT32(41030));
	wtf_foo(bmp_header, 0x12, SWAP_UINT32(128));
	wtf_foo(bmp_header, 0x16, SWAP_UINT32(160));
	wtf_foo(bmp_header, 0x22, SWAP_UINT32(40960));
	print_hex();
}

Результаты исполнения кода:

42 4D |FF FF FF FF| 00 00 00 00 46 00 00 00 38 00
00 00 |FF FF FF FF FF FF FF FF| 01 00 10 00 03 00
00 00 |FF FF FF FF| 00 00 00 00 00 00 00 00 00 00         (1)
00 00 00 00 00 00 00 F8 00 00 E0 07 00 00 1F 00
00 00 00 00 00 00

42 4D |46 A0 00 00| 00 00 00 00 46 00 00 00 38 00
00 00 |80 00 00 00 A0 00 00 00| 01 00 10 00 03 00
00 00 |00 A0 00 00| 00 00 00 00 00 00 00 00 00 00         (2)
00 00 00 00 00 00 00 F8 00 00 E0 07 00 00 1F 00
00 00 00 00 00 00

|46 A0 00 00| FF FF 00 00 00 00 46 00 00 00 38 00 
|80 00 00 00 A0 00 00 00| FF FF 01 00 10 00 03 00 
|00 A0 00 00| FF FF 00 00 00 00 00 00 00 00 00 00         (3)
00 00 00 00 00 00 00 F8 00 00 E0 07 00 00 1F 00 
00 00 00 00 00 00
  1. Оригинальный массив без изменений.
  2. Результат работы кода на x86_64, little-endian, хост.
  3. Результат работы кода на ARMv4T, big-endian, целевая железка.

Примечание: Макрос SWAP_UINT32() используется только на целевом устройстве. Код функции wtf_foo() который закомментирован отрабатывает везде одинаково и нормально.

Не понимаю откуда тут берётся сдвиг влево, почему он именно на два байта? Хочется разобраться в чём именно причина этой ошибки? Где-то моя невнимательность при работе с ARM и big-endian или что-то другое, к примеру, баг в компиляторе?

Дополнительная информация, если кому интересно:

Компилятор:
     ARM C/C++ Compiler, ADS1.2 [Build 848]
     Thumb C/C++ Compiler, ADS1.2 [Build 848]
Железо:
     SOC: Freescale/Motorola Neptune LTE
     CPU: ARM7TDMI | ARMv4T | big-endian | 52 MHz
     RAM: (4) 8 MB, 1.5 MB free.
     DSP: 104 MHz StarCore DSP
Операционная система:
     P2K OS: Synergy Environment + VRTXmc RTOS Kernel.

P.S. Внутри девайса не Linux, но есть какой-то кастрированный libc и UNIX-подобная регистрозависимая файловая система.

★★★★★

Последнее исправление: EXL (всего исправлений: 3)

Ответ на: комментарий от i-rinat

Большое спасибо, похоже это оно и есть.

Сделал у себя:

*((__packed UINT32 *) &start_address[start_offset]) = value;

Оно начало ругаться так:

Error: L6218E: Undefined symbol __rt_uwrite4 (referred from Screenshot.o).
Finished: 0 information, 0 warning and 1 error messages.

Буду искать эту __rt_uwrite4() и пытаться заюзать у себя.

https://github.com/pleasemarkdarkly/adotcorporation/blob/master/CirrusPlayer/Rel0016/player/support/uwrite4.S

Тут вот нашёл её исходник на асме, но пока не знаю как его скомпилить и подцепить правильно.

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 1)
Ответ на: комментарий от EXL

Достаточно ли там умный компилятор, чтобы вызовы memcpy() оптимизировать? Если да, то memcpy() — вполне себе решение.

i-rinat ★★★★★
()
Последнее исправление: i-rinat (всего исправлений: 1)
Ответ на: комментарий от i-rinat

Достаточно ли там умный компилятор, чтобы вызовы memcpy() оптимизировать? Если да, то memcpy() — вполне себе решение.

Не думаю, он очень древний, года так 2002-2003. Хотя код и ELF’ы генерирует довольно компактные.

Посмотрел в прошивку телефона и там оказывается есть эта функция и у неё вот такая сигнатура/паттерн:

__rt_uwrite4 A E5C10003E1A02420E5C1

Скриншот из IDA Pro: https://habrastorage.org/webt/sd/ql/sg/sdqlsgba4lszhhro2rsnwrec74q.png похоже что это практически тоже самое, что я нашёл по ссылке на GitHub выше.

А так оно будет выглядеть в либе (для разных прошивок), попробую сейчас добавить всё это в либу, чтобы была возможность использовать это выравнивание:

0x1009D658 A __rt_uwrite4 ; R3511
0x1009D5F4 A __rt_uwrite4 ; R3443H

Осталось только угадать количество аргументов, подозреваю что он там один.

EXL ★★★★★
() автор топика

@bugfixer

ПыСы. А что на ARM’ах аналога bswap нет?

А не, не совсем правильно прочитал asm - aligned write походу оптимизирован

Я в Assembler’е под ARM не особо разбираюсь, но возможно всякие там RBIT, REV, REV16 и т. д. являются аналогом этого bswap:

https://developer.arm.com/documentation/dui0489/h/arm-and-thumb-instructions/rev--rev16--revsh--and-rbit

Проблема в том, что они >= ARMv6, а у меня – ARMv4T.

EXL ★★★★★
() автор топика
Ответ на: комментарий от EXL

Проблема в том, что они >= ARMv6, а у меня – ARMv4T.

Понял. Обидно.

Но Вы поаккуратней - в Вашем макросе очень неприятный баг с sign-extention на сдвиге вправо - не дай бог старший битик взведён будет в аргументе - Вы порастёте. Заверните хотя бы в inline helper явно принимающий uint32_t.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Заверните хотя бы в inline helper явно принимающий uint32_t.

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

Ну или дёргайте за htonl (в предположении что даже в покоцанном libc он имеется).

Да, похоже что он есть в прошивке, как и кучка всяких bswap(), byteSwap32() и др., но вытаскивать их в либу довольно муторно, пока не знаю как сделать лучше.

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 1)
Ответ на: комментарий от bugfixer

В общем в этом ADS-компиляторе по умолчанию используется стандарт C90 без ключевого слова inline, поэтому нужно либо использовать ключевое слово __inline, либо врубать флажок C99 и использовать обычный inline keyword.

https://stackoverflow.com/a/26899811

EXL ★★★★★
() автор топика

@i-rinat @bugfixer

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

Теперь можно перейти к написанию чего-нибудь интересненького, благодаря тому что можно дёргать функции прошивки платка телефона по сути превращается в полноценный GSM/GPRS-модуль для различных самоделок и т. д.

Если кому интересно – грязный и пока ещё неотрефакторенный код:

https://github.com/EXL/P2kElfs/blob/master/Screenshot/Screenshot.c

Там довольно интересный фреймворк для приложений и псевдомногозадачности используется, похожий на State Machine подвязанный на Event’ы, а прямая адресация без всякой защиты позволяет делать очень интересные вещи, к примеру, читать куски самой прошивки, а ещё уникальные для каждого устройства калибровки NVRAM (PDS) просто выполнив какой-нибудь fread(void *) 0x10010000, 0xFFFF, 1, dump_file), ну и оперативку точно так же можно сдампить.

С калибровками отдельная тема, так как там хранится информация о SIM-Lock, эту область очень сильно защищают, к примеру прошивальщики не могут оттуда ничего читать и писать, а SOC-чип вообще шифрует некоторые ячейки данных 3DES и своим серийным номером. В общем, так просто не прочитать и не изменить их извне, но с помощью функций прошивки и прямой адресации – пожалуйста.

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 3)
Ответ на: комментарий от EXL

В общем, спасибо за помощь

Рад был помочь ;)

не знал о такой особенности в древних ARM’ах с этим выравниванием адресов.

По факту нынче все развращены всепрощающим x86 и на такие «мелочи» не обращают внимание. Спарки обожали кидать SIGBUS при misaligned memreads/writes. Кто об это обжигался - тот «подкован» ;)

Если кому интересно – грязный и пока ещё неотрефакторенный код

Посмотрел «по диагонали». Довольно чистенько :) Обратите внимание на обработку ошибок (allocation failures etc).

ПыСы: там где «WTF» - прикольно ;) А чего ему не хватает? Типа аргумент в регистре передаётся и адрес взять нельзя?

bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от bugfixer

По факту нынче все развращены всепрощающим x86 и на такие «мелочи» не обращают внимание. Спарки обожали кидать SIGBUS при misaligned memreads/writes. Кто об это обжигался - тот «подкован» ;)

Это да. Кстати у более дорогих моторолок того времени была ещё более экзотическая архитектура CPU собственной разработки, о которой вообще мало кто когда-либо слышал.

Называлась она M·CORE и была каким-то там идейным наследником M68K для Embedded, хотя версия M68K для Embedded тоже существовала, так называемый DragonBall.

Так вот, процессоры на этой архитектуре M·CORE ставились в продвинутые в технологическом плане и дорогие мобильники с 3G и видеосвязью, тогда как уделом процессоров ARM был рынок бюджетных мобилок за $100-$200.

Но за 20+ лет всё очень круто изменилось и теперь про M·CORE многие слышат в первый раз, а ARM вознёсся на недосягаемые высоты.

ПыСы: там где «WTF» - прикольно ;) А чего ему не хватает? Типа аргумент в регистре передаётся и адрес взять нельзя?

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

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 1)

@kuzulis

Я помню что ты очень активно работал с QBS и использовал эту систему сборки и Qt Creator IDE под различные проекты для Embedded-систем.

Собственно, ближе к делу. Вот ковыряюсь с одним экзотическим сабжевым BigEndian ARM’ом, написал такой Makefile и подцепил его в Qt Creator с помощью Generic Projects:

# Compiler path.
ARM_PATH = /opt/arm

# SDK path.
SDK_PATH = $(ARM_PATH)/SDK

# Libraries path.
LIB_PATH = $(ARM_PATH)/lib

# Main link library.
# LIB_MAIN = Lib.o
LIB_MAIN = Lib_L2_L6.o

# Defines.
DEFINES = -D__P2K__

# ELF name.
ELF_NAME = Screenshot

all:
	$(ARM_PATH)/bin/tcc -I$(SDK_PATH) $(DEFINES) -bigend -apcs /interwork -O2 -c $(ELF_NAME).c \
		-o $(ELF_NAME).o
	$(ARM_PATH)/bin/armlink -nolocals -reloc -first $(LIB_MAIN)\(Lib\) $(ELF_NAME).o $(LIB_PATH)/$(LIB_MAIN) \
		-o $(ELF_NAME).elf

clean:
	-rm -f $(ELF_NAME).o
	-rm -f $(ELF_NAME).elf

Но Generic Project довольно неудобные и в них куча файликов. Могу ли я как-нибудь перенести это всё на декларативные рельсы QBS? Наверное идеальным и простым был бы для меня вариант не описывать все эти тулчейны, а сделать так, чтобы QBS вызывал make и make.bat для сборки, при этом чтобы прокидывались хедеры SDK, флажки предупреждений и всё остальное для постройки кодовой модели так, как оно сейчас работает в Generic Projects.

EXL ★★★★★
() автор топика
Ответ на: комментарий от EXL

Наверное так просто с наскока - нет.

Тут надо разделить на два шага:

  1. Это добавление поддержки самого компилятора.

  2. Добавления модуля СДК.

В принципе, я могу помочь, ты только дай:

  1. Сам компилятор, где скачать, есть ли под винду.

  2. Сам СДК, где скачать, есть ли под винду.

  3. Какой нибудь простой пример твоей аппликухи на мейкфайлах.

Я попробую интегрировать все на Qbs, на гитхаб куда нить, и дам ссыль.

А ты уже потом, если понравится, можешь заиспользовать у себя. ))

kuzulis ★★
()
Ответ на: комментарий от bugfixer

Посмотрел «по диагонали». Довольно чистенько :)

https://github.com/EXL/P2kElfs/blob/master/Screenshot/Screenshot.c

Ещё немного почистил и даже прикрутил GUI по типу:

https://cdn.discordapp.com/attachments/988875281097957476/1048824947797143562/screen25.png

EXL ★★★★★
() автор топика
Ответ на: комментарий от kuzulis

В принципе, я могу помочь, ты только дай:

Это запросто!

Компилятор и SDK в одном архиве содержимое которого просто распаковывается в C:\ARM на Windows или /opt/arm на Linux.

Какой нибудь простой пример твоей аппликухи на мейкфайлах.

Любой пример вот отсюда:

https://github.com/EXL/P2kElfs

EXL ★★★★★
() автор топика
Ответ на: комментарий от kuzulis

А на какой именно EXE-ругнулось? На Win10 у меня ничего не детектит. Там в архиве ещё пачка инструментов, которые хакают прошивку, ищут функции по паттернам, возможно это из-за них подобное.

Залил просто чистый компилятор и SDK-хедеры:

EXL ★★★★★
() автор топика
Ответ на: комментарий от kuzulis

Понял, это утилита для редактирования либы с таблицей импортируемых функций.

Для работы компиляторов и SDK она не требуется. Выше версия без всего этого барахла.

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 2)
Ответ на: комментарий от kuzulis

Я что-то поискал, поискал и ничего толкового найти не смог. История у этого ADS-компилятора довольно запутанная. Похоже это когда-то было официальным компилятором от ARM, который пилил Keil, потом он был куплен самим ARM’ом или лицензирован у него и на его основе ARM уже начал пилить какие-то собственные инструменты, которые потом перешли на LLVM с течением времени.

Доки на сайте ARM уже под всё новое, а старого именно под этот ADS я что-то найти не смог. Но некоторое из новых доков перекликается с этим ADS. В любом случае флажки я расставляю «наугад», а их список смотрю в HEX-редакторе (какая жесть, да) открыв какой-нибудь tcc.exe и armlink.exe из набора, так как во внутренней справке многих флажков нет.

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 1)
Ответ на: комментарий от kuzulis

А хотя вот нарыл сейчас спеку похоже:

http://www.ee.ncu.edu.tw/~jfli/soc/lecture/ADS_DeveloperGuide_C.pdf

Под версию 1.1, правда, а не под 1.2, но хоть что-то.

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 1)
Ответ на: комментарий от EXL

Ага, ладно уже, обошелся и без документации, как обычно. ))

Вот сам проект https://github.com/denis-shienkov/p2k-examples

Там реализована только сборка самого приложения с минимумом опций из модуля cpp.

Хм, в кубсе еще до сих пор не реализовали нормального переиспользования модулей и вспомогательных JS файлов, поэтому пришлось копировать рализацию модуля CppModulle.qbs прямо из самого кубса (там нужен определенный минимальный набор пропертей).

В принципе компиляет и линкует твое приложение screenshot.c, хотя я в опции линкера не добавлял флаг -first %LIB_MAIN%(Lib), т.к. я хз что это (но догадываюсь), тут, ты сам, уже наверно, если надо. ))

kuzulis ★★
()
Ответ на: комментарий от kuzulis

Отлично, большое спасибо! Декларативный подход всё-таки выглядит очень удобно.

В принципе компиляет и линкует твое приложение screenshot.c, хотя я в опции линкера не добавлял флаг -first %LIB_MAIN%(Lib), т.к. я хз что это (но догадываюсь), тут, ты сам, уже наверно, если надо. ))

Ага, эта часть нужна, без неё ELF-программа не запускается, поэтому добавил такое в tcc.js:

// Input library relocation.
args.push("-first", product.cpp.staticLibraries[0] + product.cpp.staticLibrarySuffix + "(Lib)");

Такой вопрос, а директорию qbs/modules можно как-нибудь в корень тулчейна, то есть C:\ARM или /opt/arm/ добавить таким образом, чтобы оно не путалась под ногами? И чтобы где-нибудь в Kit это всё задавалось, дабы в сами qbs-файлы не писать прямые пути до этой директории.

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 5)
Ответ на: комментарий от kuzulis

Документация по Qbs ответила на мой вопрос:

https://doc.qt.io/qbs/custom-modules.html#making-custom-modules-and-items-available-across-projects

Установка preferences.qbsSearchPaths: C:\ARM\qbs в Additional Qbs Profile Settings позволила перенести Qbs-модуль описывающий SDK внутрь самого SDK, в общем теперь всё отлично и удобно, как я и хотел.

Хм, в кубсе еще до сих пор не реализовали нормального переиспользования модулей и вспомогательных JS файлов, поэтому пришлось копировать рализацию модуля CppModulle.qbs прямо из самого кубса (там нужен определенный минимальный набор пропертей).

Да, вот этот момент было бы круто обтесать в самом Qbs, условно через то же наследование, например. Чтобы можно было подобные SDK тоже описывать более-менее декларативно.

EXL ★★★★★
() автор топика
Ответ на: комментарий от EXL

Такой вопрос, а директорию qbs/modules можно как-нибудь в корень тулчейна, то есть C:\ARM или /opt/arm/ добавить таким образом, чтобы оно не путалась под ногами

Не, можно в директорию с самим кубсом по идее скопировать, туда где все cpp модули лежат.

Например у меня в винде это:

c:\Qt\Tools\QtCreator\share\qtcreator\qbs\share\qbs\modules\cpp\

в общем теперь всё отлично и удобно, как я и хотел.

А, ну или так ))

UPD: Ну и это cpp.staticLibraries: ["Lib_L2_L6"] можно спрятать либо в SDK либо в тулчейн , чтобы в приложениях каждый раз не писать это.

kuzulis ★★
()
Последнее исправление: kuzulis (всего исправлений: 2)
Ответ на: комментарий от kuzulis

UPD: Ну и это cpp.staticLibraries: [«Lib_L2_L6»] можно спрятать либо в SDK либо в тулчейн , чтобы в приложениях каждый раз не писать это.

Не, это ты очень правильно что вынес отдельно, там может меняться эта статическая либа и ещё потом добавляться другие либы после неё в некоторых случаях, например, для отладочного вывода цепляется dbg.o, для чтения INI-файликов ini.o и т. д.

EXL ★★★★★
() автор топика
15 января 2023 г.
Ответ на: комментарий от kuzulis

@kuzulis

Опять я про QBS, случайно не знаете как подфиксить такую ошибку?

tcc.js:84: error: TypeError: Result of expression 'input' [undefined] is not an object.

Она появляется при попытке линковки двух и более оbj-файлов в один ELF. Если линковать один obj-файлик, то всё норм.

Где-то в этих местах собака зарылась:

// tcc.js
function prepareLinker(project, product, inputs, outputs, input, output) {
    var cmds = [];
    var args = linkerFlags(project, product, inputs, outputs);
    var linkerPath = input.cpp.linkerPath; // 'input' [undefined] is not an object.
    var cmd = new Command(linkerPath, args);
    cmd.description = "linking " + outputs.application[0].fileName;
    cmd.highlight = "linker";
    cmd.jobPool = "linker";
    cmds.push(cmd);
    return cmds;
}

// tcc.qbs
Rule {
        id: linker
	multiplex: true
        inputs: ["obj"]
        outputFileTags: ["application"]
        outputArtifacts: [{
                fileTags: ["application"],
                filePath: FileInfo.joinPaths(product.destinationDirectory,
                                             PathTools.applicationFilePath(product))
            }];
        prepare: Tcc.prepareLinker.apply(Tcc, arguments)
    }
EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 2)
Ответ на: комментарий от kuzulis

Кажись решил, вот что было:

diff -Nuar qbs/modules/cpp/tcc.js qbs1/modules/cpp/tcc.js
--- qbs/modules/cpp/tcc.js      2022-12-07 09:12:26.221224200 +0700
+++ qbs1/modules/cpp/tcc.js     2023-01-15 17:43:55.440539000 +0700
@@ -69,7 +69,7 @@
 function prepareCompiler(project, product, inputs, outputs, input, output, explicitlyDependsOn) {
     var cmds = [];
     var args = compilerFlags(project, product, input, outputs, explicitlyDependsOn);
-    var compilerPath = input.cpp.compilerPath;
+    var compilerPath = product.cpp.compilerPath;
     var cmd = new Command(compilerPath, args);
     cmd.description = "compiling " + input.fileName;
     cmd.highlight = "compiler";
@@ -81,7 +81,7 @@
 function prepareLinker(project, product, inputs, outputs, input, output) {
     var cmds = [];
     var args = linkerFlags(project, product, inputs, outputs);
-    var linkerPath = input.cpp.linkerPath;
+    var linkerPath = product.cpp.linkerPath;
     var cmd = new Command(linkerPath, args);
     cmd.description = "linking " + outputs.application[0].fileName;
     cmd.highlight = "linker";

Не знаю, правда, правильно это или нет. У вас была опечатка в модуле qbs для tcc или нет. В статье Катаем «смоляной шарик» или создание собственных правил сборки с помощью Qbs нашёл такую инфу:

Параметры input и output не определены (undefined), если для этого правила имеется несколько артефактов ввода (и вывода соответственно). Служат они как синтаксический сахар: input = inputs[0] и output = outputs[0] и являются списками с одним элементом.

EXL ★★★★★
() автор топика
Последнее исправление: EXL (всего исправлений: 1)