Русский
Русский
English
Статистика
Реклама

Asm

Приключение чисел в ASCII-ландии. Часть 0x01u. Беззнаковые целые числа

10.10.2020 16:22:36 | Автор: admin


Думаю, с переводом чисел в ASCII строки в своей жизни сталкивался каждый программист. В свое время для меня было удивительно узнать, что перевод десятичной цифры в равнозначный ASCII символ операция сложения. С этим знанием я ложился спать, и с этим же знанием я бодро просыпался утром. Но однажды я задал себе вопрос а как переводятся числа с плавающей запятой: Float или Double!? С этого момента, сна в моей жизни, а тем более крепкого и спокойного стало меньше. Уверен, не я один задавался этим вопросом, и более того, не я один нашел ответ на оный. Но я думаю, есть те, кто заблудился, те, кто до сих пор неровно дышит от полного непонимания, что же происходит под капотом этих ваших трансляторов, компиляторов и прочего-прочего. Более того, не только полное отсутствие знаний в трансляции чисел нарушало мое психическое равновесие: люди, услышавшие мои душевные страдания, кидали сомнения в нужности и полезности этого знания. Мне говорили так: Ну раскроешь ты завесу тайны, а дальше то что?! Напишешь свой велосипед, который будет работать в сто раз медленнее?! Иди ка ты, Ваня, асфальт укладывай, а мы тут великим займемся вон, JSON пришел, надо еще подумать, как его переложить.... Я же, парировал: Нет, я напишу мотоцикл! Он будет быстр как Ямаха, а его рев будет устрашать даже матерых программистов!.

Заметка: все тесты и весь код писались / запускались исключительно под x86_x64 архитектуру, little-endian формат. Эту зависимость, следует держать в уме при чтении материала.

Сразу начать писать о переводе плавающих чисел в ASCII строки будет не верно, ведь моя задача не только рассказать о методах и алгоритмах лежащих внутри этих переводов, но и рассказать о том, как мне удалось их улучшить сделать быстрее, агрессивнее! Как-никак, я автор вот этого канала про нуууууу очень хардкорные Оптимизации Asm[x86, ARM] и C / C++. Делать решения быстрее, искать обходные пути мое хобби, мой life-style. Именно поэтому, эту статью я хотел бы посвятить переводу беззнаковых 32-ух-битных чисел в ASCII строки. Поверьте мне, рассказ будет полезным и, надеюсь, интересным: то, что я расскажу вам придумал я и на просторах сети не встречал нигде! Я не берусь утверждать, что в чем-то превзошел кого-либо, мне просто хочется поделиться своим результатом, трюком который, может, поможет и вам. Более того, вещи описанные здесь, упростят жизнь и читателю, в понимании дальнейшего развития цикла статей, и мне в описании того, как это работает(смогу делать отсылки на эту статью).

Начнем с малого: разберем примитивный алгоритм перевода 32-ух битного беззнакового целого числа в ASCII строку и опишем его недостатки.

В самом общем случае, мы получаем последнюю цифру числа, записываем ее ASCII представление в область памяти строки(в начало строки, и увеличиваем индекс начала на 1), и то же самое проделываем для оставшихся цифр числа. Затем, строку переворачиваем и получаем ответ. Конечно, мы можем ничего не переворачивать, а идти в обратную сторону с конца итоговой числовой строки, уменьшая текущую позицию. Для этого, нам все же нужно будет эту конечную позицию вычислить, то есть придется вычислить длину получающейся строки. В этом алгоритме есть моменты, на которые стоит обратить внимание если вы хотите, чтобы это работало быстрее:

1. Мы должны делать операции взятия остатка на 10, а само число на каждой итерации делить нацело на 10. Здесь, конечно, деления не будет компилятор заменит его умножением.
2. Операции из пункта один могут запросто создавать Dependency Chain в конвейере процессора.
3. Мы постоянно обращаемся к памяти, чтобы что-то туда записать, что-то достать дело тут не столько в КешМиссах, сколько в том, что даже наличие значения в кеше L1 чего-то да стоит, и опять же, грузит блоки конвейера Процессора, по линиям RW. Более того, команда MOV ассемблера при работе с голой памятью, если верить Агнеру Фогу, имеет больший latency, нежели ее регистровый собрат.
4. Мы как минимум должны вычислить либо длину строки, либо произвести операцию переворота строки, что опять же излишества

Оптимизации этого алгоритма существуют, и как правило лежат в основе различных библиотек: например, плюсовый to_chars(C++17), а еще abseil, google-овый.

Вот какие улучшения я встречал:

1. Использование LUT, для того, чтобы записывать в строку не по одной цифре, а по две за раз(деление нацело на 100, остаток из той же серии в конце перевода, делаем проверку на то, что у нас осталась либо одна цифра, либо деление завершено).
2. Разворот цикла бегущего по цифрам числа, точнее избавление от оного вовсе.

По-крайней мере, первый пункт из списка оптимизаций выше, неминуемо ведет к работе с памятью и дальнейшей амортизации скорости доступа кешем процессора: старые данные вытесняем, новые записываем. Здесь, как повезет, но будет круто, если данные LUT всегда лежат в кеше L1(L2, L3 кеши, конечно, лучше голой памяти, но все равно теряем много), а механизм хардварного префетчинга работает на ура: загрузка данных из оперативной памяти это около 100+ тактов процессора, даже больше. Тем не менее, полную зищиту от воздействия Кеш Памяти, нам дает отсутствие работы с памятью как таковой.

Вот, например, попытки улучшить to_chars в GCC видим, например, оптимизацию за счет LUT. А второй вариант, и вовсе пытается выполнять сжатие этой самой LUT до размера одной кеш линии, что, конечно, идет в ущерб производительности на горячий кеш.

В интернете мождно найти парочку неплохих бенчмарков, сравнивающих существующие реализации перевода беззнаковых, 32-ух битных чисел в ASCII строку по скорости выполнения перевода. Я прогнал многие из них, и выводом получил, что самая быстрая реализация перевода из libfmt. Позже, однако, мне написал danlark (разработчик из google) и предложил потестировать еще и реализацию от google, из библиотеки abseil. Собственно, оная сумела немного обойти реализацию libfmt. Ниже, я привожу код этой реализации, так как именно с ней и буду производить дальнейшие сравнения(все исходники вы сможете найти отдельной ссылкой на github в конце статьи, если читать под спойлером вам не удобно, например):

Код, конечно, на Хабре немного поехал...
const char one_ASCII_final_digits[10][2]        {                {'0', 0}, {'1', 0}, {'2', 0}, {'3', 0}, {'4', 0},                {'5', 0}, {'6', 0}, {'7', 0}, {'8', 0}, {'9', 0},        };const char two_ASCII_digits[100][2] =        {                {'0', '0'}, {'0', '1'}, {'0', '2'}, {'0', '3'}, {'0', '4'}, {'0', '5'},                {'0', '6'}, {'0', '7'}, {'0', '8'}, {'0', '9'}, {'1', '0'}, {'1', '1'},                {'1', '2'}, {'1', '3'}, {'1', '4'}, {'1', '5'}, {'1', '6'}, {'1', '7'},                {'1', '8'}, {'1', '9'}, {'2', '0'}, {'2', '1'}, {'2', '2'}, {'2', '3'},                {'2', '4'}, {'2', '5'}, {'2', '6'}, {'2', '7'}, {'2', '8'}, {'2', '9'},                {'3', '0'}, {'3', '1'}, {'3', '2'}, {'3', '3'}, {'3', '4'}, {'3', '5'},                {'3', '6'}, {'3', '7'}, {'3', '8'}, {'3', '9'}, {'4', '0'}, {'4', '1'},                {'4', '2'}, {'4', '3'}, {'4', '4'}, {'4', '5'}, {'4', '6'}, {'4', '7'},                {'4', '8'}, {'4', '9'}, {'5', '0'}, {'5', '1'}, {'5', '2'}, {'5', '3'},                {'5', '4'}, {'5', '5'}, {'5', '6'}, {'5', '7'}, {'5', '8'}, {'5', '9'},                {'6', '0'}, {'6', '1'}, {'6', '2'}, {'6', '3'}, {'6', '4'}, {'6', '5'},                {'6', '6'}, {'6', '7'}, {'6', '8'}, {'6', '9'}, {'7', '0'}, {'7', '1'},                {'7', '2'}, {'7', '3'}, {'7', '4'}, {'7', '5'}, {'7', '6'}, {'7', '7'},                {'7', '8'}, {'7', '9'}, {'8', '0'}, {'8', '1'}, {'8', '2'}, {'8', '3'},                {'8', '4'}, {'8', '5'}, {'8', '6'}, {'8', '7'}, {'8', '8'}, {'8', '9'},                {'9', '0'}, {'9', '1'}, {'9', '2'}, {'9', '3'}, {'9', '4'}, {'9', '5'},                {'9', '6'}, {'9', '7'}, {'9', '8'}, {'9', '9'}        };static inline void PutTwoDigits(size_t i, char* buf){    memcpy( buf, two_ASCII_digits[i], 2 );}char* uint32_to_string_1        (                uint32_t i,                char    *buffer        ) noexcept{  uint32_t digits;  // The idea of this implementation is to trim the number of divides to as few  // as possible, and also reducing memory stores and branches, by going in  // steps of two digits at a time rather than one whenever possible.  // The huge-number case is first, in the hopes that the compiler will output  // that case in one branch-free block of code, and only output conditional  // branches into it from below.  if (i >= 1000000000) {     // >= 1,000,000,000    digits = i / 100000000;  //      100,000,000    i -= digits * 100000000;    PutTwoDigits(digits, buffer);    buffer += 2;  lt100_000_000:    digits = i / 1000000;  // 1,000,000    i -= digits * 1000000;    PutTwoDigits(digits, buffer);    buffer += 2;  lt1_000_000:    digits = i / 10000;  // 10,000    i -= digits * 10000;    PutTwoDigits(digits, buffer);    buffer += 2;  lt10_000:    digits = i / 100;    i -= digits * 100;    PutTwoDigits(digits, buffer);    buffer += 2; lt100:    digits = i;    PutTwoDigits(digits, buffer);    buffer += 2;    *buffer = 0;    return buffer;  }  if (i < 100) {    digits = i;    if (i >= 10) goto lt100;    memcpy(buffer, one_ASCII_final_digits[i], 2);    return buffer + 1;  }  if (i < 10000) {  //    10,000    if (i >= 1000) goto lt10_000;    digits = i / 100;    i -= digits * 100;    *buffer++ = '0' + digits;    goto lt100;  }  if (i < 1000000) {  //    1,000,000    if (i >= 100000) goto lt1_000_000;    digits = i / 10000;  //    10,000    i -= digits * 10000;    *buffer++ = '0' + digits;    goto lt10_000;  }  if (i < 100000000) {  //    100,000,000    if (i >= 10000000) goto lt100_000_000;    digits = i / 1000000;  //   1,000,000    i -= digits * 1000000;    *buffer++ = '0' + digits;    goto lt1_000_000;  }  // we already know that i < 1,000,000,000  digits = i / 100000000;  //   100,000,000  i -= digits * 100000000;  *buffer++ = '0' + digits;  goto lt100_000_000;}


В реализации abseil, мы видим обе оптимизации, из описанных мною выше(LUT + избавление от цикла). Более того, хочу отметить, что в этом коде есть немного больше интересного, чем я описываю: эти нюансы больше относятся к архитектурным особенностям процессоров intel, работе branch predictor-а and so on.

Можно ли в принципе избавиться от использования LUT, а данные записывать в память не так часто? Да, можно: используя один трюк, который вы могли упустить из виду. Про этот трюк и пойдет дальнейший рассказ.

Память в компьютере бывает разная это вам и RAM и SSD, Кеш Память процессора, или защищенные буферы оного, анклавы e.t.c. Есть еще один очень интересный тип памяти регистровая, и она, к слову, очень быстрая. Если вам доводилось программировать GPU вы прекрасно понимаете о чем я. У каждого процессора свой набор регистров: их как правило ограниченное количество, они имеют свою битность и команды процессора, которые умеют оперировать содержимым оных. Именно регистровую память я и предлагаю использовать в качестве буфера для записи итоговой ASCII строки. Скажем, регистр емкостью 8 байт, мы можем использовать как строку из 8 ASCII символов. А если нам нужно 10 байт, а именно столько нам и нужно для корректного перевода целого, беззнакового 32-ух битного числа, мы просто возьмем два регистра. Получать значения по индексу в таком мини-буффере просто: воспользуемся логическими операциями процессора, а именно AND по битовой маске вида 0x..FF В нашем случае, получать / записывать значения по индексу не требуется. Нам нужно записывать значения в буфер так, чтобы затем, при сохранении в Оперативную память, нам не пришлось переворачивать полученную строку. Получили цифру числа, записали в конец буфера и пошли дальше, к началу. Тут нам на помощь, приходят битовые(циклические) сдвиги. Записав последнюю цифру числа и выполнив такой сдвиг мы как бы переворачиваем строку, и движемся от конца к началу. Для начала я приведу код описанной реализации, а затем, отвечу на вопросы, которые у вас точно появятся, после того, как вы увидите ее:

Код, конечно, на Хабре немного поехал...
char* uint32_to_string_0        (                uint32_t n,                char    *out_str        ) noexcept{    if ( n < 10u )    {        const uint64_t in = n + 0x30ull;                memcpy( out_str, &in, 8u );                return out_str + 1u;    }    const uint32_t b = n / 10u;    if ( n < 100u )    {        const uint64_t in = 256ull * n - 2559ull * b + 0x3030ull;        memcpy( out_str, &in, 8u );        return out_str + 2u;    }    const uint32_t c = n / 100u;    if ( n < 1000u )    {        const uint64_t in = 65536ull * n - 2559ull * ( 256ull * b + c ) + 0x303030ull;        memcpy( out_str, &in, 8u );        return out_str + 3u;    }    const uint32_t d = n / 1000u;    if ( n < 10000u )    {        const uint64_t in = 16777216ull * n - 2559ull * ( 256ull * ( 256ull * b + c ) + d ) + 0x30303030ull;        memcpy( out_str, &in, 8u );        return out_str + 4u;    }    const uint32_t e = n / 10000u;    if ( n < 100000u )    {        const uint64_t in = ( ( 16777216ull * n - 2559ull * ( 256ull * ( 256ull * b + c ) + d ) - 10 * e ) << 0x08ull ) + e + 0x3030303030ull;        memcpy( out_str, &in, 8u );        return out_str + 5u;    }    const uint32_t f = n / 100000u;    if ( n < 1000000u )    {        const uint64_t in = ( ( 16777216ull * n - 2559ull * ( 256ull * ( 256ull * b + c ) + d ) - 10 * e ) << 0x10ull ) +                            ( ( 256ull      * e - 2559ull * f ) + 0x303030303030ull );        memcpy( out_str, &in, 8u );        return out_str + 6u;    }    const uint32_t g = n / 1000000u;    if ( n < 10000000u )    {        const uint64_t in = ( ( 16777216ull * n - 2559ull * ( 256ull * ( 256ull * b + c ) + d ) - 10 * e ) << 0x18ull ) +                            ( ( 65536ull    * e - 2559ull * ( 256ull * f + g ) + 0x30303030303030ull ) );        memcpy( out_str, &in, 8u );        return out_str + 7u;    }    const uint32_t h = n / 10000000u;    if ( n < 100000000u )    {        const uint64_t in = ( ( 16777216ull * n - 2559ull * ( 256ull * ( 256ull * b + c ) + d ) - 10 * e ) << 0x20ull ) +                            ( ( 16777216ull * e - 2559ull * ( 256ull * ( 256ull * f + g ) + h ) ) + 0x3030303030303030ull );        memcpy( out_str, &in, 8u );        return out_str + 8u;    }    const uint32_t x = n / 100000000u;    const uint64_t r_0 = ( ( 16777216ull * n - 2559ull * ( 256ull * ( 256ull * b + c ) + d ) - 10 * e ) << 0x20ull ) +                         ( 16777216ull * e - 2559ull * ( 256ull * ( 256ull * f + g ) + h ) - 10 * x );    if ( 9u < x )    {        const uint64_t in_1 = ( ( x % 10u ) << 8ull ) + x / 10u + 0x3030ull;         memcpy( out_str, &in_1, 8u );        const uint64_t in_2 = ( ( r_0 + 0x3030303030303030ull ) );        memcpy( out_str + 2u, &in_2, 8u );        return out_str + 9u;    }    else    {        const uint64_t in_1 = x   + 0x30u;        memcpy( out_str, &in_1, 8u );        const uint64_t in_2 = r_0 + 0x3030303030303030ull;        memcpy( out_str + 1u, &in_2, 8u );        return out_str + 10u;    }}


Где сдивги? Где нахождение последней цифры? А эти коэффициенты, это что, магические числа, да? Обо всем по порядку. Сдвиги, это вам не только сдвиги это еще и операции умножения на степень двойки. Нахождение последнй цифры числа операция взятия остатка на 10. Компиляторы легко заменяют ее на более дешевую операцию умножения, получая тождественные результаты. Я писал про этот трюк тут(но смысл статьи в том, что можно сделать лучше, и компиляторы этого не делают). Собственно, сдвиги и коэффициенты умножения я объединил вместе. А вот эта вся сумма, а точнее формула для нахождения итоговой строки это сумма регистров, каждый из которых содержит одну ASCII цифру итоговой строки в нужной позиции. Другими словами, мы имеем дело с выражением следующего вида(переводим четырехзначное число в строку):

$( a b * 10 ) * 2^{24} + ( b - c * 10 ) * 2^{16} + ( c - d * 10) * 2^8 + d$


Здесь, a, b, c, d результаты целочисленного деления на 1, 10, 100, 1000 переводимого в строку числа.

Выражение в скобочках вычисление цифры числа. Итоговая сумма сама строка. Вру, к ней же еще нужно добавить 0x30 в каждом байтике: эту операцию мы можем сделать единожды, просто прибавив 8-ми байтовое 0x30...0x30. А что по бенчмаркам, Ваня? Вот бенчмарк(позаимствовал у abseil), а вот результаты на AMD Rayzen 7 3700x(BM_FastIntToBuffer_1 моя реализация, BM_FastIntToBuffer_2 abseil):
-----------------------------------------------------------------------------------Benchmark                                         Time             CPU   Iterations-----------------------------------------------------------------------------------BM_FastIntToBuffer_1<int32_t>/0               0.456 ns        0.456 ns   1000000000BM_FastIntToBuffer_1<int32_t>/0               0.455 ns        0.455 ns   1000000000BM_FastIntToBuffer_1<int32_t>/1                3.40 ns         3.40 ns    228868292BM_FastIntToBuffer_1<int32_t>/8                3.79 ns         3.79 ns    194594644BM_FastIntToBuffer_1<int32_t>/64               3.99 ns         3.99 ns    176719588BM_FastIntToBuffer_1<int32_t>/512              4.01 ns         4.01 ns    174835143BM_FastIntToBuffer_1<int32_t>/4096             4.01 ns         4.01 ns    174360728BM_FastIntToBuffer_1<int32_t>/32768            4.01 ns         4.01 ns    174990974BM_FastIntToBuffer_1<int64_t>/0               0.456 ns        0.456 ns   1000000000BM_FastIntToBuffer_1<int64_t>/0               0.455 ns        0.455 ns   1000000000BM_FastIntToBuffer_1<int64_t>/1                3.38 ns         3.38 ns    230695679BM_FastIntToBuffer_1<int64_t>/8                3.78 ns         3.78 ns    195911364BM_FastIntToBuffer_1<int64_t>/64               4.00 ns         4.00 ns    176595354BM_FastIntToBuffer_1<int64_t>/512              4.02 ns         4.01 ns    174571183BM_FastIntToBuffer_1<int64_t>/4096             4.02 ns         4.02 ns    173962202BM_FastIntToBuffer_1<int64_t>/32768            4.01 ns         4.01 ns    174352211BM_FastIntToBuffer_1<int64_t>/262144           4.01 ns         4.01 ns    174021355BM_FastIntToBuffer_1<int64_t>/2097152          4.02 ns         4.02 ns    174066641BM_FastIntToBuffer_1<int64_t>/16777216         4.04 ns         4.04 ns    172736260BM_FastIntToBuffer_1<int64_t>/134217728        3.91 ns         3.91 ns    178819304BM_FastIntToBuffer_1<int64_t>/1073741824       3.26 ns         3.26 ns    214479196BM_FastIntToBuffer_2<int32_t>/0                4.78 ns         4.78 ns    146524823BM_FastIntToBuffer_2<int32_t>/0                4.78 ns         4.78 ns    146525099BM_FastIntToBuffer_2<int32_t>/1                7.02 ns         7.02 ns    102428060BM_FastIntToBuffer_2<int32_t>/8                6.66 ns         6.66 ns     99453560BM_FastIntToBuffer_2<int32_t>/64               6.45 ns         6.45 ns    104754274BM_FastIntToBuffer_2<int32_t>/512              6.44 ns         6.44 ns    107937513BM_FastIntToBuffer_2<int32_t>/4096             6.44 ns         6.44 ns    108483775BM_FastIntToBuffer_2<int32_t>/32768            6.44 ns         6.44 ns    108500271BM_FastIntToBuffer_2<int64_t>/0                4.78 ns         4.78 ns    146476646BM_FastIntToBuffer_2<int64_t>/0                4.78 ns         4.78 ns    146442788BM_FastIntToBuffer_2<int64_t>/1                7.05 ns         7.05 ns     99472455BM_FastIntToBuffer_2<int64_t>/8                6.67 ns         6.67 ns     99141872BM_FastIntToBuffer_2<int64_t>/64               6.45 ns         6.45 ns    104705700BM_FastIntToBuffer_2<int64_t>/512              6.44 ns         6.44 ns    107980669BM_FastIntToBuffer_2<int64_t>/4096             6.44 ns         6.44 ns    108475908BM_FastIntToBuffer_2<int64_t>/32768            6.44 ns         6.44 ns    108548677BM_FastIntToBuffer_2<int64_t>/262144           6.44 ns         6.44 ns    108581914BM_FastIntToBuffer_2<int64_t>/2097152          6.44 ns         6.43 ns    108572736BM_FastIntToBuffer_2<int64_t>/16777216         6.43 ns         6.43 ns    108629837BM_FastIntToBuffer_2<int64_t>/134217728        6.37 ns         6.37 ns    109567105BM_FastIntToBuffer_2<int64_t>/1073741824       5.97 ns         5.97 ns    116885892

Более того, я проводил тесты и на других процессорах результаты сопоставимы. В частности, тестировал и на Ryzen 9 3900x, и на ноутбуке с Intel i7 haswell 4th gen, а также запускал на мобильном девайсе(armv8):
benchmark:            12 ns    BM_FastIntToBuffer_1<int32_t>/0benchmark:             8 ns    BM_FastIntToBuffer_1<int32_t>/0benchmark:            28 ns    BM_FastIntToBuffer_1<int32_t>/1benchmark:            30 ns    BM_FastIntToBuffer_1<int32_t>/8benchmark:            34 ns    BM_FastIntToBuffer_1<int32_t>/64benchmark:            36 ns    BM_FastIntToBuffer_1<int32_t>/512benchmark:            36 ns    BM_FastIntToBuffer_1<int32_t>/4096benchmark:            36 ns    BM_FastIntToBuffer_1<int32_t>/32768benchmark:             9 ns    BM_FastIntToBuffer_1<int64_t>/0benchmark:             9 ns    BM_FastIntToBuffer_1<int64_t>/0benchmark:            28 ns    BM_FastIntToBuffer_1<int64_t>/1benchmark:            31 ns    BM_FastIntToBuffer_1<int64_t>/8benchmark:            34 ns    BM_FastIntToBuffer_1<int64_t>/64benchmark:            36 ns    BM_FastIntToBuffer_1<int64_t>/512benchmark:            37 ns    BM_FastIntToBuffer_1<int64_t>/4096benchmark:            37 ns    BM_FastIntToBuffer_1<int64_t>/32768benchmark:            37 ns    BM_FastIntToBuffer_1<int64_t>/262144benchmark:            37 ns    BM_FastIntToBuffer_1<int64_t>/2097152benchmark:            37 ns    BM_FastIntToBuffer_1<int64_t>/16777216benchmark:            37 ns    BM_FastIntToBuffer_1<int64_t>/134217728benchmark:            32 ns    BM_FastIntToBuffer_1<int64_t>/1073741824benchmark:            15 ns    BM_FastIntToBuffer_2<int32_t>/0benchmark:            15 ns    BM_FastIntToBuffer_2<int32_t>/0benchmark:            51 ns    BM_FastIntToBuffer_2<int32_t>/1benchmark:            55 ns    BM_FastIntToBuffer_2<int32_t>/8benchmark:            59 ns    BM_FastIntToBuffer_2<int32_t>/64benchmark:            57 ns    BM_FastIntToBuffer_2<int32_t>/512benchmark:            56 ns    BM_FastIntToBuffer_2<int32_t>/4096benchmark:            56 ns    BM_FastIntToBuffer_2<int32_t>/32768benchmark:            14 ns    BM_FastIntToBuffer_2<int64_t>/0benchmark:            14 ns    BM_FastIntToBuffer_2<int64_t>/0benchmark:            51 ns    BM_FastIntToBuffer_2<int64_t>/1benchmark:            55 ns    BM_FastIntToBuffer_2<int64_t>/8benchmark:            58 ns    BM_FastIntToBuffer_2<int64_t>/64benchmark:            56 ns    BM_FastIntToBuffer_2<int64_t>/512benchmark:            56 ns    BM_FastIntToBuffer_2<int64_t>/4096benchmark:            56 ns    BM_FastIntToBuffer_2<int64_t>/32768benchmark:            56 ns    BM_FastIntToBuffer_2<int64_t>/262144benchmark:            56 ns    BM_FastIntToBuffer_2<int64_t>/2097152benchmark:            56 ns    BM_FastIntToBuffer_2<int64_t>/16777216benchmark:            55 ns    BM_FastIntToBuffer_2<int64_t>/134217728benchmark:            46 ns    BM_FastIntToBuffer_2<int64_t>/1073741824


Важно отметить, что сам алгоритм не старается выполнить все умножения, деления, остатки за раз а чередует их пачками, дабы максимально нагрузить конвейер процессора. Сначала мы грузим вычислительные блоки, а затем, грузим блоки Записи / Чтения, Логических операций, затем, снова делаем вычислительные операции, тем самым, не давая Бэкенду и Фронтенду (да, современные процессоры обладают такими штуками) простаивать, а цепочка зависимостей получается не такая и большая. К памяти мы обращаемся максимум дважды(хотя префетчер скорее всего поместит в кеш именно две кеш линии, даже если нам нужна только одна): когда записываем результат. LUT отсутствует, а вот if-ов хватает, тут уж нам остается верить в наш branch predictor.

Упомянутый выше danlark прогонял бенчмарк на серверном железе, и результаты получилсь близкими, то есть такого разрыва в 1.5 раза как выше не наблюдалось.

Смысл этой статьи не столько в переводе чисел в строки, сколько в напоминании о том, что со строками можно работать как с числами. Да, мы получаем / можем получить зависимость порядка байт, но, возможно, полученный результат будет настолько хорош, что на эти моменты и неудобства можно будет закрыть глаза. Я использовал этот прием в разных задачах, и могу сказать, что плюсы у него точно есть: реален случай, когда подобный трюк, позволял компилятору сделать довольно сложную векторизацию кода, например.

Многое из описанного здесь, писалось еще два месяца назад, и по этой причине, результаты разных бенчмарков были утеряны, тем не менее, кое-что удалось сохранить, этим я и поделился выше. Также, отмечу, что C++17 to_chars дает неплохие результаты, но не такие хорошие как у libfmt, abseil. Ниже я оставляю ссылку на код Бенчмарка, и буду очень признателен, если вы запустите его у себя, а результатами поделитесь в комментариях. Очень хочется посмотреть на Малинку, Ардуинку, Андроид(посовременнее) или Apple, RISC и пр.

А вот и код google бенчмарка:
https://github.com/ov3rdr1v337/decima/blob/main/mybenchmark.cc
Подробнее..

Перевод Почему античитерское ПО блокирует инструменты разгона?

17.04.2021 20:18:06 | Автор: admin

Кто из нас не пользовался читами в играх? Whosyourdaddy, thereisnospoon, hesoyam помните? Но обращали ли вы внимание, почему, когда игрок пытается разогнать процессор или изменить настройки ПО, срабатывают некоторые программы против читеров вплоть до блокировки? В этой статье, которая будет полезна для читателей, не обладающих глубокими техническими знаниями в области использования ПО для читеров, против читеров, драйверов и того, что с ними связано, попробуем разобраться почему инструменты мониторинга/разгона блокируются античитерским ПО.


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

Из соображений удобства конечные пользователи могут переработать код под свою ответственность. Непродуманные действия могут привести к повреждению системы.

В нашем случае код для переработки берётся с таких сайтов, как kernelmode.info, OSR Online, и других. Особую обеспокоенность вызывают используемые таким программным обеспечением драйверы. Если бы я захотел причинить вред большому количеству людей (отличной мишенью для моей атаки могли бы стать геймеры и компьютерные энтузиасты), я бы в первую очередь использовал драйверы, входящие в состав некоторых программных инструментов, о которых расскажу далее. В статье я пишу только о некоторых драйверах, на самом деле их гораздо больше кодонезависимыми десятки, если не сотни. Драйверы, о которых пойдёт речь, использовались сообществом читеров ранее или используются сейчас. Попытаемся понять, зачем вообще в такое программное обеспечение включаются драйверы.

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

Зачем нужны драйверы?

За последние 510 лет в связи с развитием индустрии профессионального гейминга и повышением технических требований к запуску определённых игр всё большую популярность приобретают инструменты мониторинга оборудования и повышения тактовой частоты процессора. Такие инструменты опрашивают различные компоненты системы, такие как GPU, CPU, датчики температуры и прочее, однако обычному пользователю получить эту информацию не так просто.

Например, чтобы отправить запрос на цифровой датчик температуры для получения данных о температуре процессора, приложение должно выполнить чтение из моделезависимого регистра процессора. Доступ к таким регистрам процессора и внутренним механизмам чтения/записи возможен только с более высоким уровнем привилегий, например ring0 (на этом уровне работают драйверы). Моделезависимый регистр процессора (MSR) это тип регистра, представляющий собой часть набора команд x86. Как следует из названия регистра, на процессорах одной модели имеются одни регистры, на процессорах другой модели другие, что делает их моделезависимыми. Такие регистры используются в первую очередь для хранения специальной информации о платформе и особенностях процессора; они также могут использоваться для мониторинга показателей производительности или значений тепловых датчиков.

Intel приняла решение включить в набор инструкций x86 две инструкции, позволяющие привилегированному ПО (операционной или другой системе) считывать или записывать данные в MSR. Инструкции rdmsr и wrmsr позволяют привилегированной программе-агенту запрашивать или изменять состояние одного из таких регистров. Для процессоров Intel и AMD имеется обширный перечень доступных MSR, которые можно найти в соответствующих SDM/APM. Тут важно отметить, что большая часть информации в таких моделезависимых регистрах не должна меняться никакими задачами не важно, привилегированные они или нет. Но даже при написании драйверов устройств необходимость в этом возникает крайне редко.

Многие драйверы, создаваемые с целью программного мониторинга оборудования, позволяют задаче без привилегий (если под привилегиями понимать привилегии администратора) считывать/записывать произвольные MSR.

Как это работает? В драйверах реализуется особый коммуникационный режим, при котором они могут читать привилегированные данные из непривилегированного приложения. Повторяю (и это важно), что большинство драйверов ПО для мониторинга оборудования/повышения тактовой частоты процессора, поставляемых в составе клиентского приложения, имеют гораздо больше функциональных возможностей (не всегда, впрочем, нужных), к которым можно получить доступ через такой коммуникационный протокол.

Клиентское приложение, например десктопное приложение CPUZ, использует функцию WinAPI под названием DeviceIoControl. Говоря простым языком, CPUZ вызывает функцию DeviceIoControl с помощью известного разработчикам управляющего кода ввода/вывода, чтобы выполнить операцию чтения MSR, например, данных накристального цифрового датчика температуры.

Вы скажете: ну и что? В чём проблема? Проблема в том, что эти драйверы реализуют дополнительную функциональность за рамками своих задач и делают это через тот же самый интерфейс речь идёт, например, о записи в MSR или в физическую память.

Но, опять скажете вы, если коды известны только разработчикам, в чём же проблема? Плодотворным начинанием будет реверс-инжинеринг: всё, что нужно сделать злоумышленнику, получить копию драйвера, загрузить её в любой дизассемблер, скажем, в IDA Pro, и проанализировать обработчик IOCTL.

Ниже представлен код IOCTL в драйвере CPUZ, используемый для отправки двух байтов с двух различных портов ввода/вывода, 0xB2 (широковещательный SMI) и 0x84 (выходной порт 4). Вот это уже становится интересно, так как SMI можно заставить использовать порт 0xB2, позволяющий войти в режим управления системой. Не хочу утверждать, что с этой функцией можно натворить дел, просто отмечаю интересную особенность. Порт SMI используется в первую очередь для отладки.

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

Недокументированный драйвер Intel

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

Примечание: под непривилегированным приложением понимается приложение, выполняемое с низким уровнем привилегий, ring-3; между тем, чтобы выполнить запрос DeviceIoControl, требуются права администратора.

Помимо прочего, драйвер предоставляет непосредственный доступ к порту ввода-вывода для записи, а эта операция должна быть доступна только привилегированным приложениям. Доступом к записи вполне можно злоупотребить во вред конечному пользователю. Вредоносная программа-агент может использовать драйвер, чтобы запустить отказ в обслуживании (denial-of-service) посредством записи в порт ввода-вывода, такая запись может использоваться для аппаратного сброса процессора.

В диагностическом инструменте Intel такие операции имеют определённый смысл. Однако драйвер подписан, входит в состав официально поставляемого инструмента, и, если он попадёт в нечистоплотные руки, его можно использовать для причинения вреда в нашем случае игровым приложениям. Возможность чтения и записи в физическую память означает, что злоумышленник может получить доступ к памяти игры в обход традиционных методов доступа, например, без получения доступа к процессу и без использования Windows API для чтения виртуальной памяти. Злоумышленнику, конечно, придётся постараться, но разве когда-нибудь такая мелочь останавливала мотивированного человека?

Мне всё равно, я не пользуюсь этим диагностическим инструментом, но как быть другим пользователям? Рассмотрим ещё два инструмента, которые задействуют уязвимые драйверы.

HWMonitor

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

На скриншоте ниже показан другой метод чтения части физической памяти через функцию MmMapIoSpace. Эта функция часто используется злоумышленниками под видом доверенного инструмента для мониторинга оборудования. А как обстоят дела с записью в моделезависимые регистры процессора? Этот инструмент не предполагает запись ни в какие MSR, тем не менее, правильно переработанный код позволяет записывать данные в любой моделезависимый регистр процессора. Ниже приводятся два примера различных блоков IOCTL в HWMonitor.

Отметим, что используемый HWMonitor драйвер это тот же самый драйвер, который использует CPUZ! ПО против читерства, естественно, может просто запретить запуск HWMonitor, но у злоумышленника есть выход он может с таким же успехом воспользоваться драйвером из CPUZ.

Эта проблема уже значительно серьёзнее, поскольку, как было сказано выше, моделезависимые регистры процессора предназначены для чтения/записи только системным программным обеспечением.

Возможность доступа к таким регистрам через любой непроверенный интерфейс дает злоумышленникам возможность изменять системные данные, к которым у них ни в коем случае не должно быть доступа. Через эту уязвимость злоумышленники могут обходить защитные механизмы, устанавливаемые третьими сторонами, например ПО против читеров. Такое ПО может фиксировать обратные вызовы, например ExCbSeImageVerificationDriverInfo, что позволяет драйверу получать информацию о загруженном драйвере. При помощи доверенного драйвера злоумышленникам удаётся скрывать свои действия. Античитерское ПО логирует/отмечает/делает дамп довольно большого количество подписанных пользователями драйверов, но всё же считает доверенными некоторые драйверы из состава WHQL или продуктов Intel. К слову, античитерское ПО само использует операцию обратного вызова, чтобы запретить загрузку драйверов, например упакованного драйвера для CPUZ (иногда античитерское ПО не запрещает загрузку драйвера, а просто фиксирует факт его наличия, даже если имя драйвера было изменено).

MSI Afterburner

Теперь вам должно быть понятно, почему загрузка многих таких драйверов блокируется античитерским ПО. Про MSI Afterburner лучше всего почитать в exploit-db. С ним проблемы те же, что и с вышеописанными драйверами, и для сохранения целостности системы и игровых приложений загрузку этого ПО разумно будет запретить.

Справедливости ради следует сказать, что описанные уязвимости уже устранены, но я всего лишь привёл пример того, как неожиданно могут повернуться многие, казалось бы, полезные инструменты. Несмотря на то, что MSI отреагировала соответствующим образом и обновила Afterburner, были обновлены не все инструменты OC/мониторинга.

Заключение

Теперь вам должно быть понятно, почему некоторое программное обеспечение против читерства пытается запретить (не важно, успешно или нет) загрузку такого рода драйверов. Многие выступают против вышеописанной тактики, но, в конце концов программы против читеров должны следовать своему предназначению сохранять целостность игры и повышать качество игрового процесса.

Если вас не смущает, что во время игры не будет работать инструмент мониторинга оборудования, просто отключите его и играйте в своё удовольствие.

Читеры стали задействовать такие драйверы ещё в 20152016 годах, а возможно, и раньше; однако до этого времени код с демонстрацией на крупных читерских форумах не публиковался. Блокировать драйверы необходимо, чтобы запретить обход античитерского ПО через доверенный сторонний драйвер и защитить игру от хакеров, использующих такой метод атаки.

Я хорошо понимаю, что лишиться возможности пользоваться средствами мониторинга не очень приятная перспектива, но было бы несправедливо всю вину возлагать на античитерское ПО. Возможно, в сложившейся ситуации больше виноваты поставщики программного обеспечения с опасным кодом, подвергающим систему риску во время игры. Если бы злоумышленником был я, то, несомненно, в первую очередь задумался об использовании такого драйвера, чтобы скомпрометировать систему.

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

Как разработчики игр создают и внедряют читы в код игры, чтобы тестирование геймплея осуществлялось проще, так и у нас в SkillFactory мы внедряем новые элементы в программы, чтобы обучение было продуктивнее, а студенты получали актуальные знания. Если вам интересна сфера тестирования обратите внимание на наш курс QA-инженер на JAVA. Если же вы хотите создавать свои игровые миры у нас есть курс разработчик игр на Unity. Приходите, будет сложно, но занимательно.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

История портирования Reindexerа как покорить Эльбрус за 11 дней

15.06.2021 18:13:51 | Автор: admin

Всем привет! На связи Антон Баширов, разработчик из ИТ-кластера Ростелекома. Импортозамещение набирает обороты, а российский софт всё глубже проникает в нашу повседневную ИТ-шную сущность бытия. Процессоры Эльбрус и Байкал становятся более востребованными, комьюнити расширяется, но мысли о необходимости портировать весь наш любимый технологический стек на неизведанную архитектуру E2K звучат страшнее рассказов про горящий в пламени production-кластер.

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

Итак, гость в студии база данных Reindexer, разработка нашего ИТ-кластера.

Стоит сказать, почему выбор пал именно на Reindexer, а не другую БД. Во-первых, всеми любимый и известный Postgres уже есть в составе пакетов ОС Эльбрус. Переносить его нет необходимости. Во-вторых, наш гость уже прошел испытания на ARM, следовательно, пришло время ему покорить Эльбрус.

Стоит упомянуть, что Reindexer появился на свет как продуктвыбора между Elastic и Tarantool. Это определенно хочется попробовать завести на отечественном процессоре.

День 0. Знакомство с гостем

Несколько слов про нашего гостя:

Имя:NoSQL in-memory database Reindexer с функционалом полнотекстового поиска
Возраст:на момент испытаний версия БД была 3.1.2
Происхождение:Россия
Место рождения:пытливый ум разработчика Олега Герасимова@olegator99
Место жительства:https://github.com/Restream/reindexer
Место работы:Ростелеком
Строение:основная составляющая C++, имеются примеси из языка Ассемблера и немного CMake
Особенности:- Ассемблерные вставки;
- Много С++11 и C++14;
- Используются корутины.

Деятельность:- Хранение данных в памяти и на диске;
- Выполнение SQL запросов со скоростью 500K queries/sec для PK запросов;
- Выполнение запросов типа full-text search (почти как Elastic, только не тормозит);
- Работа в режиме server и embedded;
- Общение с крутыми парнями: Go, Java, Rust, .NET, Python, C/C++ и (да простит меня Хабр) PHP.

Заскучали? На этом нудная часть закончилась. В эфире рубрика Ээээксперименты!

День 1. А ты думал, в сказку попал?

Для начала попробуем нашего друга собрать из того, что есть.Идем на тестовый сервер Эльбрус 8C с ОС Эльбрус 6.0.1, клонируем туда репозиторий и запускаем CMake.

Хорошие новости, мы нашли компилятор! Новость действительно хорошая, ведь у Эльбруса свой компилятор LCC.

К счастью, создатели Эльбрус сделали LCC полностью совместимым с GCC и наши любимые нативные программки и сборщики смогут чувствовать себя хорошо без особых манипуляций. LCC уже сделал за вас нужные линки:gcc -> /opt/mcst/bin/lcc*.

Зачем Эльбрусу свой компилятор?

Дело в том, что компилятор у Эльбруса не такой, как все. Он выполняет много работы, связанной с анализом зависимостей и оптимизацией:

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

Ключевая идея архитектуры давайте попробуем выполнить несколько операций за один присест. Эльбрусы позволяют реализовать параллельные выполнение кода без применения многопоточности. Достигается это путем одновременного выполнения разных инструкций внутри одной широкой команды на разных устройствах процессора. Но для того, чтобы процессор понимал, что от него хотят и мог правильно распределить инструкции, ему нужно дать на вход оптимизированный поток больших команд. Именно в формировании этого потока и заключается назначение LCC компилировать код так, чтобы широкие команды содержали параллельно выполняемые инструкции с целью задействовать все устройства CPU. Подробнеетут.

Но вернемся к нашему гостю и тому, что не понравилось CMake.

В скриптах сборки, в CMakeLists.txt выполняется определение операционной системы и архитектуры процессора, на которой собирается Reindexer. Нужно это для того, чтобы подключать правильные исходники ассемблера, ведь как говорится Write once, run anywhere.

Разумеется, на момент написания скриптов никто не знал про процессоры Эльбрус, поэтому наш скрипт и упал. Исправляем.

Программисты люди ленивые, поэтому:

Попытка 2:

А что так можно было? На самом деле да для того, чтобы завелся CMake, этого было достаточно. Теперь ударим в бубен и запустимmake -j8:

Для тех, кто уже успел поработать с кроссплатформенным софтом, не новость, что С/С++ код с помощью макросов разделяют на платформозависимые секции.

Поэтому, в некоторые места кода Reindexer понадобилось добавить парочку новых условий для__E2K__и__LCC__:

После колдовства с макросами нас ждал монстр пострашнее:

Вот что бывает, когда игнорируешь messages у CMake.

Из-за того, что CMake не нашел подходящих исходников, компилятор не смог найти реализации двух функций:jump_fcontextиmake_fcontext. Про назначение этих функций я расскажу чуть позже, а пока что давайте успокоим нашего гостя и подсунем ему пару пустышек:

Этих операций оказалось достаточно, чтобы Reindexer собрался.

Поздравляю, у вас двойня!

Вот они, наши два долгожданных файла:

# file reindexer_serverreindexer_server: ELF 64-bit LSB executable, MCST Elbrus, version 1 (GNU/Linux
# file reindexer_toolreindexer_tool: ELF 64-bit LSB executable, MCST Elbrus, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux.so.2, for GNU/Linux 2.6.33, with debug_info, not stripped

Это можно считать успехом. Мы смогли собрать наш первый Эльбрус-бинарник. Даже два бинарника: reindexer_server и reindexer_tool.

Подведем итоги. Чтобы собрать Reindexer, мы:

  • Поправили CMake-скрипты;

  • Добавили несколько условий в макросы препроцессора;

  • Заглушили ASM функции.

День 3. Наш Гость, попав на Эльбрус, начал подавать признаки жизни

Первый запуск прошел успешно - сервер поднялся, и даже открылся web-ui.

Я привык не останавливаться на достигнутом и решил немного поработать над Reindexer.

Результат меня порадовал - Гость устал и прилег отдохнуть:

Такое поведение наблюдалось только на Эльбрус. На i5 все работало штатно.

Давайте разбираться. Вооружившись отладчиком (gdb кстати под E2K работает прекрасно) и CLion, мне удалось докопаться до истины.

Пару запросов к Reindexer смогли воспроизвести ошибку:

Падает в деструкторе. Интересный момент - упало на free (в данном случае free реализуется через jemalloc). Видимо, здесь идет высвобождение памяти по некорректному указателю. Ошибку эту я исправлял в два этапа:

  1. work around - дело в том, что QueryEntry лежит в объекте ExpressionTree, в самом классе примечательного я не нашел, поэтому копнул в сторону родителя. Оказалось, что до вызова деструктора был вызван вот такой копирующий конструктор, в котором есть интересный MakeDeepCopy(), реализованный с помощью библиотечкиmpark-variant.

    Подробнее про expression tree рассказываюттут.

    На часах было 2 часа ночи, я засиделся в офисе и разбираться в variant было уже лениво, поэтому, каюсь, я решил просто убрать вызов этого метода.

    Итог - оно заработало.

  2. TODO: Исправление тоже есть, но рассказ про него уже выходит за рамки данной статьи. Небольшой спойлер (код из mpark-variant с патчем под e2k):

inline constexpr DECLTYPE_AUTO visit(Visitor &&visitor, Vs &&... vs)#ifdef E2K //Fix for Elbrus    -> decltype(detail::visitation::variant::visit_value(lib::forward<Visitor>(visitor),                                                 lib::forward<Vs>(vs)...))    {     return detail::visitation::variant::visit_value(lib::forward<Visitor>(visitor),                                                 lib::forward<Vs>(vs)...);    }#else    DECLTYPE_AUTO_RETURN(        (detail::all(             lib::array<bool, sizeof...(Vs)>{{!vs.valueless_by_exception()...}})             ? (void)0             : throw_bad_variant_access()),        detail::visitation::variant::visit_value(lib::forward<Visitor>(visitor),                                                 lib::forward<Vs>(vs)...))#endif

День 5. Он ожил! Ну почти

Сервер у нас, действительно, работает, даже удалось получить первые результаты бенчмарков.

Но все это время я упорно игнорировал один компонент.

Помните, как мы убрали завязку на ASM и функцииmake_fcontext и jump_fcontext?

Так вот, ASM исходники в Reindexer нужны для реализации C++ корутин, а эти функции - ключевые для корутин из библиотеки boost/context.

Кто такая эта ваша Корутина?

Корутина это функция, которая может быть приостановлена и возобновлена в любой момент. Для реализации достаточно трех основных компонентов:
- функция
- механизм для ее паузы (suspend)
- механизм для возобновления ее с той точки, в которой была сделана пауза (resume).

Реализацию корутин можно увидеть в таких языках и библиотеках, как:

  • Libcoro(корутины на C/С++)

  • Koishi(тоже корутины, тоже на С/С++)

  • Boost(и это тоже корутины, тоже на С/С++, корутин много не бывает!)

  • Node-fibers(корутины для NodeJS на базе libcoro)

  • Tarantool(fibers на базе libcoro)

  • Kotlin(свои корутины, не на C++)

  • C++20

  • Goroutine

Для наглядности вот скрин теста корутин из Koishi:

Последовательность следующая:

  1. Создали корутину, выделили стек, задали функцию;

  2. Возобновили корутину - в этот момент вызвалась наша функция;

  3. Приостановили корутину с помощьюkoishi_yield здесь мы вернули управление в test1 сразу после строчкиkoishi_resume;

  4. Если мы еще раз сделаемkoishi_resumeна нашей корутине мы вернемся на строчку функции cofunc1 сразу после первого вызоваkoishi_yield.

Без корутин не будет работать reindexer_tool, но не можем же мы оставить Reindexer без такой важной конечности. Будем исправлять.

Начнем с анализа ситуации. Ожидаемо, что ASM Эльбруса бустовские корутины не поддерживают (пока что), а значит нам нужно реализовать их самим. Подробности и реализацию бустовских корутин можете посмотретьздесь, а сейчас наступило время скрещивать ужа с ежом.

Есть несколько вариантов реализовать корутины:

  • Вариант 1:написать ASM реализацию. Самый очевидный, но при этом самый сложный вариант. У нас есть в планах попробовать именно этот вариант, так как ASM корутины - самые быстрые.

  • Вариант 2: забить. Нет, это не наш путь.

  • Вариант 3: использовать библиотеку.

По счастливой случайности Koishi оказалось той самой библиотекой, поддерживающей реализацию корутин с помощью встроенных E2K функций:makecontext_e2k()иfreecontext_e2k().

Koishi, Koishi, что это вообще такое?

Если коротко это библиотека, предоставляющая удобный API по использованию корутин на C/C++ с несколькими вариантами реализации:

  • ucontext

  • ucontext_e2k (наша прелесть)

  • fcontext

  • win32fiber

  • ucontext_sjlj

  • emscripten

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

Для начала внедрим Koishi в организм Reindexer:

Заменим больной орган на здоровый:

Стараемся не повредить то, что уже работает:

Одним из backend-ов Koishi для корутин выступает fcontext (те же самые boost исходники, что в Reindexer). Последуем древней мудрости работает не трогай! и оставим как есть в случае, если у нас не E2K архитектура. Для Эльбруса мы будем использоватьucontext_e2k.c

И вот он, наш мутант с корутинами полностью здоров и функционален (и на amd64, и на E2K):

День 11. Проводим финальные испытания

Две половинки нашего Франкенштейна собраны вместе, даже сторонняя библиотека прижилась и отлично себя показала. Пришло время для финальных тестов.

Всего в Reindexer около 300 функциональных тестов, и мне не терпится запустить их все.

Тесты бежали, бежали и не добежали. Один из тестов вызвал Segmentation Fault.

Всему виной оказались вот эти строчки:

struct ConnectOpts {  /* тут тоже код, но он не такой интересный */  uint16_t options = 0;  int expectedClusterID = -1;};

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

Попробуем воспроизвести на изолированном более простом примере.

ASM, настало твое время

Внимание на скриншот:

Ошибка проявляется именно в том случае, когда отсутствуют не проинициализированные поля. Другими словами, если все ваши поля инициализируются при объявлении вы счастливчик, поймавший Segmentation Fault.

Баг заключается в том, что скомпилированное создание структуры выглядит как странныйaddd. Именно эта строчка и вызывает segfault. Кстати, если вместоbool anyField = falseнаписать простоbool anyField, то код совершенно другой и ошибки нет.

Семь бед, один ответ обновитесь!

Разгадка тайны оказалась проста. На момент работы с Reindexer последней версией компилятора LCC была v.1.25.16, однако в состав пакетов ОС Эльбрус по умолчанию входит 1.25.14, на котором я и запускал тесты. Добрые люди из сообщества подсказали нам, что уже в 15 версии данный баг был исправлен. Я решил обновиться на 16-ую и посмотреть, что будет.

Вот что нам принес новый компилятор LCC v.1.25.16:

C++ код такой же, однако ASM совсем другой, и что самое главное он работает! Последовательно (заранее прошу прощения за мой ломаный asm-русский переводчик):

  1. gestp - выделяем стек и кладем его в %dr3

  2. setwd - задаем текущее окно в регистровом файле

  3. setbn - задаем подвижную базу регистров в текущем окне регистрового файла

  4. addd - кладем "дно" стека (стек размером 0x20) в %dr2

  5. addd - выделяем на стеке нашу структуру

  6. ldb - достаем из констант наш false и кладем его в %r5

  7. stb - кладем наш false из регистра %r5 в наше поле anyField1

  8. addd - подготовили локальный указатель на структуру для вызова метода

  9. addd - передаем указатель в базовый регистр для передачи при вызове anyMethod (в anyMethod мы достаем указатель из регистра %dr0)

  10. disp - подготавливаем регистр перехода на anyMethod

  11. call - вызываем anyMethod

Дальше все просто пересобрать, перезапустить, обрадоваться.

Тесты прошли успешно, и теперь у нас есть полностью рабочий и протестированный Reindexer на Эльбрусе.

На этом все ?

На самом деле нет. Сейчас мы смогли:

  • собрать Reindexer Server

  • запустить Reindexer Server

  • собрать Reindexer Tool

  • запустить Reindexer Tool

  • переписать кусочек Reindexer Tool

  • уронить Reindexer и поднять его

  • добиться 100% проходимости Reindexer тестов

Мы научились:

  • собирать C++ софт на Эльбрус

  • переписывать C++ софт под особенности и отличия Эльбрусов

  • разбираться в ASM E2K

  • превозмогать трудности

  • разбираться в C++ корутинах даже на Эльбрусе

Что в планах:

  • корутины на ASM E2K (может быть даже сравнить fcontext ASM на i5 и ucontext/ASM на E2K)

  • оптимизировать Reindexer под архитектуру Эльбрус

  • перенести еще несколько интересных приложений на Эльбрус

  • дополнить базу библиотек и пакетов Эльбрус

  • организовать E2K инфраструктуру и песочницу

Что же можно сказать про Эльбрус со стороны простого разработчика?

Эльбрус это ваш друг. Он уже умеет в 80% современного софта, работать с ним можно и достаточно комфортно. Его архитектура - действительно произведение искусства, идея одноядерного параллелизма достойна уважения и несет в себе большой потенциал. Вопрос лишь в том, получится ли его раскрыть.

На сегодня это всё, что хотелось бы вам рассказать, но я еще вернусь с новыми интересными подробностями про портирование и оптимизацию, и да прибудет с вами сила Эльбруса!

Подробнее..

Из песочницы Как оптимизировать блок проверок case

16.08.2020 16:17:46 | Автор: admin
Почему в delphi блок case работает медленно и как это исправить.

Делюсь способами оптимизации и хаками.

Почему case плох


Даже начинающие delphi-программисты знают как выглядит блок case. Однако не все знают, что для нашего процессора, он выглядит как множество if блоков.

Вот что видит программист:

procedure case_test(Index:Integer);begin  case Index of    0: writeln('Hello');    1: writeln('Habr!');  end;end;

А вот что видит процессор:

procedure case_test(Index:Integer);begin  Index:=0;  if Index = 0 then     writeln('Hello')  else      if Index = 1 then     writeln('Habr!');end;

К сожалению никакой магии за словом case не оказалось. Более того, слишком активное использование case может замедлить выполнение кода. Например если у Вас не 2 варианта проверок, а 50, 250 или ещё больше. Худшим решением для вас будет блок case.

Чем заменить case


Решение у этой проблемы есть. Само строение блока case подсказывает нам, что наши варианты должны быть достаточно прибраны, чтобы поместиться в перечисляемом типе данных например: Integer, Word, Byte, Enum или Char.

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

const   Data:Array[0..1] of String = ('Hello', 'Habr!');procedure case_test(Index:Integer);begin    writeln(Data[Index]);end;

Это работает когда в действиях внутри блока case меняется только один параметр. Но что делать если параметров несколько?

Чем заменить сложный case


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

type  TMyTextWord = record    Text:String;    NeedLinebreak:Boolean;  end;const  Data:Array[0..2] of TMyTextWord = (     (Text:'Hello'; NeedLinebreak:False),     (Text:' '; NeedLinebreak:False),     (Text:'Habr!'; NeedLinebreak:True)   );procedure case_test(Index:Integer);var  MyTextWord:TMyTextWord;begin    MyTextWord:=Data[Index];  write(MyTextWord.Text);  if MyTextWord.NeedLinebreak then writeln;end;

Здесь мы заменили блок case, который мог выглядеть вот так

procedure case_test(Index:Integer);begin  case Index of    0: write('Hello');    1: write(' ');    2:     begin      write('Habr!');       writeln;       end;  end;end;

Таким образом мы сводим количество действий для выполнения любого из случаев в блоке case до минимума, одинакового для всех случаев. Хотя здесь не сильно видна разница т.к. один набор из 3-ех действий заменился другим. Но подумайте, что выполнится быстрее: 50 раз проверить, является ли переменная одним из чисел? или Получить по индексу из массива 1 параметр из 50 возможных?. Ответ очевиден.

И так мы пришли к тому что case не далеко ушёл от известного нам if.
А раз мы научились оптимизировать case почему бы не пойти дальше?

Чем заменить if


Допустим у нас case не использует настроек, которые можно было бы записать в обычный массив. Например такой case:

class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;  Data: Pointer);var  I:Integer;  PRec:Pointer;begin  PRec:=@Data;  for I:=0 to Length(Fields)-1 do  begin    case Fields[I].Kind of      tkUString,      tkWideString:  PString(PRec)^:=PString(Fields[I].Data)^;      tkInteger:     PInteger(PRec)^:=PInteger(Fields[I].Data)^;      tkInt64:       PInt64(PRec)^:=PInt64(Fields[I].Data)^;      tkFloat:       PDouble(PRec)^:=PDouble(Fields[I].Data)^;      tkEnumeration: PWord(PRec)^:=PWord(Fields[I].Data)^;    end;    IncPtr(PRec,Fields[I].Size);  end;end;

Но перед главным решением я сделаю отступление, чтобы описать ход своих мыслей.

Допустим у нас есть некая процедура содержащая несколько блоков кода.

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

Мы знаем заведомо простое решение этой проблемы: перечислить в массиве процедуры, вот так

procedure SetString(A,B:Pointer); inline;procedure SetInt(A,B:Pointer); inline;procedure SetInt64(A,B:Pointer); inline;procedure SetDouble(A,B:Pointer); inline;procedure SetBool(A,B:Pointer); inline;implementationprocedure SetString(A,B:Pointer); begin PString(A)^:=PString(B)^; end;procedure SetInt(A,B:Pointer); begin PInteger(A)^:=PInteger(B)^; end;procedure SetInt64(A,B:Pointer); begin PInt64(A)^:=PInt64(B)^; end;procedure SetDouble(A,B:Pointer); begin PDouble(A)^:=PDouble(B)^; end;procedure SetBool(A,B:Pointer); begin PBoolean(A)^:=PBoolean(B)^; end;type  TTypeHandlerProc = reference to procedure (A,B:Pointer);var  TypeHandlers:Array[TTypeKind] of TTypeHandlerProc;class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;  Data: Pointer);var  I:Integer;  PRec:Pointer;begin  PRec:=@Data;  for I:=0 to Length(Fields)-1 do  begin    TypeHandlers[Fields[I].Kind](PRec, Fields[I].Data);    IncPtr(PRec,Fields[I].Size);  end;end;initialization  TypeHandlers[tkUString]:=SetString;  TypeHandlers[tkWideString]:=SetString;  TypeHandlers[tkInteger]:=SetInt;  TypeHandlers[tkInt64]:=SetInt64;  TypeHandlers[tkFloat]:=SetDouble;  TypeHandlers[tkEnumeration]:=SetBool;end.

Здесь мы создали массив с inline процедурами, т.е. вместо вызова этих процедур компилятор подставит их код в строку, из которой мы к ним обращаемся. А в остальном всё как раньше, обращаемся к массиву по индексу операции, и выполняем операцию указывая в круглых скобках параметры. Обработчик представлен в сокращенном варианте, т.к. обработка всех действий с типом TTypeKind выглядит громоздкой.

Как, наверное многие уже догадались, это не последний вариант решения проблемы.
Допустим, что мы не хотим выходить за рамки одной функции, и создавать вокруг неё ворох вспомогательных процедур, тип данных для них, и инициализировать массив с процедурами в конструкторе класса или при инициализации модуля.

Если вы уверенный в себе программист и хотите максимальной оптимизации, но не хотите засорять модуль кучей примочек. Я представляю вам решение прямиком из тёмной стороны кодинга. Все кодеры вокруг говорят, что использовать goto не безопасно, что сам оператор устарел и в 90% случаях существуют решения без goto. Говорят что использование ассемблерных вставок доступно только злым хакерам. Но что будет, если мы пустимся во все тяжкие?

Я давно мечтал о таком способе переключения между блоками кода, чтобы хранить в массиве тип label, для осуществления прыжка goto. Таким образом я бы мог перемещаться между кусками одной процедуры при этом использовать для этого не case, а индекс по массиву адресов прыжка. Но возможно ли такое решение? Оказалось что да.

К сожалению в generic методах и классах нельзя использовать ассемблерные вставки, по этому решение пришлось переместить в метод другого объекта, хотя на самом решении это не отразилось. Решение представлено для x32 мода.

procedure TTestClass.Test(Fields: TArray<TField>; Data: Pointer);label  LS,LI,LI64,LF,LE,FIN;var  I:Integer;  PRec:Pointer;  ADR:Cardinal;  Types:Array[TTypeKind] of Cardinal;begin  FillChar(Types,Length(Types)*4,#0);  asm    lea EDX, [EAX].Types    mov [EAX-$4C+tkUString*4], offset LS    mov [EAX-$4C+tkWideString*4], offset LS    mov [EAX-$4C+tkInteger*4], offset LI    mov [EAX-$4C+tkInt64*4], offset LI64    mov [EAX-$4C+tkFloat*4], offset LF    mov [EAX-$4C+tkEnumeration*4], offset LE  end;  PRec:=Data;  for I:=0 to Length(Fields)-1 do  begin    ADR:=Types[Fields[I].Kind];    asm jmp ADR end;    LS:   PString(PRec)^:=PString(Fields[I].Data)^;   goto FIN;    LI:   PInteger(PRec)^:=PInteger(Fields[I].Data)^; goto FIN;    LI64: PInt64(PRec)^:=PInt64(Fields[I].Data)^;     goto FIN;    LF:   PDouble(PRec)^:=PDouble(Fields[I].Data)^;   goto FIN;    LE:   PByte(PRec)^:=PByte(Fields[I].Data)^;       goto FIN;    FIN:      IncPtr(PRec,Fields[I].Size);  end;end;

Трюк этого способа заключается в том, что компилятор delphi сам хранит адреса всех label и в ассемблерных вставках дает к ним доступ. Решение нашлось неожиданно просто[1]. Когда я искал как считать регистр EIP, в котором хранится адрес текущей исполняемой команды. Оказалось, что регистр считать нельзя, а вот адрес label'а как раз можно.

Ну а дальше всё просто, на ассемблере заполняем массив адресами нужных нам label'ов.
В цикле по массиву, берется каждый элемент. Тип элемента = индекс адреса его обработчика. Берем по индексу адрес обработчика, прыгаем на адрес, профит.

    ADR:=Types[Fields[I].Kind];    asm jmp ADR end;

Остались чисто формальности: после каждого блока ставим прыжок вконец цикла goto FIN;, чтобы не попадать на следующие label блоки.

Константа $4C это количество байт до начала блока с памятью массива, чтобы её вычислить можете записать в массив заполненный нулями mov [EAX], 1 и посмотреть в дебагере какая ячейка приняла это значение, количество ячеек от начала до неё * 4 и будет ваша константа.

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

References:

1. Хак с адресом EIP через label
Подробнее..
Категории: Оптимизация , Delphi , Case , If , Asm

Assembler Editor Plus Установка

17.02.2021 20:19:33 | Автор: admin

Продолжение цикла статей.

Предыдущая статья: Редактор ассемблера для ARM микроконтроллеров для компилятора gnu as. Старт

Итак, вы все таки решились скачать и попробовать, что для этого нужно сделать:

Кто не верит никому и предпочитает проверять что ставить на свой компьютер, то вам дорога на Github, устанавливайте Lazarus и необходимые компоненты, компилируйте, изучайте. Думаю для этой категории больше особо писать ничего не надо, там спецы сами все знают.

Для тех же кому нужен лайт вариант уже есть варианты:

  1. скачать релиз с Github, вы получите архив с программой редактора, с вложенными файлами компилятора и файлами OpenOCD сервера

  1. скачать архив с программой с Яндекс.Диск, ссылку если потерялись можно так же найти

    - в группе ВК

  1. ну или кому то может быть проще скачать с того же Телеграмма (вот только узнал что например на/в Украине такие сервисы как Яндекс и ВК не доступны)

Выбирайте кому что больше нравится

во всех вышеуказанных случаях у вас должны быть установлены драйвера на ваш программатор, в редакторе сделана реализация для ST-Link (у меня китаец V2, c SWD)

если же вы не знаете где достать драйвера или хотите скачать полный комплект что сейчас собран, то

Вариант Easy - скачать с Яндекс.Диск полный пакет, с различными допами в виде драйверов, программы редактирования шрифтов, доками на некоторые микроконтроллеры и отладочные платы. (внимание размер 130 мб)

В лайт варианте вы получите в архиве папку AsmEditor, которую нужно распаковать куда нить на диск, в путь без пробелов, со следующим содержимым

Описание папок и файлов:

bin - папка программ компиляции, у меня это gnu as из пакета arm-none-eabi

inf - файлы настроек для микроконтроллеров, меню редактора и т.д.

openocd - сервер отладки

tmp - папка временных файлов редактора

AsmEdit.exe - запускаемый файл

asmedit.ini - базовые настройки редактора

new - да удалите его, затесался, и является лишним

Для Easy варианта, действия те же самые, скачать и распаковать куда вам удобно, но путь должен быть без пробелов, содержимое будет чуть больше

О папке AsmEdit сказано выше

В папке Add находятся:

  • в install: инсталляторы для ST-Link, и программа установки dll для J-Link (если кто использует именно его, не спешите с его установкой!)

  • в MCUDoc: различные справочные файлы, какие то книги скаченные с интернета, описания плат разработки, даташиты на некоторые MCU которые находятся в работе и т.д.

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

Запуск редактора

Ну экзешник один, так что запускаем

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

Нажатием кнопки "Задать" найдите и укажите файл ST-Link_CLI.exe на своем компьютере, на скриншоте настройки расположения файла по умолчанию при установке драйвера ST-Link из папки Add\Install\ST-Link в Easy варианте редактора

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

Настройки OpenOCD

Если кто внимательно читал, то сервер openOCD идет вместе с редактором (каталог openocd в папке редактора), так что вы можете использовать его, или же указать расположение уже установленного у вас сервера

  • Если желаете использовать сервер установленный с редактором, то можно выбрать версию для 32ух или 64ех битных систем

  • при использовании J-Link настройки нужно указать как это указано на скриншоте выше

  • при использовании ST-Link V2, нужно модифицировать настройки следующим образом

если поставить чек бокс "Использовать OpenOCD для записи прошивки в устройство", то прошивка устройства будет происходить так же силами OpenOCD, это было сделано для J-Link, но будет работать и с ST-Link, однако прошивка при помощи программы ST-Link_CLI будет происходить быстрее (см предыдущий шаг настройки), поэтому я рекомендую при наличии программатора ST-Link этот чекбокс не устанавливать

Настройки редактора

Старался их группировать по смыслу, получилось пока не очень, поэтому опишу немного

Настройки редактора настраивают основные параметры самого редактора, в принципе думаю в основном понятно их назначение, шрифт всегда используется моноширинный

Параметры визуализации текста в редакторе

В редактор добавлен простейший форматер текста, в режиме автоформатирования при вводе работает не идеально, но готовый текст форматирует нормально

Настройки компилятора осуществляются в разрезе проекта, поэтому о них расскажу позже

В меню Справка есть некоторые дополнительные инфобоксы

По идее будет дополнятся по мере расширения редактора.

Подробнее..

Assembler Editor Plus Добавление нового микроконтроллера

21.02.2021 12:18:25 | Автор: admin

Эта статья немного забегает вперед, и поясняет каким образом можно добавить настройки в редактор для других ARM микроконтроллеров

Перечень микроконтроллеров находится в файле inf\mculist.ini

Открыть его для редактирования можно из самого редактора

Если сопоставить то что написано с окно выбора микроконтроллера при создании проекта, то думаю понятно как строится дерево выбора микроконтроллеров

Таким образом, модификация дерева под нового производителя микроконтроллера думаю не вызовет у вас особых затруднений

И где-нибудь ниже описаний микроконтроллеров ST, добавим имя серии микроконтроллеров и имена конкретных микроконтроллеров, которые можно увидеть на сайте производителя

Сохранив файл и заново запустив создание проекта, при выборе микроконтроллера получим новое содержание диалогового окна дерева микроконтроллеров

Теперь нужно описать конкретный микроконтроллер из нашего списка, по запросу одного из участников проекта (Сергей привет!), будем описывать 1921ВК035

Сначала создадим файловую структуру для хранения настроек новых микроконтроллеров в каталоге inf/ редактора, у меня получилось так

Дополнительно нам понадобятся следующие файлы

  • Для OpenOCD нужны target файлы под выбранные микроконтроллера, их можно "утащить" из настроек OpenOCD сервера сред предлагаемых производителем/разработчиком микроконтроллера и используемых для отладочного сервера, эти файлы нужно скопировать в папку openocd\scripts\target

  • Так же для интерфейса отладчика нужен .svd файл микроконтроллера, по которому редактор будет показывать состояние регистров настройки микроконтроллера, эти файлы так же можно найти в ПО от производителя/разработчика микроконтроллера или достать из из другой среды разработки, эти файлы я помещаю в папке микроконтроллера

Теперь опять вернемся к нашему настроечному файлу списка микроконтроллеров mculist.ini и пропишем и разберем настройки микроконтроллера

  • type=config указатель на то что описывается микроконтроллер, если этой строчки нет, то содержимое секции ini файла будет рассматриваться как названия субкатегорий микроконтроллеров

  • file=inf\niiet\1921VKx\1921VK035\k1921vk035.ini - файл описания модулей микроконтроллера, пока мы его не создали, сделаем это ниже

  • openocd=openocd\scripts\target\k1921vk035.cfg расположение target файла для openOCD

  • deviceinfo=inf\NIIET\1921VKx\1921VK035\K1921VK035.svd описатель микроконтроллера для отладчика редактора

  • targetadr=0x00000000 - адрес для размещения прошивки микроконтроллера, это значение нужно искать в документации по микроконтроллеру

  • syntax=unified - формат написания команд ассемблера, это опция компилятора которая указывается в каждом .asm файле проекта

  • cpu=cortex-m4 указание на ядро микроконтроллера, так же смотрим в документации по микроконтроллеру, спрашиваем на форумах и так далее, эта опция нужна и для компилятора, и для редактора, так на основании этого значения выбираются правила подсветки команд ассемблера в редакторе

  • thumb=.thumb указание компилятору на размер используемых команд

  • fpu= - указание на сопроцессор для операций с плавающей точкой (эту информацию я еще не нашел, оставим не заполненным)

Теперь можно переходить к созданию файла описания модулей микроконтроллера

Общий шаблон выглядит следующим образом

Сначала идет имя секции с именем микроконтроллера [К1921ВК035] и дальше идут пары параметров:

  • textX - текст показываемый в дереве списка модулей

  • linkX - ссылка на поддерево

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

При необходимости запуска скрипта установки модуля в проект прописываются следующие параметры

  • textX - текст показываемый в дереве списка модулей

  • scriptX - указатель на файл скрипта модуля

Про то как сделать скрипт добавления модуля в проект будет написана дополнительная статья, а сейчас существует небольшое техническое видео с простыми примерами

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru