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

C++

Перевод Свод правил по работе с целыми числами в CC

09.04.2021 12:18:12 | Автор: admin

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

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

Типы данных


Базовые целочисленные типы


Целочисленные типы устанавливаются с помощью допустимой последовательности ключевых слов, взятых из набора {char, short, int, long, signed, unsigned}.

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

  • char: минимум 8 бит в ширину;
  • short: минимум 16 бит и при этом не меньше char;
  • int: минимум 16 бит и при этом не меньше short;
  • long: минимум 32 бит и при этом не меньше int;
  • long long: минимум 64 бит и при этом не меньше long.

Наличие знака


  • Стандартный сhar может иметь знак или быть беззнаковым, что зависит от реализации.
  • Стандартные short, int, long и long long идут со знаком. Беззнаковыми их можно сделать, добавив ключевое слово unsigned.
  • Числа со знаком можно кодировать в двоичном формате в виде дополнительного кода, обратного или как величину со знаком. Это определяется реализацией. Заметьте, что обратный код и величина со знаком имеют различные шаблоны битов для отрицательного нуля и положительного, в то время как дополнительный код имеет уникальный нуль.
  • Символьные литералы (в одинарных кавычках) имеют тип (signed) intв C, но (signed или unsigned) char в C++.

Дополнительные правила


  • sizeof(char) всегда равен 1, независимо от битовой ширины char.
  • Битовая ширина не обязательно должна отличаться. Например, допустимо использовать char, short и int, каждый шириной в 32 бита.
  • Битовая ширина должна быть кратна 2. Например, int может иметь ширину 36 бит.
  • Есть разные способы написания целочисленного типа. К примеру, в каждой следующей строке перечислен набор синонимов:
    • int, signed, signed int, int signed;
    • short, short int, short signed, short signed int;
    • unsigned long long, long unsigned int long, int long long unsigned.


Типы из стандартных библиотек


  • size_t (определен в stddef.h) является беззнаковым и содержит не менее 16 бит. При этом не гарантируется, что его ширина будет как минимум равна int.
  • ptrdiff_t (определен в stddef.h) является целочисленным типом со знаком. Вычитание двух указателей будет давать этот тип. При этом не стоит ожидать, что вычитание двух указателей даст int.
  • В stdint.h определена конкретная ширина типов: uint8_t, int8_t, 16, 32 и 64. Будьте внимательны к операциям, подразумевающим продвижение типов. Например, uint8_t + uint8_t даст int (со знаком и шириной не менее 16 бит), а не uint8_t, как можно было предположить.


Преобразования


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

Как происходит преобразование?

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

Говоря конкретнее:

  • Когда исходный тип расширяется до целевого типа с аналогичной знаковой характеристикой (например, signed char -> int или unsigned short -> unsigned long), каждое исходное значение после преобразования сохраняется.
  • Даже если исходный и целевой типы имеют разные диапазоны, все значения в их пересекающейся части будут сохранены. Например, int, содержащий значение в диапазоне [0, 255], будет без потерь преобразован в unsigned char.

В более точной форме эти правила звучат так:

  • При преобразовании в беззнаковый тип новое значение равняется старому значению по модулю 2целевая ширина в битах. Объяснение:
    • Если исходный тип беззнаковый и шире целевого, тогда старшие биты отбрасываются.
    • Если исходный тип имеет знак, тогда в процессе преобразования берется исходное значение, и из него/к нему вычитается/прибавляется 2целевая ширина в битах до тех пор, пока новое значение не впишется в диапазон целевого типа. Более того, если число со знаком представлено в дополнительном коде, то в процессе преобразования старшие биты отбрасываются, как и в случае с беззнаковыми числами.

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


Арифметика


Продвижение/преобразование


  • Унарный арифметический оператор применяется только к одному операнду. Примеры: -, ~.
  • Бинарный оператор применяется к двум операндам. Примеры: +, *, &. <<.
  • Если операнд имеет тип bool, char или short (как signed, так и unsigned), тогда он продвигается до int (signed), если int может содержать все значения исходного типа. В противном случае он продвигается до unsigned int. Процесс продвижения происходит без потерь. Примеры:
    • В реализации присутствуют 16-битный short и 24-битный int. Если переменные x и y имеют тип unsigned short, то операцияx & y продвигает оба операнда до signed int.
    • В реализации присутствуют 32-битный char и 32-битный int. Если переменные x и y имеют тип unsigned char, то операцияx y продвигает оба операнда до unsigned int.

  • В случае двоичных операторов оба операнда перед арифметической операцией неявно преобразуются в одинаковый общий тип. Ранги преобразования возрастают в следующем порядке: int, long, long long. Рангом общего типа считается старший ранг среди типов двух операндов. Если оба операнда являются signed/unsigned, то их общий тип будет иметь ту же характеристику. Если же операнд с беззнаковым типом имеет старший или равный ранг по отношению ко второму операнду, то их общий тип будет беззнаковым. В случае, когда тип операнда со знаком может представлять все значения другого типа операнда, общий тип будет иметь знак. В противном случае общий тип получается беззнаковым. Примеры:
    • (long) + (long) (long);
    • (unsigned int) * (int) (unsigned int);
    • (unsigned long) / (int) (unsigned long);
    • если int является 32-битным, а long 64-битным: (unsigned int) % (long) (long);
    • если int и long оба являются 32-битными: (unsigned int) % (long) (unsigned long).


Неопределенное поведение


Знаковое переполнение:

  • При выполнении арифметических операций над целочисленным типом переполнение считается неопределенным поведением (UB). Такое поведение может вызывать верные, несогласованные и/или неверные действия как сразу, так и в дальнейшем.
  • При выполнении арифметики над беззнаковым целым (после продвижений и преобразований) любое переполнение гарантирвоанно вызовет оборот значения. Например, UINT_MAX + 1 == 0.
  • Выполнение арифметики над беззнаковыми целыми фиксированного размера может привести к едва уловимым ошибкам. Например:
    • Пусть uint16_t = unsigned short, и int равен 32-битам. Тогда uint16_t x=0xFFFF, y=0xFFFF, z=x*y; x и y будут продвинуты до int, и x * y приведет к переполнению int, вызвав неопределенное поведение.
    • Пусть uint32_t = unsigned char, и int равен 33-битам. Тогда uint32_t x=0xFFFFFFFF, y=0xFFFFFFFF, z=x+y; x и y будут продвинуты до int, и x + y приведет к переполнению int, то есть неопределенному поведению.
    • Чтобы обеспечить безопасную арифметику с беззнаковыми целыми, нужно либо прибавить 0U, либо умножить на 1U в качестве пустой операции. Например: 0U + x + y или 1U * x * y. Это гарантирует, что операнды будут продвинуты как минимум до ранга int и при этом останутся без знаков.


Деление/остаток:

  • Деление на нуль и остаток с делителем нуля также относятся к неопределенному поведению.
  • Беззнаковое деление/остаток не имею других особых случаев.
  • Деление со знаком может вызывать переполнение, например INT_MIN / -1.
  • Остаток со знаком при отрицательных операндах может вызывать сложности, так как некоторые части являются однообразными, в то время как другие определяются реализацией.

Битовые сдвиги:

  • Неопределенным поведением считается битовый сдвиг (< < и >>)на размер, который либо отрицателен, либо равен или больше битовой ширины.
  • Левый сдвиг беззнакового операнда (после продвижения/преобразования) считается определенным правильно и отклонений в поведении не вызывает.
  • Левый сдвиг операнда со знаком, содержащего неотрицательное значение, в следствии которого 1 бит переходит в знаковый бит, является неопределенным поведением.
  • Левый сдвиг отрицательного значения относится к неопределенному поведению.
  • Правый сдвиг неотрицательного значения (в типе операнда без знака или со знаком) считается определенным правильно и отклонений в поведении не вызывает.
  • Правый сдвиг отрицательного значения определяется реализацией.

Счетчик цикла


Выбор типа


Предположим, что у нас есть массив, в котором нужно обработать каждый элемент последовательно. Длина массива хранится в переменной len типа T0. Как нужно объявить переменную счетчика цикла i типа T1?

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

uint8_t len = (...);for (uint8_t i = 0; i < len; i++) { ... }
  • Говоря обобщенно, переменная счетчика типа T1 будет работать верно, если диапазон T1 будет являться (не строго) надмножетсвом диапазона T0. Например, если len имеет тип uint16_t, тогда отсчет с использованием signed long (не менее 32 бит) сработает.
  • Говоря же более конкретно, счетчик цикла должен просто покрывать всю фактическую длину. Например, если len типа int гарантированно будет иметь значение в диапазоне [3,50] (обусловленное логикой приложения), тогда допустимо отсчитывать цикл, используя char без знака или со знаком (в котором однозначно можно представить диапазон [0,127]).
  • Нежелательно использовать переменную длины и переменную счетчика с разной знаковостью. В этом случае сравнение вызовет неявное сложное преобразование, сопровождаемое характерными для платформы проблемами. К примеру, не стоит писать такой код:

size_t len = (...);  // Unsignedfor (int i = 0; i < len; i++) { ... }

Отсчет вниз


Для циклов, ведущих отсчет вниз, более естественным будет использовать счетчик со знаком, потому что тогда можно написать:

for (int i = len - 1; i >= 0; i--) {    process(array[i]);}

При этом для беззнакового счетчика код будет таким:

for (unsigned int i = len; i > 0; i--) {    process(array[i - 1]);}

Примечание: сравнение i >= 0 имеет смысл только, когда i является числом со знаком, но всегда будет давать true, если оно будет беззнаковым. Поэтому, когда это выражение встречается в беззнаковом контексте, значит, автор кода скорее всего допустил ошибку в логике.

Заблуждения


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

  • char всегда равен 8 битам. int всегда равен 32 битам.
  • sizeof(T) представляет число из 8-битных байтов (октетов), необходимых для хранения переменной типа T. (Это утверждение ложно, потому что если, скажем, char равняется 32 битам, тогда sizeof(T) измеряется в 32-битных словах).
  • Можно использовать int в любой части программы и игнорировать более точные типы вроде size_t, uint32_t и т.д.
  • Знаковое переполнение гарантированно вызовет оборот значения. (например, INT_MAX + 1 == INT_MIN).
  • Символьные литералы равны их значениям в коде ASCII, например A == 65. (Согласно EBCDIC это утверждение ложно).
  • Преобразование указателя в int и обратно в указатель проихсодит без потерь.
  • Преобразование {указателя на один целочисленный тип} в {указатель на другой целочисленный тип} безопасно. Например, int *p (); long *q = (long*)p;. (см. каламбур типизации и строгий алиасинг).
  • Когда все операнд(ы) арифметического оператора (унарного или бинарного) имеют беззнаковые типы, арифметическая операция выполняется в беззнаковом режиме, никогда не вызывая неопределенного поведения, и в результате получается беззнаковый тип. Например: предположим, что uint8_t x; uint8_t y; uint32_t z;, тогда операция x + y должна дать тип вроде uint8_t, беззнаковый int, или другой разумный вариант, а +z по-прежнему будет uint32_t. (Это не так, потому что при продвижении типов предпочтение отдается типам со знаком).


Моя критика


  • Если вкратце, то знание и постоянное использование всех этих правил сильно нагружает мышление. Допущение же ошибки в их применении приводит к риску написания неверного или непортируемого кода. При этом такие ошибки могут как всплыть сразу, так и таиться в течение дней или даже долгих лет.
  • Сложности начинаются с битовой ширины базовых целочисленных типов, которая зависит от реализации. Например, int может иметь 16, 32, 64 бита или другое их количество. Всегда нужно выбирать тип с достаточным диапазоном. Но иногда использование слишком обширного типа (например, необычного 128-битного int) может вызвать сложности или даже внести уязвимости. Усугубляется это тем, что такие типы из стандартных библиотек, как size_t, не имеют связи с другими типами вроде беззнакового int или uint32_t; стандарт позволяет им быть шире или уже.
  • Правила преобразования совершенно безумны. Что еще хуже, практически везде допускаются неявные преобразования, существенно затрудняющие аудит человеком. Беззнаковые типы достаточно просты, но знаковые имеют очень много допустимых реализаций (например, обратный код, создание исключений). Типы с меньшим рангом, чем int, продвигаются автоматически, вызывая труднопонимаемое поведение с диапазонами и переполнение. Когда операнды отличаются знаковостью и рангами, они преобразуются в общий тип способом, который зависит от определяемой реализацией битовой ширины. Например, выполнение арифметики над двумя операндами, как минимум один из которых имеет беззнаковый тип, приведет к преобразованию их обоих либо в знаковый, либо в беззнаковый тип в зависимости от реализации.
  • Арифметические операции изобилуют неопределенным поведением: знаковое переполнение в add/sub/mul/div, деление на нуль, битовые сдвиги. Не сложно создать такие условия неопределенного поведения по случайности, но сложно вызвать их намеренно или обнаружить при выполнении, равно как выявить их причины. Необходима повышенная внимательность и усилия для проектирования и реализации арифметического кода, исключающего переполнение/UB. Стоит учитывать, что в последствии становится сложно отследить и исправить код, при написании которого не соблюдались принципы защиты от переполнения/UB.
  • Присутствие signed и unsigned версии каждого целочисленного типа удваивает количество доступных вариантов. Это создает дополнительную умственную нагрузку, которая не особо оправдывается, так как типы со знаком способны выполнять практически все те же функции, что и беззнаковые.
  • Ни в одном другом передовом языке программирования нет такого числа правил и подводных камней касательно целочисленных типов, как в С и C++. Например:
    • В Java целые числа ведут себя одинаково в любой среде. В этом языке определено конкретно 5 целочисленных типов (в отличие от C/C++, где их не менее 10). Они имеют фиксированную битовую ширину, практически все из них имеют знаки (кроме char), числа со знаком должны находится в дополнительном коде, неявные преобразования допускают только их варианты без потерь, а вся арифметика и преобразования определяются точно и не вызывают неоднозначного поведения. Целочисленные типы в Java поддерживают быстрое вычисление и эффективное упаковывание массивов в сравнении с языками вроде Python, где есть только bigint переменного размера.
    • Java в значительной степени опирается на 32-битный тип int, особенно для перебора массивов. Это означает, что этот язык не может эффективно работать на малопроизводительных 16-битных ЦПУ (часто используемых во встраиваемых микроконтроллерах), а также не может непосредственно работать с большими массивами в 64-битных системах. К сравнению, C/C++ позволяет писать код, эффективно работающий на 16, 32 и/или 64-битных ЦПУ, но при этом требует от программиста особой осторожности.
    • В Python есть всего один целочисленный тип, а именно signed bigint. В сравнении с C/C++ это сводит на нет все рассуждения на тему битовой ширины, знаковости и преобразований, так как во всем коде правит один тип. Тем не менее за это приходится платить медленной скоростью выполнения и несогласованным потреблением памяти.
    • В JavaScript вообще нет целочисленного типа. Вместо этого в нем все выражается через математику float64 (double в C/C++). Из-за этого битовая ширина и числовой диапазон оказываются фиксированными, числа всегда имеют знаки, преобразования отсутствуют, а переполнение считается нормальным.
    • Язык ассемблера для любой конкретной машинной архитектуры (x86, MIPS и т.д.) определяет набор целочисленных типов фиксированной ширины, арифметические операции и преобразования с редкими случаями неопределенного поведения или вообще без них.


Дополнительная информация (англ.)




Подробнее..

Перевод Печальная правда о пропуске копий в C

14.04.2021 16:05:26 | Автор: admin


Пропуск копий (copy elision) это оптимизация компилятора, которая, как и следует из имени, устраняет лишние операции копирования и перемещения. Она аналогична классической оптимизации размножения копий, но выполняется конкретно для объектов C++, которые могут иметь нестандартные конструкторы копирования и перемещения. В этой статьей я продемонстрирую пример, в котором очевидная ожидаемая от компилятора оптимизация на практике не происходит.

Ввод дополнительной переменной для разрыва строки


Предположим, что у нас есть длинный вызов функции, возвращающий объект, который нужно мгновенно передать другой функции так:

#include <string>#include <string_view>// Тип данных, который дорого копировать, непросто удалить и невозможно переместитьstruct Widget {  std::string s;};void consume(Widget w);Widget doSomeVeryComplicatedThingWithSeveralArguments(  int arg1, std::string_view arg2);void someFunction() {    consume(doSomeVeryComplicatedThingWithSeveralArguments(123, "hello"));}

Как видно из сгенерированного кода ассемблера, здесь все отлично:

someFunction():                      # @someFunction()        pushq   %rbx        subq    $32, %rsp        movq    %rsp, %rbx        movl    $5, %edx        movl    $.L.str, %ecx        movq    %rbx, %rdi        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        movq    %rbx, %rdi        callq   consume(Widget)        movq    (%rsp), %rdi        leaq    16(%rsp), %rax        cmpq    %rax, %rdi        je      .LBB0_2        callq   operator delete(void*).LBB0_2:        addq    $32, %rsp        popq    %rbx        retq.L.str:        .asciz  "hello"

Временный Widget, возвращаемый из doSomeVeryComplicatedThingWithSeveralArguments, создается в области стека, которую под него выделила someFunction. Затем, как объяснялось в статье о правилах передачи параметров (англ.), указатель на эту область стека передается напрямую для использования.

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

void someFunctionV2() {    auto complicatedThingResult =        doSomeVeryComplicatedThingWithSeveralArguments(123, "hello");    consume(complicatedThingResult);}

Естественно, все съезжает:

someFunctionV2():                    # @someFunctionV2()        pushq   %r15        pushq   %r14        pushq   %r12        pushq   %rbx        subq    $72, %rsp        leaq    40(%rsp), %rdi        movl    $5, %edx        movl    $.L.str, %ecx        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        leaq    24(%rsp), %r12        movq    %r12, 8(%rsp)        movq    40(%rsp), %r14        movq    48(%rsp), %rbx        movq    %r12, %r15        cmpq    $16, %rbx        jb      .LBB1_4        testq   %rbx, %rbx        js      .LBB1_13        movq    %rbx, %rdi        incq    %rdi        js      .LBB1_14        callq   operator new(unsigned long)        movq    %rax, %r15        movq    %rax, 8(%rsp)        movq    %rbx, 24(%rsp).LBB1_4:        testq   %rbx, %rbx        je      .LBB1_8        cmpq    $1, %rbx        jne     .LBB1_7        movb    (%r14), %al        movb    %al, (%r15)        jmp     .LBB1_8.LBB1_7:        movq    %r15, %rdi        movq    %r14, %rsi        movq    %rbx, %rdx        callq   memcpy.LBB1_8:        movq    %rbx, 16(%rsp)        movb    $0, (%r15,%rbx)        leaq    8(%rsp), %rdi        callq   consume(Widget)        movq    8(%rsp), %rdi        cmpq    %r12, %rdi        je      .LBB1_10        callq   operator delete(void*).LBB1_10:        movq    40(%rsp), %rdi        leaq    56(%rsp), %rax        cmpq    %rax, %rdi        je      .LBB1_12        callq   operator delete(void*).LBB1_12:        addq    $72, %rsp        popq    %rbx        popq    %r12        popq    %r14        popq    %r15        retq.LBB1_13:        movl    $.L.str.2, %edi        callq   std::__throw_length_error(char const*).LBB1_14:        callq   std::__throw_bad_alloc().L.str:        .asciz  "hello".L.str.2:        .asciz  "basic_string::_M_create"

Теперь берем наш идеальный Widget, complicatedThingResult, и копируем его в новый временный Widget, который будет передаваться в качестве первого аргумента. По завершении всех действий нужно будет удалить два Widget: complicatedThingResult и безымянный временный Widget, который мы передавали для использования. Вы можете ожидать, что компилятор оптимизирует someFunctionV2(), сделав ее подобной someFunction, но этого не произойдет.

Проблема, конечно же, в том, что мы забыли выполнить std::move complicatedThingResult:

void someFunctionV3() {    auto complicatedThingResult =        doSomeVeryComplicatedThingWithSeveralArguments(123, "hello");    consume(std::move(complicatedThingResult));}

И теперь сгенерированный код ассемблера выглядит в точности, как наш исходный пример. Постойте-качто?

someFunctionV3():                    # @someFunctionV3()        pushq   %r14        pushq   %rbx        subq    $72, %rsp        leaq    8(%rsp), %rdi        movl    $5, %edx        movl    $.L.str, %ecx        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        leaq    56(%rsp), %r14        movq    %r14, 40(%rsp)        movq    8(%rsp), %rax        leaq    24(%rsp), %rbx        cmpq    %rbx, %rax        je      .LBB1_1        movq    %rax, 40(%rsp)        movq    24(%rsp), %rax        movq    %rax, 56(%rsp)        jmp     .LBB1_3.LBB1_1:        movups  (%rax), %xmm0        movups  %xmm0, (%r14).LBB1_3:        movq    16(%rsp), %rax        movq    %rax, 48(%rsp)        movq    %rbx, 8(%rsp)        movq    $0, 16(%rsp)        movb    $0, 24(%rsp)        leaq    40(%rsp), %rdi        callq   consume(Widget)        movq    40(%rsp), %rdi        cmpq    %r14, %rdi        je      .LBB1_5        callq   operator delete(void*).LBB1_5:        movq    8(%rsp), %rdi        cmpq    %rbx, %rdi        je      .LBB1_7        callq   operator delete(void*).LBB1_7:        addq    $72, %rsp        popq    %rbx        popq    %r14        retq.L.str:        .asciz  "hello"

У нас по-прежнему есть два Widget, только временный передаваемый аргумент теперь перемещен конструктором. Первая версия someFunction все еще оказывается меньше и быстрее!

Что же здесь происходит?


Суть проблемы пропуска копий в том, что он допускается только в определенном списке случаев. (Говоря коротко, при RVO1 и инициализации из prvalue это происходит обязательно, при NRVO2 и в ряде случаев с исключениями и сопрограммами пропуск считается допустимым. Все.). На то есть философская причина: вы написали для класса конструктор копирования, который может делать все, и ожидаете, что, согласно правилам C++, он будет выполняться при каждом копировании этого класса. Если компиляторы будут непредсказуемым образом удалять копии, тем самым также удаляя пары конструкторов & деструкторов копирования/перемещения, то это может привести к нарушению кода.

Говоря конкретно, в приведенном списке допускающих пропуск копий ситуаций нет таких, которые бы соответствовали рассмотренным нами примерам. В этот список не включены такие случаи, как последнее использование переменной перед ее выходом из области или передача переменной в функцию по значению, когда других действий с ней не предпринималось, то есть очевидно, что данная операция безопасна. Возможно, в будущем такие ситуации будут учтены, но в C++20 и более ранних версиях этого точно нет.

1. RVO (return value optimization) оптимизация возвращаемого значения.
2. NRVO (named return value optimization) оптимизация именованного возвращаемого значения.

Подробнее..

Как мы верифицированный полетный контроллер для квадрокоптера написали. На Ada

30.03.2021 12:10:29 | Автор: admin

Однажды на новогодних каникулах, лениво листая интернет, бракоделы в нашем* R&D офисе заметили видео с испытаний прототипа роботакси. Комментатор отзывался восторженным тоном революция, как-никак. Здорово, да, но время сейчас такое кругом революции, и ИТ их возглавляет.

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

Перед глазами побежали флешбеки, где-то из глубин подсознания всплыла забытая уже информация о прошивках для Тойоты на миллионы тысяч строк Си и 2 тысячи глобальных переменных (Toyota: 81564 ошибки в коде).

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

Но ведь можно иначе? Можно, решили мы!

И решили это доказать. На Avito был куплен акробатический FPV-квадрик на базе STM32F405, для отладки - Discovery-плата для этого же контроллера, а дальше все как в тумане..

Так как же быть иначе?

После быстрого совещания возникли вот такие мысли:

  • нам нужен другой подход

  • язык и подход должны друг друга дополнять

  • академический подход не подойдет, нужны практические применения.

В качестве нового подхода решили, что лучше всего опираться на возможность возможность верификации ПО - до необходимого уровня, без злоупотреблений. Но для языка типа С доступных промышленных зрелых решений не существует, только прототипы [FC] и рекомендации.

При выборе языка мы поставили себе вот какими требования:

  • это должно быть что-то близкое к embedded

  • Нам нужен хороший богатый runtime с возможностями RTOS, но при этом брать и интегрировать RTOS не хочется

  • Он не должен заметно уступать в производительности тому, что используется сейчас.

Оказалось, что из практических инструментов в эти требования хорошо подходит один очень старый, незаслуженно забытый инструмент. Да, это Ada. А точнее, его модерновое, регулярно обновляемое ядро SPARK. В [SRM] описаны основные отличия SPARK от Ada, их не так много.

Что такое SPARK, будет ясно дальше, мы покажем, как именно оно было применено, почему Ада понравилась больше, чем С, как работает прувер, и почему мы при этом ничего не потеряли, а только приобрели. И почему мы не взяли Rust :)

Иной подход

Иной подход это верификация ПО. Обычно при этих словах люди начинают думать об абстрактных монадах, академический манускриптах, докладах на конференциях и пыльных трудах института системного программирования РАН, которые как будто бы от жизни отстают лет на 30, а то и 50. Но оказалось, что не все так плохо.

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

В случае с SPARK, верификация базово предоставляет нам гарантии:

  • отсутствия переполнения массивов и переменных

  • отсутствия выхода за границы в типах и диапазонах

  • отсутствия разыменования null-указателей

  • отсутствие выброса исключений.

  • гарантию неприменения инструментов, проверку которых выполнить нельзя.

  • гарантию выполнения всех инвариантов, которые мы опишем. А опишем мы много!

    Круто, да?

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

SPARK также учитывает ограничения на типы, которые описаны в Ada. В случае обычного исполнения ошибка несоответствия типов упадет в Runtime; SPARK же позволяет статически доказать, что ограничения на типы не могут быть нарушены никаким потоком исполнения.

Например:

Или другой пример:

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

Отдельный плюс SPARK в том, что система позволяет натягивать гарантии на программу поэтапно, то есть программа может быть частично верифицированной. То есть часть модулей можно объявить верифицируемыми, а часть - (пока) нет.

Сам SPARK делит верификацию на уровни: от "каменного" (Stone level) через "Бронзовый" и "Серебряный" уровни до "Золотого" (Gold) и "Платинового". Каждый из уровней усиливает гарантии:

Stone

Мы в принципе знаем, что есть SPARK

Bronze

Stone + верификация потоков исполнения и детерминизм/отсутствие неинициализированных переменных

Silver

Bronze + доказательное отсутствие runtime-ошибок

Gold

Silver + гарантии целостности - не-нарушения инвариантов локальных и глобальных состояний

Platinum

Gold + гарантия функциональной целостности

Мы остановились на уровне Gold, потому что наш квадрокоптер все-таки не Boing 777 MAX.

Как работает верификация в SPARK: прувер собирает описание контрактов и типов, на их основе генерирует правила и ограничения, и далее передает их в солвер (SMT - Z3), который проверяет выполнимость ограничений. Результат решения прувер привязывает к конкретным строкам, в которых возникает невыполнимость.

Более подробно можно почитать в [SUG]

Иной язык

Несмотря на то, что сейчас "рулят" си-подобные ECMA-языки, мы нормально отнеслись к тому, что от этого придется отказаться. Более того, кажется, что чем больше программа, тем больше вредит укорочение ключевых слов и конструкций. Что касается Rust, то он - субъективно - в отношении минимализма издалека сильно напоминает Perl, к сожалению.

И наоборот, по ощущениям, пространные, многобуквенные конструкции раздражают, когда разум летит вперед, но не мешают, когда во главу угла ставится надежность, понятность, и легкость чтения. В этом смысле Ada (а SPARK это подмножество Ada) вполне хорош. Теперь давайте посмотрим, что же язык Ada может дать embedded-разработчику.

Профили

Сам язык и стандартная библиотека позволяет определять и использовать так называемые "профили". Профиль это набор ограничений, выполнение которых контролирует компилятор. Например, можно определить ограничение "нельзя использовать динамическую память". Или "нельзя бросать исключения". Или "не более двух активных потоков". Или "нельзя использовать объекты синхронизации". Все это помогает контролировать целостность состояния программы, и ее валидность.

Мы используем профиль Ravenscar, специально для embedded-разработки. Он включает пару дюжин ограничений, которые делают вашу разработку для микроконтроллеров более удобной и верифицируемой: нельзя на ходу переназначать приоритеты задач-потоков, переключать обработчики прерываний, сложные объекты из stdlib-ы и такое.

Вот ограничения профиля Ravescar, для примера

Runtime

Когда в embedded необходимо создать более-менее сложное приложение, там всегда заводится RTOS, и ее выбор и интеграция это отдельная песня. В случае с Ada с этим больше нет сложностей - сама Ada включает в себя минимальную исполняемую среду с вытесняющим планировщиком потоков (в Ada это tasks, задачи), с интегрированными в сам язык средствами синхронизации (семафоры, рандеву, называются entries) и даже средствами избегания дедлоков и инверсии приоритетов. Это оказалось очень удобно для квадрокоптера, как станет понятно ниже.

Для embedded-разработчика доступны на выбор также разные рантаймы:

  • zero-footprint - с минимальными ограничениями и даже без многопоточности; зато минимальная программа не превышает пары килобайт, влезает даже в TO MSP430

  • small footprint - доступна большая часть функций Ada, но и требования побольше, несколько десятков килобайт RAM

  • full ravenscar - доступны все функции в рамках профиля Ravenscar/Extended Ravenscar

Вот пример описания пустой задачи

Хочется обратить внимание, что эти задачи - это по сути легковесные green threads, так как под ними нет никаких настоящих потоков не существует. Поэтому мы не страдаем от отсутствия корутин, ведь задачи не тяжелее них, зато встроены в язык.

Кроме этого, в Ada есть достаточно мощная stdlib для ядра STM32, включая полную реализацию рантайма. Возможно, и для вашей архитектуры она тоже есть.

Почему не rustRustRUST!

Когда мы говорим, про гарантии в языках программирования, сразу вспоминается Rust и его гарантии относительно указателей. Почему тут не он? Нам кажется, что Spark мощнее.

Ada не очень любит указатели - там они называются access types, и в большинстве случаев там они не нужны, но если нужны, то - в Spark также есть проверки владения, как в Rust. Если вы аллоцировали объект по указателю, то простое копирование указателя означает передачу владения (которую проконтролирует компилятор/верификатор), а передачу во временное владение (или доступ на чтение) верификатор также понимает и контролирует.

В общем, концепция владения объектом по указателю, и уровень доступа через этот указатель - есть не только в Rust, и его преимуществами можно пользоваться и в других инструментах, в частности, в Ada/SPARK. Подробно можно почитать в [UPS]

Вот пример кода с владением

Почему мы пишем, что в Ada/SPARK не нужны указатели? В отличие от Си, где базовым инструментом является указатель (хочешь ссылку - вот указатель, хочешь адрес в памяти - вот указатель, хочешь массив - вот указатель - ну вы поняли?), в Ada для всего этого есть строгий тип. Если не хватает встроенных операций, их допустимо переопределять (например, реализовать инлайновый автоинкремент), аналогично можно создать placement constructor, используя т.н. limited-типы - типы, которые компилятор запрещает копировать.

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

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

IDE

Для работы доступна вполне приятная и удобная IDE, но всегда можно использовать и VSCode с плагинами, и другие текстовые редакторы.

О производительности и надежности

Вполне валидным аргументом может быть вопрос с эффективностью ПО. Что касается эффективности, то в интернете доступно свежее исследование [EFF], из которого хочется привести табличку, показывающую, что старичок Ada еще огого:

Если говорить о надежности, то SPARK/Ada известен как один из языков с наименьшим количеством ошибок. В планируемом на 21 запуске кубсатов [LIC] полетное ПО планируется реализовывать на Ada, предыдущий спутник BasiLEO тоже на Ada был единственным среди 12, кому удалось приступить к планируемой миссии.

А теперь - о самом полетном контроллере

После такого вступления стало понятно, что написать полетный контроллер на таких могучих инструментах - совершенно просто :) И действительно, если разобраться, то ничего сложного.

Структурная схема управляющего ПО показана на рисунке

Как видно из рисунка, ПО состоит из двух частей:

  • Veriflight - собственно, верифицированный полетный контроллер с алгоритмами.

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

Так как тратить много времени не хотелось, то драйвер для USB в STM32 был взят прямо нативный и при помощи Interop был слинкован с оберткой на Ada.

Плата оказалась оснащена минимальным количеством периферийных устройств:

  • STM32F405 микроконтроллер на 168 МГц (192кб RAM, 1Mб flash)

  • трансивером S.BUS на USART1

  • 6-осевым гиро-акселерометром без магнитного компаса

  • токовыми усилителями PWM

  • USB-интерфейсом, PHY-часть которого реализована на самом микроконтроллере платы.

Полетный контроллер реализован по простой схеме и крутит 2 цикла:

  1. внешний

  2. внутренний

Внешний цикл это цикл опроса периферии (CMD task на рисунке) в ожидании команд с радиопередатчика. Если команды нет, он передает признаки сохраняем высоту, держим горизонт. Если команда с пульта есть, передаем ее - целевой угол наклона, целевую мощность на пропеллеры. Частота внешнего цикла 20 Гц.

Внутренний цикл - цикл опроса гиро-акселерометра и распределения мощности на двигатели. Цикл оборудован 3 PID-регуляторами, и математикой Махони для расчета текущего положения по сигналам с гироскопов. В расчетах внутри используем кватернионы, для генерации управляющего сигнала - углы Эйлера. Частота размыкания внутреннего цикла - 200 Гц. Да, Ада без проблем успевает диспетчеризировать с такой скоростью.

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

Внутренний цикл реализует опрос PID и стабилизацию аппарата:

  • Считали затребованные пилотом углы

  • Запросили у математики расчетные углы положения

  • Нашли расхождение между желаемыми и настоящими

  • Пересчитали текущее положение на основании сигналов с гиро-акселерометров

  • Зарядили PID-регуляторы на новую коррекцию, если пришли новые затребованные углы

  • Запросили у PID-пакетов текущие импульсы коррекции

  • На основании них, а также запрошенной пилотом мощности на двигатели, сформировали необходимое распределение скоростей вращения на двигателях

Забавно, что большинство опен-сорсных реализаций Махони (для Arduino и не только) - на Cи и Wiring оказались содержащими разнообразные баги. Это мешало системе заработать. После того, как было выпито пол-ящика лимонада и съедена корзина круассанов, алгоритм воссоздали с нуля по описанию из [MHN], и система тут же заработала.

Данная схема, как и любая упрощенная модель, испытывает сложности при приближениях параметров к предельным. Здесь это 90 по крену и тангажу - при их превышении для безопасности реализовано отключение двигателей (disarm).

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

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

Статья получилась неожиданно длинная, поэтому придется разбить ее на две части. Мы в первой части постарались познакомить вас с инструментами предков, которые на удивление неплохи: по крайней мере, мы сделали выводы, что дальнейшие проекты для эмбеддед хочется делать на Ada, а не на Си.

Итог на текущий момент

Квадрокоптер с прошивкой на Ada/SPARK прошел тесты на подпружиненном стенде и полетах в закрытом помещении, собираются логи, схема стабилизации работает в соответствии с ожиданиями, но в рамках ограничений на углы маневров, как описано выше.

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

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

Для себя мы сделали вывод, что для embedded будем стараться писать только на Ada.

Если вы также считаете, что современная robotics и automotive это слишком важные вещи, чтобы позволить себе переполнения буфера и разыменование нуля и ой программисты не написали тесты, пишите, комментируйте, присоединяйтесь: пора сделать ПО надёжнее, потому что оно вокруг нас везде.

Литература для дальнейшего изучения

[SUG] SPARK user guide https://docs.adacore.com/spark2014-docs/html/ug/index.html

[SRM] SPARK reference manual (https://docs.adacore.com/live/wave/spark2014/html/spark2014_rm/index.html)

[FC] Frama-C - платформа для модульного анализа кода С https://frama-c.com/

[UPS] https://blog.adacore.com/using-pointers-in-spark

[MHN] https://nitinjsanket.github.io/tutorials/attitudeest/mahony

[EFF] https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sleFinal.pdf

[LIC] https://en.wikipedia.org/wiki/Lunar_IceCube

Подробнее..

QGit, улучшения

28.03.2021 16:13:00 | Автор: admin

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

Немного истории

При переходе с Windows на Linux мне пришлось изучать не только новую операционную систему, но и новую систему контроля версий - GIT. Очень быстро я проникся идеей о том, что работать с GIT нужно из консоли и только визуализацию дерева коммитов оставить на откуп графическим утилитам. Отсмотрев несколько графических программ, я остановил свой выбор на QGit. Утилита показалась достаточно минималистичной и симпатичной в графическом плане. К тому же, она была написана на Qt, что давало мне потенциальную возможность заглянуть под капот QGit (я - разработчик ПО и последние лет десять создаю программы с использованием Qt Framework).

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

Первые опыты

Вхождение в чужой проект - достаточно трудоемкое занятие. Мне потребовалось почти две недели, чтобы составить представление о том, как QGit устроен изнутри. Потраченные усилия оказались не напрасны, проблему с отображением кириллических символов удалось локализовать и исправить. Изменения были приняты в основной репозиторий QGit. Далее последовали несколько небольших доработок по улучшению юзабилити интерфейса, они тоже были приняты мантейнером. А вот предложение добавить в проект поддержку стандарта C++11 было отклонено. Мантейнер пояснил, что на данный момент есть еще много разработчиков использующих компиляторы без поддержки C++11. На дворе был 2016 год... С этого момента все доработки выполнялись в моем форке. За следующие два года было выполнено более 30 изменений, среди них пара существенных:

  • переработана реализация внутреннего кэша, используемого для построения дерева коммитов;

  • значительное ускорение построения дерева коммитов для проектов с большой историей.

При этом, форк периодически синхронизировался с основным проектом.

Рубикон

Для меня QGit, без преувеличения, программа "на каждый день" :). Частенько приходится работать в выходные или заниматься проектами подобными этому. И несмотря на то, что QGit меня практически полностью устраивал, одной функции очень сильно не хватало. Речь идет о проверке орфографии при создании комментария для коммита.

Уже в 2018 году у меня было понимание, что проверку орфографии нужно было внедрять в QGit, вопрос состоял только в том, когда и какими средствами? Реализация нового функционала и фикс мелких ошибок это совсем не одно и тоже. Вдобавок, у меня была внутренняя дилемма: нужно ли сохранять исходную кодовую стилистику проекта или же использовать более агрессивный подход по модификации кода, привнося в него сторонние библиотеки и собственные наработки в виде зависимостей. Перспективы, что мою работу примут в основную ветку, были туманны. Теоретически это могло произойти, но пришлось бы вести долгую переписку, убеждая мантейнера в ценности вносимых изменений. С другой стороны, использование собственных наработок делало рабочий процесс более комфортным и экономило время. Последний фактор оказался решающим. Так я перешел психологический "Рубикон".

Механизм проверки орфографии

Процесс проверки орфографии можно разбить на два шага:

  • определение языка проверяемого слова;

  • собственно, сама проверка орфографии с использованием словаря для языка определенного на первом шаге.

Существует несколько добротно проработанных open-source решений для детектирования языка. Даже есть реализации под GPU. К сожалению, слово "добротное" так же подразумевает под собой "тяжелое" и этот факт мне сильно не нравился. Поясню в чем дело: одна из сильных сторон QGit - быстрый старт. Приложение запускается за 1-2 секунды, при этом дерево коммитов уже построено, можно работать. Это позволяет многократно открывать/закрывать QGit без потери комфорта использования. Долгая инициализация механизма проверки орфографии могла изменить это обстоятельство не в лучшую сторону, поэтому тяжелые решения были отброшены.

На момент старта работ, у меня был небольшой опыт использования библиотеки Sonnet. Её я и взял в качестве кандидата. Забегая вперед, скажу, что в проекте от Sonnet мало что осталось: пара функций по подчеркиванию слов красной волнистой линией и пара переработанных утилит по генерации языковых триграмм. Но, обо все по порядку. Строго говоря, Sonnet не обладает функционалом для проверки орфографии, а является лишь оберткой для специализированных библиотек, таких как aspell, hunspell. С другой стороны, специализированные библиотеки не умеют определять язык проверяемых слов, это как раз и делает для них Sonnet. У меня не было намерения использовать Sonnet "как есть", от этой библиотеки мне нужен был только механизм определения языка. Даже не так, не сам механизм, а принципы его функционирования, потому что к качеству работы механизма были вопросы.

Что же не так с Sonnet? Детектирующий механизм определяет язык не для отдельного слова, а целиком для строки (фразы). Вероятно, для текста большого объема такой подход является оправданным, но у него есть существенный недостаток: если в строке встречается слово из другого языка, оно всегда будет детектироваться как ошибочное независимо от того, правильно оно написано или нет. Комментарии к коммитам как раз являются случаем, когда русскоязычные и англоязычные слова могут использоваться в одной строке. Разбирая исходный код Sonnet, я наткнулся на флаги, которые, могли активировать режим детектирования по отдельным словам. Но на тот момент это было не важно, я уже знал, что определение языка по строке - не самая большая проблема. Дело в том, что подход используемый в Sonnet не отличается высокой точностью детектирования. Список языковых триграмм насчитывает всего 300 элементов (для каждого языка), что недостаточно для надежной работы механизма. При этом, тригаммы содержат пробельные символы (прямое следствие детектирования по строке), что еще больше ухудшает ситуацию. Мой опыт эксплуатации механизма показал, что даже 3000 триграмм (без пробельных символов) могут давать сбои при детектировании русского языка. Стабильный результат был достигнут только при 5000. Английскому языку достаточно всего 2000 триграмм.

Понимая, что 300 триграмм явно недостаточно для уверенной работы, разработчики Sonnet решили подстраховаться альтернативными механизмами. Второй уровень детектирования предполагает анализ юникод-символов строки/слов на принадлежность к различным языковым группам, далее делается предположение о языке (более подробно об этом методе рассказать не могу, так как просматривал его поверхностно). Для особо тяжелых случаев существует третий уровень. Он мне особенно понравился! Слова по очереди отправляются в механизм проверки орфографии (aspell, hunspell). Если механизм проверки орфографии возвращает "успех", то запоминается язык словаря. Далее по совокупности таких проверок делается вывод о языке. Спрашивается: "Зачем тогда в начале с триграммами морочиться было!?"

В QGit используется единственный механизм - детектирование по триграммам, но в более надежном варианте (5000 элементов, триграммы без пробельных символов). Язык детектируется для каждого слова. Проверка орфографии выполняется при помощи hunspell, сейчас это наиболее популярная библиотека. Закомментированные строки не проверяются.

Список изменений

Существенные

  • проверка орфографии при создании коммита;

  • ускорение построения дерева коммитов для проектов с большой историей;

  • улучшение поддержки юникода.

Несущественные

  • закрытие окна консоли при нажатии на "пробел" (кнопка "OK" получает фокус по умолчанию);

  • при создании подписи к коммиту нажатие на Ctrl+Enter эквивалентно клику по "OK";

  • Shift+! вызывает форму для основного коммита;

  • Shift+@ вызывает форму для amend-коммита;

  • QGit завершает работу по нажатию на 'Q';

  • сохраняется ширина столбцов в дереве коммитов;

  • список файлов виден в Init-коммите;

  • авто-перенос для длинных однострочных коммитов (отображение в несколько строк);

  • диалоговые окна с сообщениями об ошибках отображаются только когда консоль скрыта;

  • удалена панель статуса с формы консоли;

  • для дерева коммитов запрещен режим DragAndDrop;

  • для команды checkout изменена комбинация клавиш: Ctrl+Shift+C -> Ctrl+C;

  • добавлена возможность задавать размер иконок;

  • идентификатор нулевого коммита не выводится в интерфейс;

  • формат конфиг-файла изменен с INI на YAML;

  • добавлена возможность скрывать диалог подтверждения при создании коммита;

  • исправлен приоритет отображения закладок Log/Diff при обновлении дерева.

Сильно не существенные

  • система логирования заменена на ALog (нужно для системы YAML-конфигурирования).

Дистрибутивы

Код форка расположен тут. Собранный пакет под Ubuntu 20.04 можно взять здесь. Так же есть standalone-пакет под Ubuntu 18.04/20.04 содержащий Qt-компоненты и hunspell (устанавливается в /opt).

Минорное заключение

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

Подробнее..
Категории: Программирование , C++ , Qt , Git

Перевод Rust теперь и на платформе Android

10.04.2021 14:17:23 | Автор: admin

Корректность кода на платформе Android является наиважнейшим аспектом в контексте безопасности, стабильности и качества каждого релиза Android. По-прежнему сложнее всего вытравливаются ошибки, связанные с безопасностью памяти и попадающиеся в коде на С и C++. Google вкладывает огромные усилия и ресурсы в обнаружение, устранение багов такого рода, а также в уменьшение вреда от них, старается, чтобы багов в релизы Android проникало как можно меньше. Тем не менее, несмотря на все эти меры, ошибки, связанные с безопасностью памяти, остаются основным источником проблем со стабильностью. На их долю неизменно приходится ~70% наиболее серьезных уязвимостей Android.

Наряду с текущимиипланируемымимероприятиями по улучшению выявления багов, связанных с памятью, Google также наращивает усилия по их предотвращению. Языки, обеспечивающие безопасность памяти наиболее эффективные и выгодные средства для решения этой задачи. Теперь в рамках проекта Android Open Source Project (AOSP) наряду с языками Java и Kotlin, отличающимися безопасностью памяти, поддерживается и язык Rust, предназначенный для разработки операционной системы как таковой.

Системное программирование

Управляемые языки, в частности, Java и Kotlin, лучше всего подходят для разработки приложений под Android. Эти языки проектировались в расчете на удобство использования, портируемость и безопасность. Среда исполнения Android (ART)управляет памятью так, как указал разработчик. В операционной системе Android широко используется Java, что фактически защищает большие участки платформы Android от багов, связанных с памятью. К сожалению, на низких уровнях ОС Android Java и Kotlin бессильны.

На низких уровнях ОС нужны языки для системного программирования, такие как C, C++ и Rust. При проектировании этих языков приоритет отдавался контролируемости и предсказуемости. Они обеспечивают доступ к низкоуровневым ресурсам системы и железу. Они обходятся минимальными ресурсами, прогнозировать их производительность также сравнительно просто.

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

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

Пределы работы в песочнице

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

В Android это означает, что, если код написан на C/C++ и разбирает потенциально небезопасный ввод, то он должен содержаться в жестко ограниченной песочнице без привилегий. Тогда как следование правилу двух хорошо помогает снижать тяжесть и повышать доступность уязвимостей, связанных с безопасностью, оно сопряжено с некоторыми ограничениями. Работа в песочнице дорогое удовольствие; для ее обеспечения требуются новые процессы, которыесопряжены с дополнительными накладными расходами и провоцируют задержки, связанные с межпроцессной коммуникацией и дополнительным расходом памяти. Работа в песочнице не позволяет полностью исключить уязвимости в коде, а эффективность такой работы снижается при высокой плотности багов, позволяющей злоумышленникам сцеплять вместе множество уязвимостей сразу.

Язык, обеспечивающий безопасность памяти, такой, как Rust, помогает преодолеть эти ограничения двумя способами:

  1. Снижает плотность багов в нашем коде, тем самым повышая эффективность применяемой песочницы.

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

Что же насчет всего имеющегося C++?

Разумеется, если мы введем новый язык программирования, это никак не поможет нам с исправлением уже имеющихся багов в имеющемся коде на C/C++.

Вышеприведенный анализ возраста багов, связанных с безопасностью памяти (отсчитывается с момента их появления) позволяет судить, почему команда Android делает акцент на новых разработках, а не на переписывании зрелого кода на C/C++. Большинство багов возникает в новом или недавно измененном коде, причем, возраст около 50% багов составляет менее года.

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

Ограничения находимости багов

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

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

Каждый из вышеприведенных шагов обходится дорого, а если пропустить хотя бы один из них, то баг останется непропатчен у некоторых или даже у всех пользователей. Что касается сложных базах кода на C/C++, зачастую только считанные специалисты могут разработать и внедрить исправление, причем, даже при затрачивании существенных усилий на исправление багов,внесенные изменения бывают некорректными.

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

Предотвращение прежде всего

В Rust модернизируется ряд языковых аспектов, что позволяет улучшить корректность кода:

  • Безопасность памяти обеспечивается благодаря сочетанию проверок во время компиляции и во время выполнения.

  • Конкурентность данных предотвращает гонку данных. Учитывая, насколько легко при этом становится писать эффективный потокобезопасный код, Rust обрел слоганБезбоязненная Конкурентность.

  • Более выразительная система типов помогает предотвратить логические ошибки при программировании (напр., обертки нового типа, варианты перечислений с содержимым).

  • Ссылки и переменные по умолчанию являются неизменяемыми что помогает разработчику следовать безопасному принципу наименьших привилегий. Программист помечает ссылку или переменную как изменяемые, только если в самом деле намерен сделать их таковыми. Притом, что в C++ есть const, эта возможность обычно используется нечасто и несогласованно. Напротив, компилятор Rust помогает избегать случайных аннотаций об изменяемости, так как выдает предупреждения об изменяемых значениях, которые никогда не меняются.

  • Улучшенная обработка ошибок в стандартных библиотеках потенциально провальные вызовы обертываются в Result, и поэтому компилятор требует от пользователя проверять возможность провала даже для функций, не возвращающих необходимого значения. Это позволяет защититься от таких багов как уязвимостьRage Against the Cage, возникающая из-за необработанной ошибки. Обеспечивая легкое просачивание ошибок при помощи оператора ? и оптимизируя Result с расчетом на низкие издержки, Rust стимулирует пользователей писать все потенциально провальные функции в одном и том же стиле, благодаря чему все они получают ту же защиту.

  • Инициализация требует, чтобы все переменные инициализировались перед началом использования. Исторически сложилось, что неинициализированные уязвимости памяти были в Android причиной 3-5% уязвимостей, связанных с безопасностью. В Android 11, чтобы сгладить эту проблему, стала применятьсяавтоматическая инициализация памяти на C/C++. Однако, инициализация в ноль не всегда безопасна, особенно для таких штук, как возвращаемые значения, и в этой области может стать новым источником неправильной обработки ошибок. Rust требует, чтобы любая переменная перед использованием инициализировалась в полноценный член своего типа. Тем самым избегается проблема непреднамеренной инициализации небезопасного значения. Подобно Clang в C/C++, компилятор Rust знает о требовании инициализации и позволяет избежать потенциальных проблем производительности, связанных с двойной инициализацией.

  • Более безопасная обработка целых чисел Очистка во избежание включена в отладочных сборках Rust по умолчанию, что стимулирует программистов указывать wrapping_add, если действительно предполагается допустить переполнение при расчетах, или saturating_add если не предполагается. Очистка при переполнении в дальнейшем должна быть включена для всех сборок Android. Кроме того, при всех преобразованиях целочисленных типов применяются явные приведения: разработчик не может сделать случайного приведения в процессе вызова функции при присваивании значения переменной или при попытке выполнять арифметические операции с другими типами.

Что дальше

Введение нового языка на платформу Android серьезное предприятие. Существуют такие связки инструментов и зависимости, которые необходимо поддерживать, тестовая инфраструктура и оснащение, которые потребуется обновить. Также придется дополнительно обучить разработчиков. Это проект не на один год. Следите за обновлениями в блоге Google.

Подробнее..

Простое сложное программирование

15.04.2021 16:11:54 | Автор: admin


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

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

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

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

Выбор попугаев для измерения


Я не стал придумывать свои или вычислять эмпирические метрики программного кода, и в качестве попугая решил взять самую простую метрику SLOC (англ.Source Lines of Code) количество строк исходного текста компилятора, которая очень легко вычисляется.

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

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

Но для численной оценки сложности кода в рамках одного проекта, метрика SLOC подходит хорошо.

Методика подсчета SLOC


Изначально попробовал использовать простой bash скрипт с поиском по маске и подсчетом числа строк в файлах исходника через wc -l. Но через некоторое время стало понятно, что приходится изобретать очередной велосипед.
Ну вы поняли:



Поэтому решил взять уже готовый. После быстрого поиска остановился на утилите SLOCCount, которая умеет анализировать почти три десятка типов исходников.
Список типов файлов для автоматического анализа
1. Ada (.ada, .ads, .adb)
2. Assembly (.s, .S, .asm)
3. awk (.awk)
4. Bourne shell and variants (.sh)
5. C (.c)
6. C++ (.C, .cpp, .cxx, .cc)
7. C shell (.csh)
8. COBOL (.cob, .cbl) as of version 2.10
9. C# (.cs) as of version 2.11
10. Expect (.exp)
11. Fortran (.f)
12. Haskell (.hs) as of version 2.11
13. Java (.java)
14. lex/flex (.l)
15. LISP/Scheme (.el, .scm, .lsp, .jl)
16. Makefile (makefile) not normally shown.
17. Modula-3 (.m3, .i3) as of version 2.07
18. Objective-C (.m)
19. Pascal (.p, .pas)
20. Perl (.pl, .pm, .perl)
21. PHP (.php, .php[3456], .inc) as of version 2.05
22. Python (.py)
23. Ruby (.rb) as of version 2.09
24. sed (.sed)
25. SQL (.sql) not normally shown.
26. TCL (.tcl, .tk, .itk)
27. Yacc/Bison (.y)



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

Меня изначально интересовал объем исходников на С/С++ и может быть еще на Ассемблере, если таких файлов окажется достаточно для много. Но после начала работы очень обрадовался, что не стал изобретать велосипед, а взял готовую тулзу, т.к. она отдельно считала статистику исходных файлов синтаксического анализатора Yacc/Bison (.y), который и определяет фактическую сложность парсера (читай сложность синтаксиса языка программирования).

Старые исходники gcc брал с gcc.gnu.org/mirrors.html, но перед запуском анализатора удалили каталоги других компиляторов (ada, fortran, java и т.д.), чтобы они не попадали в итоговую статистику.

Результаты в попугаях.


Таблица




Объем кода синтаксического анализатора Yacc/Bison


Объем общей которой базы GCC (только для языков C и C++)

Выводы


К сожалению синтаксический анализатор Yacc/Bison использовался только до 3 версии, а после его использование свелось на нет. Поэтому оценить сложность синтаксиса С/С++ с помощью объема кода парсера можно лишь примерно до 1996-98 года, после чего его стали постепенно выпиливать, т.е. чуть менее, чем за десять лет. Но даже за этот период объем кодовой базы синтаксического анализатора вырос двукратно, что примерно соответствует по времени реализации стандарта C99.

Но даже если не учитывать код синтаксического анализатора, то объем общей кодовой базы так же коррелирует с внедрением новых стандартов C++: C99, С11 и C14.

На графике не видно выраженного пика для С+17 и следующих версий, но могу предположить, что при текущем объеме кода (более 4 миллионов строк только С и С++ кода), несколько тысяч строк, необходимых для поддержки синтаксических конструкций новых стандартов просто незаметно.

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


Фактически на примере проекта GCC можно видеть постоянный и неотвратимый рост сложности рабочих инструментов.

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

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

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

Вывод второй порог входа


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

И если для простых сайтов существуют и простые решения, то для тех, кто не ищет легких путей существует CMS Drupal, который примечателен тем, что имеет фантастически высокий порог входа для начала использования.


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

Итоговый вывод не утешительный


Если рассматривать увеличение сложности только самого ПО, то это одно дело. Вот к примеру
Статистика ядра Linux с вики
17 сентября 1991: Linux версии 0.01 (10 239 строк кода).
14 марта 1994: Linux версии 1.0.0 (176 250 строк кода).
Март 1995: Linux версии 1.2.0 (310 950 строк кода).
9 июня 1996: Linux версии 2.0.0 (777 956 строк кода).
25 января 1999: Linux версии 2.2.0, изначально довольно недоработанный (1 800 847 строк кода).
4 января 2001: Linux версии 2.4.0 (3 377 902 строки кода).
18 декабря 2003: Linux версии 2.6.0 (5 929 913 строк кода).
23 марта 2009: Linux версии 2.6.29, временный символ Linux тасманский дьявол Tuz (11 010 647 строк кода).
22 июля 2011: релиз Linux 3.0 (14,6 млн строк кода).
24 октября 2011: релиз Linux 3.1.
15 января 2012: релиз Linux 3.3 преодолел отметку в 15 млн строк кода.
23 февраля 2015: первый релиз-кандидат Linux 4.0 (более 19 млн строк кода).
7 января 2019: первый релиз-кандидат Linux 5.0 (более 26 млн строк кода).

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

Понятно, что чем дальше, тем проще уже не будет. Ведь область IT, это среда с самой высокой конкуренцией. И как уж тут не вспомнить Льюиса Кэррола, что его крылатое выражение
Нужно бежать со всех ног, чтобы только оставаться на месте, а чтобы куда-то попасть, надо бежать как минимум вдвое быстрее!
Ведь это относится не только к Алисе в стране чудес, но и к всем информационным технологиям в целом!
Подробнее..

CDD Cli Driven Development

03.04.2021 10:09:09 | Автор: admin

Все-таки самоизоляция не проходит бесследно. Сидишь себе дома, а в голову разные мысли приходят. Как, чем осчастливить человечество? И вот оно: CDD! (И еще PDD / SOLID / KISS / YAGNI / TDD / Bootstraping...)

1. CDD - Cli Driven Development - Новый подход

Немного истории

Как-то поручили мне сделать Cli в одном нашем embedded устройстве. Разумеется, C/C++ (пусть будет C++, раз ресурсов хватает). Конечно есть много Cli-фреймворков.

Но я сделал свой вариант.

Для Linux можно использовать <termios.h> и получать коды символов после установки свойств терминала:

signal(SIGINT, SIGINT_Handler); // Ctrl+Csignal(SIGTSTP, SIGTSTP_Handler); // Ctrl+Zint res_tcgetattr = tcgetattr(STDIN_FILENO, &terminal_state_prev);terminal_state_new = terminal_state_prev;terminal_state_new.c_lflag &= ~(ICANON | ECHO);int res_tcsetattr = tcsetattr(STDIN_FILENO, TCSANOW, &terminal_state_new);

Для Windows можно использовать <conio.h>.

Добавляем немного классов, делаем список команд, и добавляем команды по типу:

{ Cli_Command_Abstract_t *cmd = new Cli_Command_Abstract_t(Cli_Command_ID_help); cmd->Add(help_keyword); cmd->Help_Set("show this help, \"help full\" - show all available commands"); command_tree->Add(cmd);}

И все-бы ничего, пока команд 10-20. Ну пусть еще help / quit / debug cli (типа очень нужная команда - об этом позже). Интересно, что основной функционал уложился в 20 команд, а вот разные обвязки Управление SNMP / Syslog / NTP / Users / FTP / SSH / VLAN и у нас - 250 команд. Ух ты! Начинаются проблемы с монолитным приложением, и очень хочется разбить все на модули, желательно попроще и поменьше. И вот отсюда и начинается CDD - Cli Driven Development.

1.1 Использование Cli в различных типах приложений

Вообще, Cli, не смотря на GUI, используется во многих типах приложений: САПР, игры, базы данных, среды выполнения (Erlang, Lua и др.), IDE. Можно утверждать, что включение консоли могло бы сделать многие приложения более удобными (например, можно представить Paint с командной строкой: количество команд невелико, VBA будет лишним, но одна лишь возможность выполнения скриптов могла бы значительно изменить работу с программой).

1.2 Введение в CDD

Cli-интерфейс жив и развивается. Cisco-like - это вполне вполне рабочий термин.

Что же может современный Cli? - Довольно много:

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

  • группировку команд ("уровни");

  • задание группы объектов для управления ("параметры");

  • логгирование;

  • исполнение скриптов;

  • типизированный ввод данных с валидацией;

Я придумал еще одну функцию: debug cli - проверка команд (CMD_ID / CMD_Item / CMD_Handler)

  • может показать число ID ("задуманные команды"), Realized- и NotRealized-команды для каждого модуля; (В идеале счетчики ID, Realized должны быть равны, но если NotRealized не равен 0, то это еще один стимул для разработчика: ну осталось всего-то 30...20...5...2 нереализованных команд - неужели оставим так? может лучше доделать? - и это работает!)

1.3 Основные идеи CDD

Можно сформулировать основные идеи CDD:

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

  2. Модульное построение: любой модуль можно убрать или заменить с предсказуемыми изменениями в функционале.

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

1.4 mCli - Реализация CDD

CDD использовано при построении mCli - Cli-фреймворка модульного типа (github.com/MikeGM2017/mCli). В текущем состоянии имеются события, типы и модули.

1.4.1 События mCli

В простейшем виде для ввода с клавиатуры нужно определение кода нажатой клавиши и (отдельно) определение нажатия Enter (ввод команды) и Ctrl+C (прерывание команды). В полном наборе необходимо определение нажатия Enter (ввод команды), Ctrl+C (прерывание команды), Up/Down (просмотр истории команд), Left/Right/Home/End (перемещение по строке ввода), Back/Delete (изменение строки ввода).

1.4.2 Типы mCli

mCli предполагает использование типов при вводе данных. В текущей реализации имеются следующие типы:

  • Word / Word_List / Word_Range (ключевые слова, List - можно ввести несколько ключевых слов через запятую, Range - выбор одного ключевого слова из нескольких вариантов)

  • Int / Int_List / Int_Range

  • Str

  • IP4 / IP6

  • MAC

  • Date / Time / DateTime

  • EQU_Range ( == != > < >= <= - для использования в скриптах, условное выполнение)

  • Rem (комментарий - для использования в скриптах)

1.4.3 Модули mCli

Модули mCli можно разделить на базовые, платформо-зависимые и кастомные.

Базовые модули:

  • Base_Quit (выход из приложения)

  • Base_Help (вывод информации по командам и их аргументам)

  • Base_Modules (вывод информации по задействованным модулям)

  • Base_History (история команд)

  • Base_Script (выполнение скриптов)

  • Base_Rem (комментарий, для использования в скриптах)

  • Base_Wait (пауза, для использования в скриптах)

  • Base_Log (управление логом)

  • Base_Debug (проверка списка команд, определение нереализованных команд)

  • Check (условное выполнение, для использования в скриптах)

Платформо-зависимые модули

Вывод:

  • Output_printf (Linux/Window)

  • Output_cout (Linux/Window)

  • Output_ncurses (Linux)

  • Output_pdcurses (Linux/Window)

Ввод:

  • Input_termios (Linux)

  • Input_conio (Window)

  • Input_ncurses (Linux)

  • Input_pdcurses (Linux/Window)

Кастомные модули:

  • ConfigureTerminal (демо: тестирование переменных)

  • SecureTerminal (демо: вход в модуль по паролю)

  • TestTerminal (демо: тестирование типов)

1.5 Объединение модулей в mCli

Связывание модулей происходит на самом верхнем уровне, например в функции main():

Cli_Modules Modules;// Modules Add - BeginModules.Add(new Cli_Module_Base_Rem(Str_Rem_DEF, Cli_Output));bool Cmd_Quit = false;Modules.Add(new Cli_Module_Base_Quit(Cmd_Quit));Str_Filter str_filter('?', '*');Modules.Add(new Cli_Module_Base_Help(User_Privilege, Modules, str_filter, Cli_Output));Modules.Add(new Cli_Module_Base_Modules(User_Privilege, Modules, str_filter, Cli_Output));Cli_History History;Modules.Add(new Cli_Module_Base_History(History, Cli_Output));Modules.Add(new Cli_Module_Base_Log(Cli_Input));bool Cmd_Script_Stop = false;int Script_Buf_Size = 1024;Modules.Add(new Cli_Module_Base_Script(History, Cli_Output,            Str_Rem_DEF, Cmd_Script_Stop, Cmd_Quit, Script_Buf_Size,            CMD_Processor));bool Log_Wait_Enable = true;bool Cmd_Wait_Stop = false;Modules.Add(new Cli_Module_Base_Wait(Log_Wait_Enable, Cmd_Wait_Stop, Cli_Input, Cli_Output));Modules.Add(new Cli_Module_Test_Tab_Min_Max());Modules.Add(new Cli_Module_Test_Terminal(Cli_Input, Cli_Output));Modules.Add(new Cli_Module_Base_Debug(User_Privilege, Modules, Levels, CMD_Processor, Cli_Output));Modules.Add(new Cli_Module_Check(Modules, Values_Map, str_filter, Cli_Output, Cmd_Script_Stop));// Modules Add - End

1.6 CDD и SOLID

SOLID в CDD достаточно легко обнаружить на уровне подключения и объединения модулей. Какие-то модули практически всегда используются, например Cli_Output нужен в большинстве модулей. Другие - гораздо реже (например, Cli_Input нужен только в модулях, в которых команда требует подтверждения).

Таким образом, SOLID в CDD - это:

  • S - каждый модуль отвечает за свой круг задач

  • O - здесь есть проблема: в каждом модуле есть enum Local_CmdID, и получается, что при наследовании список Local_CmdID не так просто расширить? Но в новом модуле мы можем завести новый enum Local_CmdID или (лучше) можно ввести новый enum Local_CmdID только для новых команд, стартующий с последнего элемента предыдущего enum (для этого можно использовать CMD_ID_LAST)

  • L - модуль может быть заменен на другой, с доработанной реализацией

  • I - при замене модуля может возникнуть ситуация, что потребуется больше (или меньше) связанных модулей; при создании экземпляра модуля это легко учесть (через конструктор или статический инициализатор)

  • D - модули связываются на верхнем уровне

1.7 CDD и KISS

В целом, набор команд любого модуля стремится к расширению: более удобный формат команд, новые флаги, новые возможности. Но сам список команд всегда доступен, и, если он сильно разрастается то просто делим модуль на 2 или более модуля!

На уровне модуля команды могут различаться флагами или дополнительными параметрами. Но реализация у них может быть одна. Например, "help" и "help full" реализуются одним методом, в качестве параметра принимающий строку фильтра - "*". Так что KISS сохраняется в таком смысле:

  • команда выполняется методом, имеющим несколько флагов (да, из-за этого метод делается чуть сложнее, зато несколько команд Cli могут выполняться однотипно).

1.8 CDD и DRY

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

1.9 CDD и YAGNI

Нужно убрать какой-то ненужный функционал? - Убираем ненужный модуль (или команды в модуле). За счет слабой связности модулей это несложно.

1.10 CDD и Bootstraping

В некоторых случаях (например, Embedded Baremetal) у нас есть только консоль. CDD может быть применено для разработки приложения "с нуля".

1.11 CDD и TDD

За счет наличия скриптов и модуля условного исполнения автоматизация тестирования сводится к следующему сценарию:

  • вручную вводится последовательность тестируемых команд;

  • история команд сохраняется в файле скрипта;

  • при необходимости скрипт редактируется / дополняется проверкой правильности выполнения;

  • вызов скрипта добавляется в общий скрипт тестирования или используется сам по себе;

1.12 CDD и GUI

А что GUI? GUI (да и Web - тоже) пусть посылает текстовые команды в Cli - эстетично, наглядно, надежно.

2. CDD и PDD

А вот еще и PDD!!!

2.1 PDD - Provocation Driven Development - еще один новый термин :)

Вообще, PDD - это то, что нас настигает постоянно. Допустим, есть путь, по которому мы идем к цели. Но на что нас провоцирует этот путь? Считаю, что мы должны осознавать это. Например, на что провоцируют языки программирования:

  • C провоцирует на нарушения доступа к памяти и на плохо контролируемые приведения типов;

  • C++ - на создание монолита (если за этим не следить, то имеем типовой пример: classMyCoolGame;myCoolGame.Run());

  • SQL, Lua - "все есть таблица";

  • Assembler - "стандартов нет";

  • Java - "щас понаделаем объектов";

  • JavaScript - "щас наподключаем библиотек, не самим же все делать"; и так далее - дополнительные примеры каждый, думаю, сможет придумать.

2.2 Что есть PDD для CDD?

В первую очередь - это тенденция на разбиение проекта на модули. Действительно:

  • Есть объект управления? - Выносим в модуль.

  • Есть повторяющийся код? - Выносим в модуль.

  • Новый функционал? - Добавляем новый модуль.

  • Новая архитектура? - Заменяем модули.

Описание команд - это текстовое описание функционала, фактически мы получаем DSL. Чтобы получить информацию о доступном функционале, достаточно ввести команду "help".

Предсказательный характер архитектуры:

  • пусть в расчетах на каждую Cli-команду отводим 1 (один) человеко-день. Да, можно за 1 день ввести 10-20 простых Cli-команд (да, простые или однотипные команды реализуются быстро), но не нужно обманываться: будет (обязательно будет!) функция, которая потребует 10 дней на реализацию и тестирование. Поэтому проект средней сложности на 200-300 Cli-команд займет 200-300 человеко-дней (хотя, это скорее оценка "сверху", реально проект может быть закончен раньше).

Скрипты с возможностью условного исполнения означают встроенную возможность тестирования, что (в теории) уменьшает вероятность регресса.

Расширяемость: новый модуль может, конечно, добавлять новые команды в глобальную видимость, но предпочтительный путь - группировка команд на уровне модуля, тогда в глобальной видимости появится всего лишь одна команда - переход на "уровень" модуля.

В Cli достаточно легко задавать обработку группы объектов. Можно, например:

  • ввести список объектов в команду;

  • ввести фильтр по именам объектов в команду;

  • ввести список объектов как параметр;

  • или даже ввести модуль Selection для управления группировкой объектов. Таким образом, можно использовать различные варианты групповой обработки.

Приложение достаточно легко поддерживать в постоянно рабочем состоянии: да, какой-то функционал еще не введен, зато все остальное - работает.

По-видимому, CDD ортогональна другим подходам, так как CDD может быть применено как в Waterfall, так и в Agile. Действительно, ввод сначала новых команд, а потом их реализация - близок к Waterfall. Но модульное построения и малая связность модулей способствуют применению Agile. Но CDD не мешает ни Waterfall, ни Agile, а только помогает.

3. Встроенный язык скриптов

3.1 Модуль Check

Условное выполнение реализовано в модуле "Check".

Для условного выполнения команд, в принципе, достаточно всего двух команд: "check label " - установка метки "check if == goto " - условный переход (здесь сравнение может быть не только на равенство: == != > < >= <= - вот полный список, но при этом команду можно оставить одну и ту же, а операторы сравнения ввести в виде списка возможных значений)

Переменные в простейшем случае - глобальные, заносятся в map<string,string>, для чего в модуле предусмотрен виртуальный метод .To_Map().

Для работы с переменными введены команды условного и безусловного присвоения, объединения, вывода на экран. Для полноценного языка этого, возможно, мало, но для задач тестирования функционала - вполне приемлемо.

3.2 Модуль Check vs Lua

Да, вместо встроенных модулей скриптов и условного выполнения можно подключить Lua. Однако, вместо нескольких команд (в действительности модуль условного выполнения Check получается не такой уж маленький - более 30 команд, хотя и однотипных) подключение Lua означает большое увеличение размера исполняемого файла, а в некоторых случаях это может быть критичным. Но как вариант, Lua выглядит очень привлекательно.

3.3 Модуль Check vs Erlang

Было бы неплохо, но запросы Уж очень большие у Erlang требования к ресурсам. Возможно, на "жирных" устройствах, как вариант, можно и Erlang подключить.

4. CDD vs Erlang

Неплохая попытка, подход Erlang - довольно похож на CDD. Но задумаемся, в чем PDD для Erlang? - "Ошибаемся и еще раз ошибаемся, а система все равно работает". Это, конечно, сильно. Поэтому вопрос: "CDD или Erlang" безусловно стоит. Но CDD можно реализовать на многих языках программирования (C/C++, C#, Java, JavaScript). А у Erlang - очень специфичный подход. Может быть, не Erlang vs CDD, а Erlang + CDD ??? Кажется, надо попробовать...

5. CDD и дробление монолита

Примерный путь преобразования монолита в CDD-приложение:

  • создаем CDD-приложение из Base-модулей;

  • legacy-монолит добавляем в виде нового Cli-модуля на новом "уровне" с минимальными командами вида "version get" / "info get" - на первом этапе достаточно "установить контакт" с монолитом;

  • в новом модуле вводим команды, специфичные для него: "start" / "stop" / "configure" ;

  • скорее всего новые команды будут группироваться вокруг каких-то понятий / объектов / процедур и т.п. - это повод выделить такие группы в отдельные модули + объекты управления; при этом в основном монолите вводятся ссылки на выделенные объекты;

  • в результате должен получиться набор модулей, причем каждый модуль должен содержать не более 10-20 команд;

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

6. Итоги

CDD выполняет SOLID, KISS, DRY, YAGNI, Bootstraping, TDD.

CDD провоцирует на модульное построение.

CDD дает возможность выполнения скриптов и внутреннее тестирование.

CDD может быть основой большого количества типов приложений.

CDD позволяет вводить новый функционал прогнозируемым способом.

CDD может быть основой построения OS.

CDD может быть использовано для относительного удобного поглощения монолитов. Причем вполне возможна оркестрация как монолитов, так и сервисов.

CDD дает возможность разделения работ:

  • постановщик задачи описывает новый модуль в виде набора команд;

  • исполнитель реализует команды;

  • тестировщик пишет скрипты для проверки нового функционала.

CDD поддерживает введение нового функционала, в том числе на разных уровнях:

  • новые модули;

  • новые команды в существующих модулях.

CDD обеспечивает безопасность при вводе команд:

  • команды парсятся, данные валидируются, сделать что-то вне Cli-команд невозможно (если, конечно, не вводить команды типа exec / system / eval).

CDD фактически дает документацию по функционалу приложения:

  • достаточно подать команду "help * verbose" - и описание команд и их аргументов уже есть.

Этого мало?

Тогда вот вам напоследок: CDD позволяет захватить мир. КМК

Да, и Linux стоит переписать по CDD. КМК

Подробнее..

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

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. Приходите, будет сложно, но занимательно.

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

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

Stm32 USB на шаблонах C. Продолжение. Делаем HID

27.03.2021 20:12:03 | Автор: admin

В прошлый раз показал один из способов распределение ресурсов между конечными точками, а именно регистров EPnR, памяти под дескрипторы буферов и под сами буферы. Предлагаю продолжить начатое и рассмотреть написанную библиотеку на примере создания простого HID-устройства, позволяющего управлять светодиодом.

Разделение прерывания

Кроме обозначенных ранее ресурсов конечные точки также делят единственное прерывание. Соответственно, общий (главный) обработчик должен корректно передать управление обработчику прерывания нужной конечной точки. Номер конечной точки, к которой обращается хост, записано в битах EP_ID регистра ISTR. Придерживаясь линии реализации полностью шаблонной библиотеки, получил следующий класс:

using EpRequestHandler = std::add_pointer_t<void()>;template<typename...>class EndpointHandlersBase;template<typename... Endpoints, int8_t... Indexes>class EndpointHandlersBase<TypeList<Endpoints...>, Int8_tArray<Indexes...>>{public:  // Массив указателей на обработчики  static constexpr EpRequestHandler _handlers[] = {Endpoints::Handler...};  // Индексы обработчиков  static constexpr int8_t _handlersIndexes[] = {Indexes...};public:  inline static void Handle(uint8_t number, EndpointDirection direction)  {    _handlers[_handlersIndexes[2 * number + (direction == EndpointDirection::Out ? 1 : 0)]]();  }};

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

template<int8_t Index, typename Endpoints>class EndpointHandlersIndexes{  // Предикат для поиска очередной конечной точки.  using Predicate = Select<Index % 2 == 0, IsTxOrBidirectionalEndpointWithNumber<Index / 2>, IsRxOrBidirectionalEndpointWithNumber<Index / 2>>::value;  static const int8_t EndpointIndex = Search<Predicate::template type, Endpoints>::value;public:  // В конец массива индекса вставляется номер соответствующей конечной точки или -1 в случае пропуска.  using type = typename Int8_tArray_InsertBack<typename EndpointHandlersIndexes<Index - 1, Endpoints>::type, EndpointIndex>::type;};template<typename Endpoints>class EndpointHandlersIndexes<-1, Endpoints>{public:  using type = Int8_tArray<>;};

Из такой реализации, кстати, вытекает рекомендация объявлять конечные точки с номерами по порядку, потому что размер массива индексов обработчиков равен удвоенному максимальному номеру конечной точки.

Класс конечной точки

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

template <uint8_t _Number, EndpointDirection _Direction, EndpointType _Type, uint16_t _MaxPacketSize, uint8_t _Interval>class EndpointBase...

А также расширенный, с адресами буферов и регистра (которые назначаются менеджером, описанным в прошлой статье). Из-за различий между конечными точками разных типов/направлений (например, у однонаправленной конечной точки типа Interrupt будет один буфер, а у двунаправленной или Bulk с двойной буферизацией - два) для каждого типа объявлен свой класс:

template <typename _Base, typename _Reg>class Endpoint : public _Base...template<typename _Base, typename _Reg, uint32_t _TxBufferAddress, uint32_t _TxCountRegAddress, uint32_t _RxBufferAddress, uint32_t _RxCountRegAddress>class BidirectionalEndpoint : public Endpoint<_Base, _Reg>...template<typename _Base, typename _Reg, uint32_t _Buffer0Address, uint32_t _Count0RegAddress, uint32_t _Buffer1Address, uint32_t _Count1RegAddress>class BulkDoubleBufferedEndpoint : public Endpoint<_Base, _Reg>

Конечная точка на текущий момент реализована простой: экспортирует метод инициализации (в котором заполняется регистр EPnR), метод заполнения дескриптора, методы управления битами регистра (Очистка битов CTR_TX/RX, установка битов TX/RX_STATUS), а также отправку данных.

Класс интерфейса

Следующей сущностью в иерархии является интерфейс, который, по сути (как я понимаю) есть просто контейнер для конечных точек, поэтому реализующий его класс очень простой (прикладываю код полностью, потому что здесь применяется мощь variadic-шаблонов, который позволил исключить лишние зависимости):

template <uint8_t _Number, uint8_t _AlternateSetting = 0, uint8_t _Class = 0, uint8_t _SubClass = 0, uint8_t _Protocol = 0, typename... _Endpoints>class Interface{public:  using Endpoints = Zhele::TemplateUtils::TypeList<_Endpoints...>;  static const uint8_t EndpointsCount = ((_Endpoints::Direction == EndpointDirection::Bidirectional ? 2 : 1) + ...);  static void Reset()  {    (_Endpoints::Reset(), ...);  }  static uint16_t FillDescriptor(InterfaceDescriptor* descriptor)  {    uint16_t totalLength = sizeof(InterfaceDescriptor);    *descriptor = InterfaceDescriptor {      .Number = _Number,      .AlternateSetting = _AlternateSetting,      .EndpointsCount = EndpointsCount,      .Class = _Class,      .SubClass = _SubClass,      .Protocol = _Protocol    };        EndpointDescriptor* endpointsDescriptors = reinterpret_cast<EndpointDescriptor*>(++descriptor);    totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + ...);    return totalLength;  }};

Класс конфигурации

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

template <uint8_t _Number, uint8_t _MaxPower, bool _RemoteWakeup = false, bool _SelfPowered = false, typename... _Interfaces>class Configuration{public:  using Endpoints = Zhele::TemplateUtils::Append_t<typename _Interfaces::Endpoints...>;  static void Reset()  {    (_Interfaces::Reset(), ...);  }...

Класс устройства

Наконец, в вершине иерархии находится непосредственно само устройство. В моей реализации соответствующий класс является производным от нулевой конечной точки, поскольку она особая и обмен данными с нулевой конечной точкой - это обмен данными (как правило, управляющими) с самим девайсом.

template<  typename _Regs,  IRQn_Type _IRQNumber,  typename _ClockCtrl,   uint16_t _UsbVersion,  DeviceClass _Class,  uint8_t _SubClass,  uint8_t _Protocol,  uint16_t _VendorId,  uint16_t _ProductId,  uint16_t _DeviceReleaseNumber,  typename _Ep0,  typename... _Configurations>class DeviceBase : public _Ep0{  using This = DeviceBase<_Regs, _IRQNumber, _ClockCtrl, _UsbVersion, _Class, _SubClass, _Protocol, _VendorId, _ProductId, _DeviceReleaseNumber, _Ep0, _Configurations...>;  using Endpoints = Append_t<typename _Configurations::Endpoints...>;  using Configurations = TypeList<_Configurations...>;  // Replace Ep0 with this for correct handler register.  using EpBufferManager = EndpointsManager<Append_t<_Ep0, Endpoints>>;  using EpHandlers = EndpointHandlers<Append_t<This, Endpoints>>;...

Класс содержит главный обработчик прерывания, диспетчеризуя запросы по различными конечным точкам:

static void CommonHandler(){  if(_Regs()->ISTR & USB_ISTR_RESET)  {    Reset();  }  if (_Regs()->ISTR & USB_ISTR_CTR)  {    uint8_t endpoint = _Regs()->ISTR & USB_ISTR_EP_ID;    EpHandlers::Handle(endpoint, ((_Regs()->ISTR & USB_ISTR_DIR) != 0 ? EndpointDirection::Out : EndpointDirection::In));  }  NVIC_ClearPendingIRQ(_IRQNumber);}

Являясь одновременно и нулевой конечной точкой, класс Device содержит обработчик обращений к нулевой точке, реализуя, на данный момент, процесс нумерации:

Обработчик прерывания нулевой конечной точки
static void Handler(){  if(_Ep0::Reg::Get() & USB_EP_CTR_RX)  {    _Ep0::ClearCtrRx();    if(_Ep0::Reg::Get() & USB_EP_SETUP)    {      SetupPacket* setup = reinterpret_cast<SetupPacket*>(_Ep0::RxBuffer);      switch (setup->Request) {      case StandartRequestCode::GetStatus: {        uint16_t status = 0;        _Ep0::Writer::SendData(&status, sizeof(status));        break;      }      case StandartRequestCode::SetAddress: {        TempAddressStorage = setup->Value;        _Ep0::Writer::SendData(0);        break;      }      case StandartRequestCode::GetDescriptor: {        switch (static_cast<GetDescriptorParameter>(setup->Value)) {        case GetDescriptorParameter::DeviceDescriptor: {          DeviceDescriptor tempDeviceDescriptor;          FillDescriptor(reinterpret_cast<DeviceDescriptor*>(&tempDeviceDescriptor));          _Ep0::Writer::SendData(&tempDeviceDescriptor, setup->Length < sizeof(DeviceDescriptor) ? setup->Length : sizeof(DeviceDescriptor));          break;        }        case GetDescriptorParameter::ConfigurationDescriptor: {          uint8_t temp[64];          uint16_t size = GetType<0, Configurations>::type::FillDescriptor(reinterpret_cast<ConfigurationDescriptor*>(&temp[0]));          _Ep0::Writer::SendData(reinterpret_cast<ConfigurationDescriptor*>(&temp[0]), setup->Length < size ? setup->Length : size);          break;        }        case GetDescriptorParameter::HidReportDescriptor: {          uint16_t size = sizeof(GetType_t<0, Configurations>::HidReport::Data);          _Ep0::Writer::SendData(GetType_t<0, Configurations>::HidReport::Data, setup->Length < size ? setup->Length : size);          break;        }        default:          _Ep0::SetTxStatus(EndpointStatus::Stall);          break;        }        break;      }      case StandartRequestCode::GetConfiguration: {        uint16_t configuration = 0;        _Ep0::Writer::SendData(&configuration, 1);        break;      }      case StandartRequestCode::SetConfiguration: {        _Ep0::Writer::SendData(0);        break;      }      default:        _Ep0::SetTxStatus(EndpointStatus::Stall);        break;      }    }    _Ep0::SetRxStatus(EndpointStatus::Valid);  }  if(_Ep0::Reg::Get() & USB_EP_CTR_TX)  {    _Ep0::ClearCtrTx();    if(TempAddressStorage != 0)    {      _Regs()->DADDR = USB_DADDR_EF | (TempAddressStorage & USB_DADDR_ADD);      TempAddressStorage = 0;    }    _Ep0::SetRxStatus(EndpointStatus::Valid);  }}

Интерфейс HID

HID-устройство - это устройство как минимум с одним интерфейсом типа HID, поэтому в библиотеке класс HID - это производный от интерфейса:

Класс интерфейса hid
template <uint8_t _Number, uint8_t _AlternateSetting, uint8_t _SubClass, uint8_t _Protocol, typename _Hid, typename... _Endpoints>class HidInterface : public Interface<_Number, _AlternateSetting, 0x03, _SubClass, _Protocol, _Endpoints...>{  using Base = Interface<_Number, _AlternateSetting, 0x03, _SubClass, _Protocol, _Endpoints...>;public:  using Endpoints = Base::Endpoints;  static uint16_t FillDescriptor(InterfaceDescriptor* descriptor)  {    uint16_t totalLength = sizeof(InterfaceDescriptor);    *descriptor = InterfaceDescriptor {      .Number = _Number,      .AlternateSetting = _AlternateSetting,      .EndpointsCount = Base::EndpointsCount,      .Class = 0x03,      .SubClass = _SubClass,      .Protocol = _Protocol    };    _Hid* hidDescriptor = reinterpret_cast<_Hid*>(++descriptor);    *hidDescriptor = _Hid {    };    uint8_t* reportsPart = reinterpret_cast<uint8_t*>(++hidDescriptor);    uint16_t bytesWritten = _Hid::FillReports(reportsPart);    totalLength += sizeof(_Hid) + bytesWritten;    EndpointDescriptor* endpointsDescriptors = reinterpret_cast<EndpointDescriptor*>(&reportsPart[bytesWritten]);    totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + ...);    return totalLength;  }private:};

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

HID-устройство

Теперь давайте из всего этого сделаем устройство, которое будет содержать один светодиод (потому что так удобно, он есть на плате BluePill) и поддерживать возможность управления этим светодиодом с компьютера (через USB HID Demonstrator).

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

using Report = HidReport<  0x06, 0x00, 0xff,    // USAGE_PAGE (Generic Desktop)  0x09, 0x01,          // USAGE (Vendor Usage 1)  0xa1, 0x01,          // COLLECTION (Application)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x15, 0x00,          //   LOGICAL_MINIMUM (0)  0x25, 0x01,          //   LOGICAL_MAXIMUM (1)  0x75, 0x08,          //   REPORT_SIZE (8)  0x95, 0x01,          //   REPORT_COUNT (1)  0xb1, 0x82,          //   FEATURE (Data,Var,Abs,Vol)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x91, 0x82,          //   OUTPUT (Data,Var,Abs,Vol)  0xc0                 // END_COLLECTION>;

Далее нужно определить всю ирерахию: конечные точки, интерфейс, конфигурацию и устройство:

using HidDesc = HidDescriptor<0x1001, Report>;using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 32>;using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;using Hid = HidInterface<0, 0, 0, 0, HidDesc, LedsControlEp>;using Config = HidConfiguration<0, 250, false, false, Report, Hid>;using MyDevice = Device<0x0200, DeviceClass::InterfaceSpecified, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;

В общем-то всё, осталось написать обработчик для конечной точки управления светодиодом:

using Led = IO::Pc13Inv; // Inv - инвертированный.template<>void LedsControlEp::Handler(){  LedsControlEp::ClearCtrRx();  uint8_t* buffer = reinterpret_cast<uint8_t*>(LedsControlEp::Buffer);  bool needSet = buffer[1] != 0;  // Код почти целиком позаимствован из поста "STM32 и USB-HID  это просто".  // Не стал изменять его для удобной навигации.  switch(buffer[0])  {  case 1:    needSet ? Led::Set() : Led::Clear();    break;  }  LedsControlEp::SetRxStatus(EndpointStatus::Valid);}

Целиком файл main.c для Stm32f103 выглядит так (по-моему, достаточно компактно):

Полный код программы
#include <clock.h>#include <iopins.h>#include <usb.h>using namespace Zhele;using namespace Zhele::Clock;using namespace Zhele::IO;using namespace Zhele::Usb;using Report = HidReport<  0x06, 0x00, 0xff,        // USAGE_PAGE (Generic Desktop)  0x09, 0x01,          // USAGE (Vendor Usage 1)  0xa1, 0x01,          // COLLECTION (Application)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x15, 0x00,          //   LOGICAL_MINIMUM (0)  0x25, 0x01,          //   LOGICAL_MAXIMUM (1)  0x75, 0x08,          //   REPORT_SIZE (8)  0x95, 0x01,          //   REPORT_COUNT (1)  0xb1, 0x82,          //   FEATURE (Data,Var,Abs,Vol)  0x85, 0x01,          //   REPORT_ID (1)  0x09, 0x01,          //   USAGE (Vendor Usage 1)  0x91, 0x82,          //   OUTPUT (Data,Var,Abs,Vol)  0xc0               // END_COLLECTION>;using HidDesc = HidDescriptor<0x1001, Report>;using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 32>;using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;using Hid = HidInterface<0, 0, 0, 0, HidDesc, LedsControlEp>;using Config = HidConfiguration<0, 250, false, false, Report, Hid>;using MyDevice = Device<0x0200, DeviceClass::InterfaceSpecified, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;using Led = IO::Pc13Inv;void ConfigureClock();void ConfigureLeds();int main(){  ConfigureClock();  ConfigureLeds();  Zhele::IO::Porta::Enable();  MyDevice::Enable();  for(;;)  {  }}void ConfigureClock(){  PllClock::SelectClockSource(PllClock::ClockSource::External);  PllClock::SetMultiplier(9);  Apb1Clock::SetPrescaler(Apb1Clock::Div2);  SysClock::SelectClockSource(SysClock::Pll);  MyDevice::SelectClockSource(Zhele::Usb::ClockSource::PllDividedOneAndHalf);}void ConfigureLeds(){  Led::Port::Enable();  Led::SetConfiguration<Led::Configuration::Out>();  Led::SetDriverType<Led::DriverType::PushPull>();  Led::Set();}template<>void LedsControlEp::Handler(){  LedsControlEp::ClearCtrRx();  uint8_t* buffer = reinterpret_cast<uint8_t*>(LedsControlEp::Buffer);  bool needSet = buffer[1] != 0;  switch(buffer[0])  {  case 1:    needSet ? Led::Set() : Led::Clear();    break;  }  LedsControlEp::SetRxStatus(EndpointStatus::Valid);}extern "C" void USB_LP_IRQHandler(){  MyDevice::CommonHandler();}

Заключение

Не совсем очевидная реализация библиотечного кода (в прошлой статье получил заслуженные комментарии в стиле "Не хотел бы увидеть такой код в продакшне", "Как это поддерживать" и т.п.) позволила максимально упростить непосредственно реализацию устройства, не нужно даже вручную объявлять дескрипторы: все генерируется из подставленных в шаблоны аргументов. Использование variadic-шаблонов помогло избавиться от лишних зависимостей. Прошивка тоже получается компактной, код из примера выше с оптимизацией Og вышел в 2360 байтов Flash и 36 байтов RAM (с оптимизацией Os прошивка весит 1712 байтов, но не работает. Пока не разобрался, почему именно), что я считаю неплохим результатом.

Благодарности

За замечательный пост про HID благодарен @RaJa. Также менее, чем за неделю до написания этого поста вышел еще крутой материал по HID от @COKPOWEHEU. Без этих постов я бы ничего не осилил. Еще большую помощь оказали пользователи с форума radiokot (COKPOWEHEU и VladislavS), был приятно удивлен оперативностью ответов и желанием помочь.

Подробнее..

Ядро macOS, есть ли червячки в этом яблоке?

29.03.2021 12:16:08 | Автор: admin

0818_XNU_MacOS_Kernel_ru/image1.png


В самом начале этого года Apple выложили в открытый доступ исходный код системных компонентов macOS 11.0 Big Sur, включая XNU ядро операционной системы macOS. Пару лет назад исходный код ядра уже проверялся PVS-Studio в связи с выходом анализатора для macOS. Прошло достаточно много времени, и вышел новый релиз исходного кода ядра. Почему бы и не провести повторную проверку.


Что это за проект, Apple и open-source?


XNU X is Not Unix используется и разрабатывается Apple в качестве ядра операционных систем OS X. Исходные коды этого ядра 20 лет назад были опубликованы под лицензией APSL (Apple Public Source License) вместе с OC Darwin. Раньше Darwin можно было даже установить в качестве полноценной операционной системы, однако теперь это стало невозможно. Причиной публикации исходного кода является тот факт, что он во многом основан на других open-source проектах.


Исходные коды компонентов можно найти тут. Для проверки я использовала зеркало проекта на GitHub.


Предыдущая проверка


Как я уже упомянула, этот проект ранее проверялся нами с помощью PVS-Studio. С предыдущими результатами можно познакомиться в статье: "Релиз PVS-Studio для macOS: 64 weaknesses в Apple XNU Kernel". После публикации мой коллега Святослав также отправил статью разработчикам на почту, но ответа не получил. Так что я предполагаю, что наша проверка никак не связана с исправлениями, которые мы дальше рассмотрим. Разработчикам пришлось искать их другим путём. А могли бы просто взять и запустить PVS-Studio :). Сейчас, после публикации статей, мы в основном пишем об этом в GitHub репозиторий проекта.


Мне стало интересно, были ли исправлены ошибки, описанные в предыдущей статье, или всё так и осталось. Большинство из найденных ошибок действительно были исправлены. Это показывает, что отобранные предупреждения анализатора оказались верными. Хотя для написания статьи с отчётом работал человек, не участвующий в разработке XNU, то есть близко не знакомый с этим исходным кодом.


Я приведу здесь несколько примеров исправлений. Но, чтобы не раздувать объём статьи, не буду полностью приводить объяснение ошибок. Если из исправления будет неясно, в чём была проблема, то вы всегда можете обратиться к первой статье по проверке этого проекта. Я не буду разбирать все исправленные фрагменты, большинство из фрагментов всё-таки было поправлено. А фрагментов в предыдущей статье было ни много ни мало 64!


Перейдём к рассмотрению исправлений примеров из прошлой статьи.


Фрагмент N1, в котором член класса сравнивался сам с собой:


intkey_parse(      struct mbuf *m,      struct socket *so){  ....  if ((m->m_flags & M_PKTHDR) == 0 ||      m->m_pkthdr.len != m->m_pkthdr.len) {    ....    goto senderror;  }  ....}

Был исправлен следующим образом:


0818_XNU_MacOS_Kernel_ru/image2.png


Где макрос, из которого получена переменная orglen, выглядит следующим образом:


#define PFKEY_UNUNIT64(a) ((a) << 3)

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


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


0818_XNU_MacOS_Kernel_ru/image3.png


Накосячить в условии assertf одно, но ещё и перезаписать переменную для отладочной версии такое точно стоит поправить.


Фрагменты 6 и 7 были исправлены одинаково. Оказалось, что во вложенной проверке перепутали значение перечислителя для сравнения. Вместо PBUF_TYPE_MBUF во внутренней проверке должен быть элемент PBUF_TYPE_MEMORY в обоих случаях.


0818_XNU_MacOS_Kernel_ru/image4.png


В случае фрагментов N8, 9, 10 исправление было таким:


0818_XNU_MacOS_Kernel_ru/image5.png


На это исправление я обратила внимание, так как серьёзная часть коммита в целом (обновление репозитория до xnu-4903.270.47 от 11 января) содержит помимо прочего много правок код-стайла. Это может указывать на то, что для данной версии кодовая база была подчищена с помощью разных инструментов качества кода. Что сделает эту проверку PVS-Studio более интересной. Ведь видно, что качество кодовой базы уже было улучшено другими инструментами.


Что касается фрагментов 11, 12, 13, 14 был исправлен только фрагмент 11:


0818_XNU_MacOS_Kernel_ru/image6.png


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


static intkauth_resolver_getwork(user_addr_t message){  struct kauth_resolver_work *workp;  int error;  KAUTH_RESOLVER_LOCK();  error = 0;  while ((workp = TAILQ_FIRST(....)) == NULL) { // <=    thread_t thread = current_thread();    struct uthread *ut = get_bsdthread_info(thread);    ut->uu_save.uus_kauth.message = message;    error = msleep0(....);    KAUTH_RESOLVER_UNLOCK();    /*     * If this is a wakeup from another thread in the resolver     * deregistering it, error out the request-for-work thread     */    if (!kauth_resolver_identity) {      printf("external resolver died");      error = KAUTH_RESOLVER_FAILED_ERRCODE;    }    return error; //<=  }  return kauth_resolver_getwork2(message);}

Предупреждение PVS-Studio: V612 An unconditional 'return' within a loop. kern_credential.c 951


Я привела код почти целиком, чтобы можно было сформировать общее представление о том, что происходит в этой функции. В случае отмеченного цикла при выполнении условия входа в него будет совершён один проход по телу цикла, завершающийся возвращением error. Видимо, подразумевалось, что если выполняется условие (workp = TAILQ_FIRST(....)) == NULL, то нужно найти причину ошибки и завершить функцию возвращением информации об ошибке. Однако по какой-то причине вместо if был написан while, как и во фрагменте из предыдущей статьи. Строчка error = msleep0(....) выглядит в коде таким образом:


error = msleep0(&kauth_resolver_unsubmitted,                kauth_resolver_mtx,                PCATCH,                "GRGetWork",                0,                 kauth_resolver_getwork_continue);

Здесь последним аргументом передаётся указатель на функцию kauth_resolver_getwork_continue. В теле этой функции есть условие, аналогичное условию цикла, на который нам указал анализатор. Но в нём уже корректно используется if, а не while.


static intkauth_resolver_getwork_continue(int result){  ....  if (TAILQ_FIRST(&kauth_resolver_unsubmitted) == NULL) {    ....    return error;  }  ....}

В принципе этот код работает немного сложнее, чем я описала. В нём присутствует рекурсия (в методе kauth_resolver_getwork_continue), и, как я поняла, он нацелен на нахождение потоков, которые можно перезагрузить. Но я не стала вдаваться в подробности, так как while всё равно лишний. Возможно, он остался здесь с того времени, когда исходный код выполнял ту же задачу, но без использования рекурсии.


Это примеры из начала статьи. Проскочим в середину и возьмём фрагмент N40. В нём одному и тому же элементу дважды присваивается одно значение:


Предупреждение PVS-Studio: V519 CWE-563 The 'wrap.Seal_Alg[0]' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 2070, 2071. gss_krb5_mech.c 2071


Эта ошибка, конечно же, тоже была поправлена:


0818_XNU_MacOS_Kernel_ru/image7.png


Ну и ближе к концу статьи, фрагмент 62 был исправлен так, как и было предложено в предыдущей статье. Причём это было единственной правкой в том файле.


0818_XNU_MacOS_Kernel_ru/image8.png


Фрагменты 63 и 64 также были исправлены, но там код был изменён капитально. Поэтому понять, какое исправление было именно для рассмотренного предупреждения, сложно.


Новые находки


После этого долгого вступления перейду к ошибкам, которые привлекли моё внимание при последней проверке исходного кода XNU статическим анализатором PVS-Studio. Скажу честно, мне тяжело далась работа с отчётом, так как проект имеет сложный код и у меня нет опыта работы с подобной кодовой базой. Но предупреждения PVS-Studio достаточно подробны и имеют ссылку на документацию с примерами правильного и неправильного кода и описанием возможной проблемы, что очень меня выручило.


К этой проверке cloc насчитал в проекте 1346 *.c файлов, 1822 С/C++ хэдера и 225 *.cpp файлов.


Ну и перейдём к разбору интересных находок.


Фрагмент N1


voidpe_identify_machine(__unused boot_args *args){  ....  // Start with default values.  gPEClockFrequencyInfo.timebase_frequency_hz = 1000000000;  gPEClockFrequencyInfo.bus_frequency_hz      =  100000000;  ....  gPEClockFrequencyInfo.dec_clock_rate_hz =     gPEClockFrequencyInfo.timebase_frequency_hz;  gPEClockFrequencyInfo.bus_clock_rate_hz =   gPEClockFrequencyInfo.bus_frequency_hz;  ....   gPEClockFrequencyInfo.bus_to_dec_rate_den =    gPEClockFrequencyInfo.bus_clock_rate_hz /    gPEClockFrequencyInfo.dec_clock_rate_hz;}

Предупреждение PVS-Studio: V1064 The 'gPEClockFrequencyInfo.bus_clock_rate_hz' operand of integer division is less than the 'gPEClockFrequencyInfo.dec_clock_rate_hz' one. The result will always be zero. pe_identify_machine.c 72


Все используемые здесь поля имеют целочисленный тип:


extern clock_frequency_info_t gPEClockFrequencyInfo;struct clock_frequency_info_t {  unsigned long bus_clock_rate_hz;  unsigned long dec_clock_rate_hz;  unsigned long bus_to_dec_rate_den;  unsigned long long bus_frequency_hz;  unsigned long timebase_frequency_hz;  ....};

Через промежуточные присвоения полю gPEClockFrequencyInfo.bus_clock_rate_hz, являющемуся делимым, присваивается значение 100000000, а полю-делителю gPEClockFrequencyInfo.dec_clock_rate_hz присваивается значение 1000000000. Делитель в этом случае в десять раз больше делимого. Так как все поля здесь являются целочисленными, поле gPEClockFrequencyInfo.bus_to_dec_rate_den окажется равным 0.


Судя по наименованию результирующего поля bus_to_dec_rate_den, делитель и делимое были перепутаны местами. Я допускаю возможность, что код был написан с расчётом на то, что исходные значения изменятся и результат уже не будет равен 0. Но этот код всё равно кажется мне очень подозрительным.


Фрагмент N2


voidsdt_early_init( void ){  ....  if (MH_MAGIC_KERNEL != _mh_execute_header.magic) {  ....  } else {    ....    for (....) {    const char *funcname;    unsigned long best;                           //<=    ....    funcname = "<unknown>";    for (i = 0; i < orig_st->nsyms; i++) {      char *jname = strings + sym[i].n_un.n_strx;      ....      if ((unsigned long)sym[i].n_value > best) { //<=        best = (unsigned long)sym[i].n_value;        funcname = jname;      }    }    .....  }}

Предупреждение PVS-Studio: V614 Uninitialized variable 'best' used. sdt.c 572


Насколько я поняла, этот метод ищет название некоей функции. В алгоритме используется переменная best, возможно, это положение наилучшего кандидата на результат. Однако изначально эта переменная только объявляется без инициализации. Следующее же использование сверяет значение некоего элемента с переменной best, которая будет неинициализированной на тот момент. Еще страннее, что она инициализируется только внутри условия, в котором используется её же значение.


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


Фрагмент N3


intcdevsw_isfree(int index){  struct cdevsw * devsw;  if (index < 0) {    if (index == -1) {      index = 0;    } else {      index = -index;     }    devsw = &cdevsw[index];    for (; index < nchrdev; index++, devsw++) {      if (memcmp(....) == 0) {        break;      }    }  }  if (index < 0 || index >= nchrdev) {    return -1;  }  ....  return index;}

Предупреждение PVS-Studio: V560 A part of conditional expression is always false: index < 0. bsd_stubs.c:236


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


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


Фрагмент N4


intnfs_vinvalbuf_internal(....){  struct nfsbuf *bp;  ....  off_t end = ....;  /* check for any dirty data before the EOF */  if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end))  {    /* clip dirty range to EOF */    if (bp->nb_dirtyend > end)    {      bp->nb_dirtyend = end;      if (bp->nb_dirtyoff >= bp->nb_dirtyend)             //<=      {        bp->nb_dirtyoff = bp->nb_dirtyend = 0;      }    }    if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) //<=    {      ....    }  }  ....}

Предупреждения PVS-Studio:


  • V547 Expression 'bp->nb_dirtyoff >= bp->nb_dirtyend' is always false. nfs_bio.c 3858
  • V560 A part of conditional expression is always true: (bp->nb_dirtyoff < end). nfs_bio.c 3862

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


Начнём с первого предупреждения. Анализатор решил, что nb_dirtyoff не может быть больше или равен nb_dirtyend. Разберёмся почему. Перед подозрительной проверкой есть ещё два if с проверками (bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end) и bp->nb_dirtyend > end. А также осуществляется присвоение bp->nb_dirtyend = end.


Почему же третья проверка bp->nb_dirtyoff >= bp->nb_dirtyend будет всегда false?


0818_XNU_MacOS_Kernel_ru/image9.png


Всё просто. Из условий выходит, что nb_dirtyoff меньше, чем end, а nb_dirtyend равно end. В итоге nb_dirtyend точно больше, чем nb_dirtyoff. Присвоение bp->nb_dirtyoff = bp->nb_dirtyend = 0 никогда не будет выполнено.


В итоге вот такой участок кода:


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) {  /* clip dirty range to EOF */  if (bp->nb_dirtyend > end) {    bp->nb_dirtyend = end;    if (bp->nb_dirtyoff >= bp->nb_dirtyend) {  //<=      bp->nb_dirtyoff = bp->nb_dirtyend = 0;    }  }}

Можно упростить хотя бы до такого:


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) {  if (bp->nb_dirtyend > end) {    bp->nb_dirtyend = end;  }}

Но только если в настоящий момент этот алгоритм работает корректно.


Второе предупреждение указывает на четвёртый if, вложенный в первый.


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end))

Здесь анализатор выдаёт предупреждение на основании того, что присвоение нуля никогда не будет выполнено. В итоге во внешнем условии уже была проверка bp->nb_dirtyoff < end и внутренняя проверка из-за ошибки в условии выше становится бессмысленной.


Фрагмент N5


tcp_output(struct tcpcb *tp){  ....  if (isipv6) {    ....    if (len + optlen) {      ....    }  } else {    ....    if (len + optlen) {      ....    }  }  ....}

Предупреждение PVS-Studio: V793 It is odd that the result of the 'len + optlen' statement is a part of the condition. Perhaps, this statement should have been compared with something else.


Это достаточно простой недочёт. В условии вместо логического выражения просто складываются две переменные. В итоге выражение будет ложным, только если сумма окажется равной нулю. Если это и подразумевалось, то, возможно, стоит сделать сравнение с 0 явным, чтобы вопроса о правильности условия точно не возникало.


Конечно, может быть, что так и задумано, но чуть выше в коде есть вот такая проверка:


if (len + optlen + ipoptlen > tp->t_maxopd) {  ....}

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


Ещё замечу, что эта функция, сокращённая тут до 16 строк, занимает в оригинале 2268 строк! Ещё один возможный повод для рефакторинга ;)


Второе предупреждение на этот же участок:


V793 It is odd that the result of the 'len + optlen' statement is a part of the condition. Perhaps, this statement should have been compared with something else.


Фрагмент N6


intttyinput(int c, struct tty *tp){  ....  if (tp->t_rawq.c_cc + tp->t_canq.c_cc) {  ....}

Предупреждение PVS-Studio: V793 It is odd that the result of the 'tp->t_rawq.c_cc + tp->t_canq.c_cc' statement is a part of the condition. Perhaps, this statement should have been compared with something else. tty.c 568


Аналогичный случай. Тут повыше в коде снова есть проверка, которая не просто использует сумму, а сравнивает результат с другой переменной:


if (   tp->t_rawq.c_cc + tp->t_canq.c_cc > I_HIGH_WATER  3 // <=    && ....) {  ....}

В упрощённом коде условие, на которое указал анализатор, выглядит заметно. Но в оригинале оно было вложено в несколько if. Так что при код-ревью такое можно и пропустить, а анализатор не пропустит ;)


Фрагмент N7


errno_tmbuf_adjustlen(mbuf_t m, int amount){  /* Verify m_len will be valid after adding amount */  if (amount > 0) {    int used =  (size_t)mbuf_data(m)              - (size_t)mbuf_datastart(m)              + m->m_len;    if ((size_t)(amount + used) > mbuf_maxlen(m)) {      ....    }  ....  return 0;}

Предупреждение PVS-Studio: V1028 Possible overflow. Consider casting operands of the 'amount + used' operator to the 'size_t' type, not the result. kpi_mbuf.c


Снова ошибка в условии, но уже совсем другого рода. Вместо приведения к size_t операндов сложения, чтобы результат точно поместился в числовой тип, к size_t приводится результат сложения. Если в итоге сложения возникнет переполнение, то с результатом mbuf_maxlen(m) будет сравниваться бессмысленное значение, приведённое к size_t. Раз программист всё-таки хотел защититься от переполнения, то стоит его сделать правильно:


if ((size_t)amount + used > mbuf_maxlen(m))

Таких срабатываний было несколько, стоит обратить на этот момент внимание.


  • V1028 Possible overflow. Consider casting operands, not the result. vm_compressor_pager.c 1165
  • V1028 Possible overflow. Consider casting operands, not the result. vm_compressor_pager.c 1131
  • V1028 Possible overflow. Consider casting operands, not the result. audit_worker.c 241
  • V1028 Possible overflow. Consider casting operands of the '((u_int32_t) slp * hz) + 999999' operator to the 'long' type, not the result. tty.c 2199

Фрагмент N8


intfdavail(proc_t p, int n){  ....  char *flags;  int i;  int lim;  ....  lim = (int)MIN(....);  if ((i = lim - fdp->fd_nfiles) > 0 && (n -= i) <= 0) //<=  {    return 1;  }  ....  for (....)  {    if (*fpp == NULL && !(*flags & UF_RESERVED) && --n <= 0)    {      return 1;    }  }  return 0;}

Предупреждение PVS-Studio: V1019 Compound assignment expression 'n -= i' is used inside condition. kern_descrip.c_99 3916


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


i = lim - fdp->fd_nfiles;if (i > 0){  n -= i;  if(n <= 0)    return 1;}

Этот код выглядит менее эффективным, но точно является более понятным. Для быстрой проверки равнозначности эффективности этого кода можно зайти на Godbolt (Compiler Explorer), где, кстати, можно тестировать работу диагностик PVS-Studio. Анализатор легко найти среди инструментов этого сервиса.


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


Но, если обратить внимание на тело этого if, новое значение n в нём не используется. То есть вполне возможно, что никакое присвоение здесь и не нужно. Тогда можно обойтись таким кодом:


i = lim - fdp->fd_nfiles;if (i > 0) {  if(n  i <= 0)    return 1;}

И, более того, исходный код может приводить к ошибке при дальнейшем использовании переменной n. Если выражение (n -= i) <= 0 окажется ложным, то далее будет использоваться уже новое значение n. Так как я не работала вплотную с исходным кодом, мне сложно сказать, какое поведение является верным.


Фрагмент N9


static errno_tvsock_put_message_listening(struct vsockpcb *pcb,                             enum vsock_operation op,                            struct vsock_address src,                             struct vsock_address dst){  switch (op)  {    case VSOCK_REQUEST:      ....      if (....)      {        vsock_pcb_safe_reset_address(pcb, dst, src);        ....      }      ....      done:        ....        break;    case VSOCK_RESET:      error = vsock_pcb_safe_reset_address(pcb, dst, src);      break;    default:      vsock_pcb_safe_reset_address(pcb, dst, src);      ....      break;  }  return error;}

Предупреждение PVS-Studio: V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 549


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


static errno_tvsock_pcb_safe_reset_address(struct vsockpcb *pcb,                              struct vsock_address src,                              struct vsock_address dst)

При использовании этой функции в данном фрагменте последние два аргумента с аналогичными именами передаются в другом порядке.


Срабатывания на тот же фрагмент:


  • V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 587
  • V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 590

Фрагмент N10


intifclassq_tbr_set(struct ifclassq *ifq, ....){  struct tb_regulator *tbr;  ....  tbr = &ifq->ifcq_tbr;  ....  tbr->tbr_rate = TBR_SCALE(rate / 8) / machclk_freq;  ....  tbr->tbr_last = read_machclk();  if (   tbr->tbr_rate > 0               //<=      && (ifp->if_flags & IFF_UP))  {     ....  } else {    ....  }  ....  return 0;}

Предупреждение PVS-Studio: V1051 Consider checking for misprints. It's possible that the 'tbr->tbr_last' should be checked here. classq_subr.c 685


В проекте эта диагностика работала не лучшим образом, так как в коде постоянно над телом условия или цикла инициализировались сторонние переменные с именами, похожими на используемые в условии. Поэтому на этот раз диагностика выдала несколько явно ложных предупреждений. Но рассматриваемое нами срабатывание всё же показалось мне подозрительным, так как проверяемое поле tbr_rate не использовалось в теле условия и было инициализировано на 35 строк выше этой проверки. А вот поле tbr_last, инициализированное прямо перед этой проверкой, больше нигде не используется. Можно предположить, что проверить нужно было его вместо tbr_rate.


Фрагмент N11


voidaudit_arg_mac_string(struct kaudit_record *ar, ....){  if (ar->k_ar.ar_arg_mac_string == NULL)  {    ar->k_ar.ar_arg_mac_string = kheap_alloc(....);  }  ....  if (ar->k_ar.ar_arg_mac_string == NULL)  {    if (ar->k_ar.ar_arg_mac_string == NULL) // <=    {      return;    }  }  ....}

Предупреждение PVS-Studio: V571 Recurring check. The 'if (ar->k_ar.ar_arg_mac_string == NULL)' condition was already verified in line 245. audit_mac.c 246


Предупреждение PVS-Studio: V547 Expression 'ar->k_ar.ar_arg_mac_string == NULL' is always true. audit_mac.c 246


На этот код анализатор выдал сразу два предупреждения.


Сначала взгляд может зацепиться за то, что проверка в самом первом if и во втором совпадает. Но тут всё правильно: внутри тела первой проверки аллоцируется память, а для второй проверки есть пояснение:


/* * XXX This should be a rare event. * If kheap_alloc() returns NULL, * the system is low on kernel virtual memory. To be * consistent with the rest of audit, just return * (may need to panic if required to for audit). */

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


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


Фрагмент N12


intutf8_encodestr(....){  u_int16_t ucs_ch;  int swapbytes = ....;  ....  ucs_ch = swapbytes ? OSSwapInt16(*ucsp++) : *ucsp++;  ....}

Предупреждение PVS-Studio: V567 Undefined behavior. The 'ucsp' variable is modified while being used twice between sequence points. vfs_utfconv.c 298


Макросы очень коварная штука. Возможно, вы даже уже встречались с нашей статьей "Вред макросов для C++ кода". Я обычно при написании статей избегаю срабатываний на макросы. С ними всегда всё оказывается сложно без знакомства с кодовой базой проекта.


Но в случае этой ошибки всё оказалось чуть проще. Хотя, чтобы дойти до причины и развернуть цепочку макросов, пришлось прыгнуть в ту ещё кроличью нору. Собственно, цепочка эта начинается с выражения OSSwapInt16(*ucsp++).


0818_XNU_MacOS_Kernel_ru/image10.png


Потом я поняла, что есть способ попроще, и просто обратилась к .i файлу, который остался после проверки проекта. По нему строка с этим макросом развернулась следующим образом:


ucs_ch = swapbytes? ( (__uint16_t)(__builtin_constant_p(*ucsp++)   ? ((__uint16_t)(  (((__uint16_t)(*ucsp++) & 0xff00U) >> 8)                   | (((__uint16_t)(*ucsp++) & 0x00ffU) << 8)))   : _OSSwapInt16(*ucsp++))): *ucsp++;

Больше всего здесь нас интересует вот этот участок выражения:


  (((__uint16_t)(*ucsp++) & 0xff00U) >> 8)| (((__uint16_t)(*ucsp++) & 0x00ffU) << 8)

Никакой из операторов в выражении не является точкой следования. Так как точно неизвестно, какой из аргументов оператора | будет вычисляться первым, значение *uscp оказывается неопределённым.


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


Однако это ещё не всё! Есть очень интересный и важный момент. Готова поспорить, что человек, писавший этот код, планировал увеличить значение *ucsp только один раз. Но, на самом деле, значение увеличится дважды. Это не видно и непонятно. Макросы очень и очень опасны из-за вот таких случаев. Во многих ситуациях лучше написать обыкновенную функцию. Скорее всего, компилятор автоматически выполнит подстановку и никакого ухудшения производительности не произойдёт.


Фрагмент N13


struct pf_status pf_status;intpf_insert_state(struct pf_state *s, ....){  ....  if (....) {    s->id = htobe64(pf_status.stateid++);    ....  }  ....}

Предупреждение PVS-Studio: V567 Undefined behavior. The 'pf_status.stateid' variable is modified while being used twice between sequence points. pf.c 1440


И снова коварные макросы смешали все карты для инкремента. Рассмотрим строку с вызовом htobe64, которая оказалась подозрительной для анализатора после препроцессинга:


s->id = (__builtin_constant_p(pf_status.stateid++) ? ((__uint64_t)((((__uint64_t)(pf_status.stateid++) &0xff00000000000000ULL) >> 56) | (((__uint64_t)(pf_status.stateid++) &0x00ff000000000000ULL) >> 40) | (((__uint64_t)(pf_status.stateid++) &0x0000ff0000000000ULL) >> 24) | (((__uint64_t)(pf_status.stateid++) &0x000000ff00000000ULL) >> 8)  | (((__uint64_t)(pf_status.stateid++) &0x00000000ff000000ULL) << 8)  | (((__uint64_t)(pf_status.stateid++) &0x0000000000ff0000ULL) << 24) | (((__uint64_t)(pf_status.stateid++) &0x000000000000ff00ULL) << 40) | (((__uint64_t)(pf_status.stateid++) &0x00000000000000ffULL) << 56))) : _OSSwapInt64(pf_status.stateid++));

0818_XNU_MacOS_Kernel_ru/image11.png


Проблема собственно та же, что и в предыдущем примере. Во внутренней цепочке с операндами | и & нет точек следования. Поэтому неизвестно, какое значение примет pf_status.stateid на моменте выполнения каждой операции. Результат также неопределён.


И, опять-таки, переменная увеличивается несколько раз подряд, что является неприятным сюрпризом от макроса :).


Вот остальные срабатывания этой диагностики на этом проекте:


  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. ip_id.c 186
  • V567 Undefined behavior. The 'lp' variable is modified while being used twice between sequence points. nfs_boot.c 505
  • V567 Undefined behavior. The 'lp' variable is modified while being used twice between sequence points. nfs_boot.c 497
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 588
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 665
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 1543

Фрагмент N14


__private_extern__ boolean_tipsec_send_natt_keepalive(....){  ....  struct udphdr *uh = (__typeof__(uh))(void *)(  (char *)m_mtod(m)                                                + sizeof(*ip));  ....  if (....)  {    uh->uh_sport = (u_short)sav->natt_encapsulated_src_port;  } else {    uh->uh_sport = htons((u_short)esp_udp_encap_port);  }  uh->uh_sport = htons((u_short)esp_udp_encap_port);  ....}

Предупреждение PVS-Studio: V519 The 'uh->uh_sport' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 4866, 4870. ipsec.c 4870


В этом фрагменте возникла подозрительная ситуация: полю uh_sport в зависимости от определённого условия присваиваются разные значения. Однако сразу после if-else этому же полю снова присваивается значение, такое же как в ветке else. В итоге этот if-else блок теряет смысл, так как значение поля всё равно будет перезаписано.


Фрагмент N15


static kern_return_tvm_shared_region_slide_page_v3(vm_offset_t vaddr, ....){  ....  uint8_t *page_content = (uint8_t *)vaddr;  uint16_t page_entry;  ....  uint8_t* rebaseLocation = page_content;  uint64_t delta = page_entry;  do {    rebaseLocation += delta;    uint64_t value;    memcpy(&value, rebaseLocation, sizeof(value));    ....    bool isBind = (value & (1ULL << 62)) == 1;   // <=    if (isBind) {      return KERN_FAILURE;    }    ....  } while (delta != 0);  ....}

Предупреждение PVS-Studio: V547 Expression '(value & (1ULL << 62)) == 1' is always false. vm_shared_region.c 2820


Тут получилось много кода из-за использования большого числа переменных. Но интересует нас только указанная мною строка с инициализацией isBind. Разберём это выражение по шагам.


В результате побитового сдвига создаётся маска с единственной единицей в 63-ем бите. Результат побитового & с переменной value может принимать только значения 0 или 0x4000000000000000. А никакое из этих значений не равно 1. В итоге условие всегда будет ложным.


Судя по тому, что выполнение этого условия должно было привести к выходу из функции с возвратом KERN_FAILURE, возможно, как раз значение 0x4000000000000000 является тем самым исключительным случаем, после которого нужно выйти из функции. Тогда результат побитовых операций надо было сравнивать с этим числом, а не с 1. Ну, или написать так:


bool isBind = (value & (1ULL << 62)) != 0;

Фрагмент N16


intvn_path_package_check(char *path, int pathlen, ....){  char *ptr, *end;  int comp = 0;  ....  end = path + 1;  while (end < path + pathlen && *end != '\0') {    while (end < path + pathlen && *end == '/' && *end != '\0') {      end++;    }    ptr = end;    while (end < path + pathlen && *end != '/' && *end != '\0') {      end++;    }    ....  }  ....}

Предупреждение PVS-Studio: V590 Consider inspecting this expression. The expression is excessive or contains a misprint. vfs_subr.c 3589


Эта диагностика всегда указывает на излишний код. Иногда под ним скрывается более серьёзная ошибка. Но здесь это, скорее всего, просто недочёт. Предупреждение было выдано на первый внутренний while. Нет смысла проверять, что символ одновременно равен '/' и не равен '\0'. Достаточно только первой проверки, так как если *end равен '/', то он точно не может быть равен '\0'.


Следующий while содержит столько же проверок. Но из-за того, что в обоих случаях проверяется неравенство, эти проверки могут работать вместе. Возможно, сначала был написан второй while, а первый был скопирован с него с изменённой проверкой для '/'. Тогда перед нами недочёт, возникший из-за копипасты.


Заключение


В этот раз в проекте нашлось несколько меньше ошибок, чем в предыдущей статье. Весьма вероятно, что в процесс разработки XNU был внедрён статический анализ и другие инструменты контроля качества кода. Почти наверняка на проекте используется Clang Static Analyzer. Но ошибки и недочёты всё-таки нашлись. Я не стала приводить здесь некоторые срабатывания на подозрительные места, вывод по которым можно сделать только на основании большего понимания кодовой базы.


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


Если вам интересно, какие ошибки может помочь найти статический анализ в целом и PVS-Studio конкретно, то можете познакомиться с нашей подборкой статей про проверку исходных проектов. Там есть проверки кода не только операционных систем, но и, например, компиляторов и других инструментов программирования, которыми вы, возможно, пользуетесь ежедневно. Например, совсем недавно вышла статья про проверку Qt6.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Victoria Khanieva. MacOS Kernel, Is This Apple Rotten?.

Подробнее..

Пишем свою навигацию в Qt

02.04.2021 18:19:41 | Автор: admin

Всем привет. Я android разработчик с небольшим стажем. И сегодня я бы хотел поделиться опытом разработки учебного проекта на C++ и Qt с простой навигацией между экранами. Буду рад услышать критику или дополнения к моему решению и надеюсь, что оно сможет кому-то помочь и упростить жизнь.

Задача

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

У меня была идея проекта, что-то вроде упрощенной версии Trello, но для ее реализации нужно было определиться с тем, как будет осуществляться смена контента в окне. Какие вообще есть варианты? Мы можем создавать разные окна на каждую задачу, но такой вариант плохо сказывается на пользовательском опыте и не нравится мне.

Чтобы отказаться от главного окна, нужно написать навигатор, который будет уметь заменять экраны внутри QStackedWidget и предоставит различные сценарии переходов от одного экрана к другому. Также необходимо знать о предыдущих экранах для возможности вернуться к ним и как-то передавать данные.

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

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

  1. Общий родитель у экранов для реализации взаимодействий.

  2. Фабрика экранов.

  3. Определение команд для навигации у экранов.

  4. Хранение стека экранов.

Общая модель экрана

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

class BaseFragment: public QFrame {    Q_OBJECTsignals:    //тут мы потом определим сигналы для навигатора public:    BaseFragment();    ~BaseFragment();    //тут реализуем что-то из жизненного цикла};

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

class LoginFragment: public BaseFragmentclass StartFragment: public BaseFragmentclass RegistrationFragment : public BaseFragment

Фабрика фрагментов

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

class BaseScreensFactory {public:    BaseScreensFactory();    ~BaseScreensFactory();    virtual BaseFragment* create(QString tag);    virtual QString createStart();};

Фабрика имеет всего два виртуальных метода:

  1. create(QString tag) создает экран по его идентификатору (идентификатор мы указываем в конкретной реализации фабрики);

  2. createStart() возвращает идентификатор стартового экрана.

Ниже реализация фабрики в моем проекте:

// screensfacrory.h заголовочный файлnamespace screens {    static const QString SPLASH_TAG = "splash";    static const QString START_TAG = "start";    static const QString LOGIN_TAG = "login";    static const QString REGISTRATION_TAG = "registration";  // и так далее.....};class ScreensFactory: public BaseScreensFactory {public:    ScreensFactory();    ~ScreensFactory();    BaseFragment* create(QString tag) override;    QString createStart() override;};// screensfacrory.cpp исходникиBaseFragment* ScreensFactory::create(QString tag) {    qDebug("ScreensFactory create");    if (tag == SPLASH_TAG) {        return new SplashFragment;    } else if (tag == START_TAG) {        return new StartFragment;    } else if (tag == LOGIN_TAG) {        return new LoginFragment;    } else if (tag == REGISTRATION_TAG) {       // и так далее.....    }}QString ScreensFactory::createStart() {    return SPLASH_TAG; // идентификатор стартового экрана.}

Наконец сама навигация

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

Нужные нам методы навигации:

  • navigateTo(tag) переход к новому экрану с добавлением этого экрана в цепочку открытых живущих.

  • back() переход к предыдущему экрану с в цепочке с удалением текущего из нее.

  • replace(tag) замена текущего экрана в цепочке на новый.

  • newRootScreen(tag) удаление текущей цепочки и создание нового экрана.

  • navigateToWhithData(tag, data) то же самое, что и navigateTo(tag), только вместе с именем экрана передается ссылка на какой-то объект.

Например, когда мы открываем экран с регистрацией, находясь на экране приветствия, выполняется метод navigateTo(REGISTRATION_TAG), после регистрации нужно открыть главный экран, но запретить переход назад. Для этого выполняем newRootScreen(MAIN_TAG).

Чтобы навигатор понимал, что нужно текущему фрагменту, в класс BaseFragment нужно дописать сигналы для методов навигации, описанных выше.

class BaseFragment: public QFrame {    Q_OBJECTsignals:  //дописанные сигналы    void back();    void navigateTo(QString tag);    void newRootScreen(QString tag);    void replace(QString tag);        void navigateWhithData(QString tag, BaseModel* model); public:    BaseFragment();    ~BaseFragment();  //дописанные методы для жизненного цикла    virtual void onPause();    virtual void onResume();    virtual void setData(BaseModel* model);};

Поскольку фрагменты не все время находятся на экране, мы можем ставить на паузу процессы, выполняющиеся в нем, вызывая onPause(), и возобновлять, вызывая onResume() из навигатора, который мы скоро напишем. И метод setData() мы вызываем при передаче данных в фрагмент.

После долгих подготовок переходим к написанию навигации. Навигатор будет получать QStackedWidget как контейнер для фрагментов и BaseScreensFactory для создания фрагментов.

navigator.h:

class Navigator: public QObject {    Q_OBJECTprivate:    QStackedWidget *currentContainer;    BaseScreensFactory *screensFactory;    QLinkedList<BaseFragment*> stack;    /**     * @brief createAndConnect     * @param tag тэг создаваемого фрагмента.     *     * Создание фрагмента по тегу и его     * прикрепление к навигатору.     *     * @return фрагмент присоединенный к слотам навигатора.     */    BaseFragment* createAndConnect(QString tag);    /**     * @brief connectFragment     * @param fragment фрагмент который переходит     *        в активное состояние.     *     * Прикрепление текущего фрагмента     * к слотам навигатора для быстрого     * и удобного перехода между экранами.     *     */    void connectFragment(BaseFragment *fragment);    /**     * @brief disconnectFragment     * @param fragment     *     * Отключение сигналов от фрагмента.     */    void disconnectFragment(BaseFragment *fragment);public:    Navigator(            QStackedWidget *container,            BaseScreensFactory *screensFactory    );    ~Navigator();    BaseFragment* getStartScreen();public slots:    /**     * @brief navigateTo     * @param tag имя следующего экрана.     *     * Переход к следующему экрану.     */    void navigateTo(QString tag);    /**     * @brief back     *     * Переход назад по цепочке.     */    void back();    /**     * @brief replace     * @param tag имя экрана на который     *        произойдет замена.     *     * Замена текущего экрана с сохранением     * предыдущей цепочки.     */    void replace(QString tag);    /**     * @brief newRootScreen     * @param tag имя экрана на который     *        произойдет замена.     *     * Замена текущего экрана на новый и сброс     * всей цепочки экранов.     */    void newRootScreen(QString tag);    /**     * @brief navigateWhithData     * @param model     *     * Тот же navigateTo но с данными.     */    void navigateWhithData(QString tag, BaseModel* model);};

Во время смены экранов происходит немного магии с их заменой. При создании фрагмента его сигналы нужно прикрепить к слотам навигатора. При смене фрагмента текущий нужно открепить от слотов и прикрепить новый. connectFragment присоединяет все слоты к сигналам, после этого навигатор управляется этим фрагментом. disconnectFragment открепляет все сигналы. createAndConnect создает фрагмент по его имени через фабрику и сразу прикрепляет его к навигатору. getStartScreen создает стартовый экран по имени, указанному в фабрике, и прикрепляет его к навигатору.

BaseFragment* Navigator::getStartScreen() {    return createAndConnect(this->screensFactory->createStart());}void Navigator::connectFragment(BaseFragment *fragment) {    connect(fragment, &BaseFragment::back, this, &Navigator::back);    connect(fragment, &BaseFragment::replace, this, &Navigator::replace);    connect(fragment, &BaseFragment::navigateTo, this, &Navigator::navigateTo);    connect(fragment, &BaseFragment::newRootScreen, this, &Navigator::newRootScreen);    connect(fragment, &BaseFragment::navigateWhithData, this, &Navigator::navigateWhithData);}void Navigator::disconnectFragment(BaseFragment *fragment) {    disconnect(fragment, &BaseFragment::back, this, &Navigator::back);    disconnect(fragment, &BaseFragment::replace, this, &Navigator::replace);    disconnect(fragment, &BaseFragment::navigateTo, this, &Navigator::navigateTo);    disconnect(fragment, &BaseFragment::newRootScreen, this, &Navigator::newRootScreen);    disconnect(fragment, &BaseFragment::navigateWhithData, this, &Navigator::navigateWhithData);}BaseFragment* Navigator::createAndConnect(QString tag) {    BaseFragment *fragment = this->screensFactory->create(tag);    connectFragment(fragment);    return fragment;}

В конструкторе сохраняем ссылки на контейнер и фабрику, создаем стартовый фрагмент и кладем его в контейнер.

Navigator::Navigator(        QStackedWidget *container,        BaseScreensFactory *screensFactory) {    this->screensFactory = screensFactory;    this->currentContainer = container;    BaseFragment* startFragment = getStartScreen();    this->stack.append(startFragment);    currentContainer->addWidget(stack.last());    currentContainer->setCurrentIndex(0);}

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

void Navigator::navigateTo(QString tag) {    BaseFragment *newFragment = this->screensFactory->create(tag);    stack.last()->onPause();    disconnectFragment(stack.last());    connectFragment(newFragment);    stack.append(newFragment);    currentContainer->addWidget(newFragment);    currentContainer->setCurrentWidget(newFragment);}

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

MainWindow::MainWindow(QWidget *parent)    : QMainWindow(parent){       try {        container = new QStackedWidget;        this->factory = new ScreensFactory;        this->navigator = new Navigator(                    this->container,                    this->factory        );        this->setCentralWidget(container);    } catch (std::exception& e) {        qDebug("%s", e.what());    }}

Взгляд со стороны

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

Мне мое решение очень помогло в реализации проекта. Я смог ускорить разработку и разделить код.

Всем дочитавшим спасибо, надеюсь кому-то это поможет.

Вот ссылка GitHub проекта

Подробнее..

Новый язык программирования Relax

04.04.2021 14:14:58 | Автор: admin

Вступление

Всем привет, я являюсь автором языка программирования Relax. На данный момент я разрабатываю RVM(RelaxVirtualMachine) И Relasm(Relax Assembly). Первые попытки сделать свой язык начались в конце лета 2020, тогда я и не думал что делать язык - это так сложно. Сам же проект Relax начался 30 декабря 2020 года. Прошло полтора месяца, а на нем уже можно написать что-нибудь простенькое.

первое лого языкапервое лого языка

Как компилировать код?

Начнем с того, что файлы relasm лучше сохранять с расширением .rasm, файлы байт-кода - .ree. Для того чтобы скомпилировать и запустить код нужно скачать 3 файла: Relasm.exe, RelaxVM.exe, QtCore.dll. Сделать вы это сможете вот по этим ссылкам: https://github.com/UnbelievableDevelopmentCompany/RVM/tree/master/x64/Release
https://github.com/UnbelievableDevelopmentCompany/Relasm/tree/master/x64/Release

После того как скачали, желательно добавить эти 3 файла в любую папку, которая есть в переменной PATH(или же создать новую папку). Далее в cmd переходим в папку с программой на Relasm и вводим следующие команды:

Relasm main.rasm program.reeRelaxVM program.ree

Первая команда компилирует relasm в байт-код, а вторая уже запускает программу.

Примеры кода на Relasm

Как же выглядит код на Relasm?

mclass MainClassmethod public static void MainClass.Main():.maxstack 1push.str "hello world"callm std static Relax.Console.Write(Relax.String)

Это самая простая программа - hello world! Давайте пройдемся по коду. Первая строчка создает главный класс, в котором обязана быть функция Main(начало выполнения). Во второй строчке мы как раз таки создаем этот метод. Следующие строчки - это тело метода, так как пишутся с табуляцией в начале. Третья строчка кода указывает, что максимальное количество объектов, которые могут находится на стеке равно 1. Четвертая строчка кода добавляет строку "hello world" в стек. Ну и наконец пятая строчка вызывает метод вывода строки на консоль. Строка берется из стека, как и любые другие аргументы в Relasm. Я не буду подробно останавливаться на каждой детали в этом коде.

Хорошо, мы написали hello world, теперь можно что-нибудь по серьёзнее.

mclass MainClassmethod public static void MainClass.Main():.maxstack 2; Объявление переменныхlocal firstNum Relax.Int32local secondNum Relax.Int32local result Relax.Int32local op Relax.String; Получение первого числаcallm std static Relax.Console.Read()callm std static Relax.Converter.StringToInt32(Relax.String)set firstNum; Получение знака операцииcallm std static Relax.Console.Read()set op; Получение второго числаcallm std static Relax.Console.Read()callm std static Relax.Converter.StringToInt32(Relax.String)set secondNum; Проверки на знаки операций; Проверка на сложениеget oppush.str "+"callm std instance Relax.String.operator==(Relax.String)jmpif opAdd; Проверка на вычитаниеget oppush.str "-"callm std instance Relax.String.operator==(Relax.String)jmpif opSub; Проверка на произведениеget oppush.str "*"callm std instance Relax.String.operator==(Relax.String)jmpif opMul; Проверка на делениеget oppush.str "/"callm std instance Relax.String.operator==(Relax.String)jmpif opDivopAdd: ; Сумма чиселget firstNumget secondNumaddset resultjmp endopSub: ; Разность чиселget secondNumget firstNumsubset resultjmp endopMul: ; Произведение чиселget firstNumget secondNummulset resultjmp endopDiv: ; Деление чиселget secondNumget firstNumdivset resultjmp endend: ; вывод результата на экранpush.str "\nResult: "callm std static Relax.Console.Write(Relax.String)get resultcallm std static Relax.Console.Write(Relax.Int32)

Это простой калькулятор. Сначала мы создаем все переменные. Затем считываем данные с консоли. Далее определяем какую операцию нужно выполнять и в зависимости от этого переходим на нужную метку. В каждой метке операции мы получаем 2 числа, выполняем определенную операцию устанавливаем результат в переменную result и переходим в метку end, в которой мы выводим результат в консоль.

Теперь давайте сделаем свой собственный метод.

mclass MainClassmethod public static void MainClass.Main():.maxstack 2; Помещаем аргументы для нашего метода на стекpush.int32 10push.str "Result - "; Вызываем методcallm usr static MainClass.StringPlusInt32(Relax.String, Relax.Int32); Возвращаемый результат выводим на консольcallm std static Relax.Console.Write(Relax.String)method public static Relax.String MainClass.StringPlusInt32(Relax.String str, Relax.Int32 num):.maxstack 2get numcallm std static Relax.Converter.Int32ToString(Relax.Int32) ; конвертируем число в строкуget strcallm std instance Relax.String.Concat(Relax.String) ; добавляем в переменной str конвертированное значениеreturn ; возвращаем результат

Метод StringPlusInt32 нужен для того, чтобы конкатенировать строку и число, для этого мы преобразуем число в строку при помощи метода Relax.Converter.Int32ToString и конкатенируем параметр str с числом, преобразованным в строку. И возвращаем результат при помощи инструкции return. Далее в методе Main просто выводим этот результат в консоль.

Вывод

Relax'у всего лишь полтора месяца, а он уже такое может. Он будет развиваться еще долго. Но даже сейчас можно писать простенькие консольные программы.

Репозиторий виртуальной машины(там есть документация relasm) - https://github.com/UnbelievableDevelopmentCompany/RVM

Репозиторий компилятора Relasm - https://github.com/UnbelievableDevelopmentCompany/Relasm

Пакет для sublime text 3 - RelasmST3Package

Подробнее..
Категории: C++ , Qt , Assembler , Байт-код , Relax , Relasm , Яп , Pl , Udc , Lofectr

Из первых уст. Про впечатления от курса Яндекс Практикума Разработчик С

05.04.2021 20:15:29 | Автор: admin

Приветствую уважаемое сообщество.

В последнее время стало появляться множество курсов, связанных с IT. Вполне логично, что народ стал делиться своими наблюдениями от их прохождения. Так на Хабре можно найти отзывы об обучении на некоторых факультетах (курсах) от Яндекс Практикума [1-3]. Однако про курс "С++ разработчик" такой информации еще не было, был только рекламный пост о его запуске [4], после которого я туда и вписался, попав в первый поток (когорту).

На момент публикации большая часть курса позади, осталось пройти 3 спринта и защитить дипломный проект, поэтому есть что рассказать, как говориться, от первого лица.

О первом лице

Мне 38 лет и последние 12 лет (после защиты кандидатской) работаю в одном из научных учреждений Санкт-Петербурга. До этого успел немного поработать в IT (выездной сисадмин, установка торгового оборудования). Опыта коммерческой разработки нет. Зато есть небольшой опыт разработки программ для управления научными приборами на Delphi.

Мотивация

Некоторое время назад пришло осознание, что в науке важную роль играет высокотехнологичное оборудование, на котором можно проводить конкурентоспособные исследования со всеми вытекающими плюшками - в виде публикаций, конференций и грантов. Однако все течет, все изменяется и нашу организацию в ближайшее время постигнут серьезные трансформации, в том числе ожидается потенциальная потеря прямого доступа к оборудованию. Теоретик из меня не очень, зато с детства имеется тяга к программированию, точнее, после того как отец впервые принес домой Спектрум с бейсиком. Мы тогда с братом дрались за право посидеть за компьютером - он поиграть, я - разобраться как самому нарисовать квадрат и с помощью джойстика перемещать его по экрану. Может кто помнит - прилагалась еще толстая книжка с описанием команд Бейсика и примерами программ. И это в перерывах между паянием электронных схем и выращиванием кристаллов медного купороса на батарее. Так вот, поразмыслив что делать дальше, решил попробовать себя в настоящем программировании. Прямо так с нуля, без опыта работы. Терпения и настойчивости имеется в избытке. С логическим мышлением тоже все в порядке, в науке без него никак.

Опущу тему про почему выбор пал на Яндекс Практикум, самое интересное - что же там внутри.

Что внутри

Про тренажер, про спринты, наставников, дедлайны все уже было здесь на Хабре расписано вдоль и поперек [4-6]. Я даже догадываюсь о возможных причинах написания тех публикаций (только некоторых). Нам сейчас пообещали взамен на отзыв один из подарков на выбор. Очень хочу получить Яндекс.Носки. Не знаю пока что это и как выглядит, но представляю, что это носки из спец шерсти, которая акупунктурно воздействует на определенные точки стопы так, что в мозгу стимулируются области, ответственные за мыслительный процесс.

Как и наверно в большинстве онлайн-курсов все начинается с вводного бесплатного курса, где можно оценить свои силы, время, удобство и стиль изложения материала. Что мне больше всего понравилось, финальным проектом вводного курса был вполне законченный проект "поисковик по поиску потерянных домашних животных" с ранжированием результатов выдачи и стоп-словами. Хотя в описании вводного курса было указано ориентировочное время прохождения 24 часа, я его решал целый месяц во время отпуска. Зато на основе готового поисковика, пока ждал начала уже платного обучения, довольно легко сделал программу "Любимая фраза классиков литературы", куда вошло большинство изученных алгоритмов и приемов. Таким образом, на вводном курсе были даны базовые типы, строки, вектор, словарь, немного шаблонов и лямбда функции. Подача материала мне понравилась, она была выполнена в виде комикса с некоторой историей и юмором.

Главные герои вводного курсаГлавные герои вводного курса

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

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

Описывать подробно каждый спринт не буду, сейчас идет 14-й по счету. Расскажу только про самые яркие на мой взгляд моменты.

Возможно везде так устроено, но что меня больше всего поразило, что обучение тут построено по принципу Panic Driven Education. В начале каждого нового спринта читая очередное задание впадаешь в шок от того, что это нереально сделать, тем более в такой срок. Затем начинаешь потихоньку писать код, общаешься с ребятами, с наставником, интернет в помощь и к концу спринта смотришь так со стороны - "а было совсем и не сложно". И так по нарастающей от спринта к спринту. Примерно к середине курса уже с трудом стал укладываться в срок, нагрузка возросла, задания стали на порядок сложнее. Недавно один из спринтов по просьбе большинства пришлось расширить до трех недель. Но это все общие моменты.

Что конкретно

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

Страничка моей лабораторииСтраничка моей лаборатории

По ходу обучения мы разработали свои версии стандартного вектора. Сначала детский вариант - на основе std::array, затем, вот только что, с использованием размещающего оператора new и инициализацией объектов в сырой памяти, по количеству копирований и перемещений не уступающий std::vector.

Второй крупный проект, который мы уже заканчиваем - это городской маршрутизатор. Все началось невинно, с разработки справочника остановок и маршрутов автобусов, однако очень скоро программа обросла функцией генерации карты маршрутов в SVG, работой с JSON для ввода и вывода данных, используя библиотеки которые сами же и разработали. Самая жесть была с построением оптимального маршрута между остановками с использованием графа, библиотеку которого к счастью нам уже дали готовую и надо было всего лишь разобраться с ее API. Из освоенных технологий: умные указатели, move-семантика, полиморфизм и наследование, RAII, variadic templates. Если в начале курса в тренажер для проверки приходилось загружать единственный файл main.cpp, то последний проект состоял из 24 файлов с самописными библиотеками, пространствами имен и пр.

До конца обучения остается 6 недель, из 28 студентов на нашем первом потоке осталось 12. Остальные ребята дорешивают то, что не успели с последующими потоками. Здесь допускается два таких перехода. Есть мягкие дедлайны и жесткие. Не уложился в жесткий - переходи на следующий поток.

Про проблемы тренажера

Да, было дело, в самом начале. Сделал все как в условии, задание не засчитывается, хоть ты тресни. В большинстве случаев дело было в последнем символе - либо перенос строки нужно было добавить либо пробел один убрать. Мы даже иногда угадывали. Но эти детские проблемы ко второму спринту уже починили. Единственный серьезный глюк был в недавнем задании, где тренажер слишком строго определял правильность формирования svg-файла в плане порядка тегов. Пришлось лишний день потратить на поиск возможных ошибок в своем коде, которых к счастью не оказалось. Зато нисколько не жалею, т.к. более глубоко проникся некоторыми моментами языка и оптимизировал все что можно было. Да, еще, в некоторых последних заданиях скорость работы решения проверяется наравне с корректностью алгоритма. Решать задачи становится сложнее, но интереснее.

Про портфолио

Как и было обещано, по завершению курса у каждого студента будет портфолио из решенных задач. По сути это итоговые работы каждого из спринтов. А конкретно, это многопоточный поисковый сервер, городской маршрутизатор, две реализации вектора, ну и в промежутках - реализация библиотеки для работы с json, рендеринг в svg формат, paginator и работа с графом. Возможно что-то еще вдруг неожиданно появится в оставшихсяспринтах.

Мой репозитарий с решенными задачамиМой репозитарий с решенными задачами

Про ревьюеров

Тренажеру как бездушной машине все равно, лишь бы сигнатуры функций совпадали, да на выходе было ожидаемое значение. Так и было по завершении бесплатного вводного курса. Неформатированная простыня дублирующегося кода с однобуквенными переменными и кучей копирования вместо передачи по ссылке. Это была одна из причин шока, когда после небольшого перерыва понадобилось добавить новый функционал в код. Благо дальше все наши итоговые проекты отправлялись на рецензирование ревьюеру - живому человеку из реальных разработчиков Яндекса. За каждым студентом у нас в потоке с самого начала был закреплен конкретный ревьюер, что логично, дабы все время быть на одной волне. В одном из спринтов мы - студенты сами рецензировали решения друг друга. Было довольно интересно сравнивать свою реализацию с чужой, да еще давать комментарии и замечания.

Про нагрузку

На вводном вебинаре на мой вопрос сколько часов в день в среднем придется уделять учебе чтобы уложиться в дедлайны, я услышал ответ - порядка 4 часов в день. Реально в среднем примерно так и оказалось. Но это в среднем. Поначалу были простые задания и большинство ребят успевало отправить ревьюеру итоговые работы на неделю раньше. Как началась собственная разработка вектора, пошли итераторы, класс внутри класса, тогда все резко поменялось. Стало катастрофически не хватать времени ни на что. По пути на работу читаешь теорию и описание нового задания. В перерыве на обед делаешь наброски будущего решения, по пути домой крутишь в голове варианты реализации конкретных частей кода. Дома до часа или двух ночи отлаживаешь и тестируешь решение. И так снова и снова, пока не дойдешь до итогового задания. Т.е. телевизор, сериалы, игры можно сразу отбросить. Домашних тоже нужно морально подготовить, чтобы на 9 месяцев относились с пониманием. Не знаю, наверное можно было бы снизить нагрузку, но тогда пришлось бы растянуть программу на больший срок. Там столько всего интересного оказывается придумали в языке, что до сих пор поражаюсь А что, так можно было?.

Про трудоустройство

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

Что не хватает

Обязательной практики в реальной компании. Читая Хабр можно лишь отдаленно представить, чем занимаются начинающие разработчики в реальных компаниях. То, что есть дедлайны и спринты? Да, к этому готовят. Разбираться в чужом коде? Да, практически в каждом задании в тренажере дается код-заготовка и нужно понять и расширить его функционал. Уметь работать с GIT? Все итоговые работы мыотправляем ревьюеру через репозитарий на Github. Умение общаться с такими же новичками и опытными специалистами? Тоже да. На курсе все общение построено через Slack, все друг другу помогают советами, иногда всем миром пытаемся понять, что еще не нравится тренажеру, чтобы он засчитал задание.

Насчет типов задач, которые даются в тренажере. Практически не встречались задачи на алгоритмы, о которых недавно рассказывали тут в теме про собеседования в Яндекс [7]. Все задачи имеют адекватную связь с реальностью. Разве что при работе над темой про умные указатели мы клонировали октопусов. Знаете как называется осьминог с 4 щупальцами? Квадропус. А осьминог без щупалец? Колобок. Так авторы задания шутливо давали имена переменным в заготовке кода.

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

Личные впечатления

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

Список источников:

[1] Что вас ждет на курсе Алгоритмов в Яндекс.Практикуме

[2] Опыт обучения из первых рук. Яндекс.Практикум Аналитик данных

[3] Яндекс.Практикум Аналитик данных. Окончание обучения

[4] C++ в Практикуме. Как обучить студентов плюсам, не отпугивая

[5] Веб-тренажёр Яндекс.Практикума. Как всё устроено

[6] Что вас на самом деле ждёт на курсе про алгоритмы в Яндекс.Практикуме

[7] Собеседование в Яндекс: театр абсурда :/

Подробнее..

Разработка firmware на С словно игра в бисер. Как перестать динамически выделять память и начать жить

07.04.2021 10:06:45 | Автор: admin

C++ is a horrible language. It's made more horrible by the fact that a lotof substandard programmers use it, to the point where it's much mucheasier to generate total and utter crap with it.

Linus Benedict Torvalds

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

А firmware мы пишем на C++! мой будущий коллега заулыбался и откинулся в кресле, ожидая моей реакции на свою провокативную эскападу.

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

У вас есть какие-то опасения? поспешил спросить он с искренней озабоченностью в голосе.

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

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

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

И у нас есть код ревью! встрепенувшись, поспешили добавить хором двое других невероятно квалифицированных члена команды.

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

IAR

Так уж получилось, что мы впервые встретились на этом проекте. "Ну, это же специальный компилятор для железок", наивно думал я, "сработаемся". Не скажу, что я жестоко ошибся и проклял тот день, но использование именно этого компилятора доставляет определенный дискомфорт. Дело в том, что в проекте уже начали внедрение относительно нового стандарта С++17. Я уже потирал потные ладошки, представляя, как перепишу вон то и вот это, как станет невероятно красиво, но IAR может охладить пыл не хуже, чем вид нововоронежской Аленушки.

Новый стандарт реализован для нашего любимого коммерческого компилятора лишь частично, несмотря на все заверения о поддержке всех возможностей новейших стандартов. Например, structured binding declaration совсем не работает, сколько не уговаривай упрямца. Еще IAR весьма нежен и хрупок, какая-нибудь относительно сложная конструкция может довести его до истерики: компиляция рухнет из-за некой внутренней ошибки. Это самое неприятное, поскольку нет никаких подсказок, по какой причине все так неприятно обернулось. Такие провалы огорчают даже сильнее финала Игры престолов.

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

SIL

Для некоторых классов устройств существует такое понятие, как стандарты SIL. Safety integrity level уровень полноты безопасности, способность системы обеспечивать функциональную безопасность.

Проще говоря, если от вашего устройства зависят жизни людей, то при его разработке нужно придерживаться определенных правил. Одно из них это отсутствие динамического распределения памяти, по крайней мере, после вызова функции main. Думаю, все знают, что в no-OS устройствах динамические аллокации чреваты проблемами, вроде фрагментации памяти и т.п.

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

std::exception

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

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

__cxa_allocate_exception

Название у нее уже какое-то нехорошее, и действительно, выделяет память для объекта исключения и делает это весьма неприятным образом прямо в куче. Вполне возможно эту функцию подменить на собственную реализацию и работать со статическим буфером. Если не ошибаюсь, то в руководстве для разработчиков autosar для с++14 так и предлагают делать. Но есть нюансы. Для разных компиляторов реализация может отличаться, нужно точно знать, что делает оригинальная функция, прежде чем грубо вмешиваться в механизм обработки. Проще и безопаснее от исключений отказаться вовсе.Что и было сделано, и соответствующий флаг гордо реет теперь над компилятором! Только вот стандартную библиотеку нужно будет использовать вдвойне осторожнее, поскольку пересобрать ее с нужными опциями под IAR возможности нет.

std::vector

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

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

template <class T, std::size_t Size>class StaticArray { using ssize_t = int;public: using value_type = T; template <class U> struct rebind {   using other = StaticArray<U, Size>; }; StaticArray() = default; ~StaticArray() = default; template <class U, std::size_t S> StaticArray(const StaticArray<U, S>&); auto allocate(std::size_t n) -> value_type*; auto deallocate(value_type* p, std::size_t n) -> void; auto max_size() const -> std::size_t;};

Ключевые функции, конечно, allocate и deallocate. Передаваемый им параметр n это не размер в байтах, а размер в попугаях, которые хранятся в векторе. Функция max_size используется при проверке вместимости аллокатора и возвращает максимально возможное теоретически число, которое можно передать в функцию allocate.

Тут очевиднейший пример использования аллокатора
std::vector<int, StaticArray<int, 100>> v;    v.push_back(1000);std::cout<<"check size "<<v.size()<<std::endl;    v.push_back(2000);std::cout<<"check size "<<v.size()<<std::endl;

Результат выполнения такой программы (скомпилировано GCC) будет следующий:

max_size() -> 100

max_size() -> 100

allocate(1)

check size 1

max_size() -> 100

max_size() -> 100

allocate(2)

deallocate(1)

check size 2

deallocate(2)

std::shared_ptr

Умные указатели, безусловно, хорошая вещь, но нужная ли в bare metal? Требование безопасности, запрещающее динамическую аллокацию памяти, делает использование умных указателей в этой области крайне сомнительным мероприятием.

Конечно, контролировать управление памятью путем использования кастомных аллокаторов вполне возможно. В стандартной библиотеке есть замечательная функция std::allocate_shared, которая создаст разделяемый объект именно там, где мы укажем. Указать же можно самолепным аллокатором примерно такого вида:

template <class Element,           std::size_t Size,           class SharedWrapper = Element>class StaticSharedAllocator {  public:  static constexpr std::size_t kSize = Size;  using value_type = SharedWrapper;  using pool_type = StaticPool<Element, kSize>;  pool_type &pool_;  using ElementPlaceHolder = pool_type::value_type;  template <class U>  struct rebind {    using other = StaticSharedAllocator<Element, kSize, U>;  };  StaticSharedAllocator(pool_type &pool) : pool_{pool} {}  ~StaticSharedAllocator() = default;  template <class Other, std::size_t OtherSize>  StaticSharedAllocator(const StaticSharedAllocator<Other, OtherSize> &other)     : pool_{other.pool_} {}  auto allocate(std::size_t n) -> value_type * {    static_assert(sizeof(value_type) <= sizeof(ElementPlaceHolder));    static_assert(alignof(value_type) <= alignof(ElementPlaceHolder));    static_assert((alignof(ElementPlaceHolder) % alignof(value_type)) == 0u);      return reinterpret_cast<value_type *>(pool_.allocate(n));  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(reinterpret_cast<value_type *>(p), n);  }};

Очевидно, Element тип целевого объекта, который и должен храниться как разделяемый объект. Size максимальное число объектов данного типа, которое можно создать через аллокатор. SharedWrapper это тип объектов, которые будут храниться в контейнере на самом деле!

Конечно, вы знаете, что для работы shared_ptr необходима некоторая дополнительная информация, которую нужно где-то хранить, лучше прямо с целевым объектом вместе. Поэтому для этого аллокатора очень важна структура rebuild. Она используется в недрах стандартной библиотеки, где-то в районе alloc_traits.h, чтобы привести аллокатор к виду, который необходим для работы разделяемого указателя:

using type = typename _Tp::template rebind<_Up>::other;

где _Tp это StaticSharedAllocator<Element, Size>,

_Up это std::_Sp_counted_ptr_inplace<Object, StaticSharedAllocator<Element, Size>, __gnu_cxx::_S_atomic>

К сожалению, это верно только для GCC, в IAR тип будет немного другой, но общий принцип неизменен: нам нужно сохранить немного больше информации, чем содержится в Element. Для простоты тип целевого объекта и расширенный тип должны быть сохранены в шаблонных параметрах. Как вы уже догадались, SharedWrapper и будет расширенным типом, с которым непосредственно работает shared_ptr.

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

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

Еще немного кода для иллюстрации

Сам пул объектов основан на StaticArray аллокаторе. А чего добру пропадать?

template <class Type, size_t Size>struct StaticPool {  static constexpr size_t kSize = Size;  static constexpr size_t kSizeOverhead = 48;  using value_type = std::aligned_storage_t<sizeof(Type)+kSizeOverhead,                                             alignof(std::max_align_t)>;  StaticArray<value_type, Size> pool_;    auto allocate(std::size_t n) -> value_type * {    return pool_.allocate(n);  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(p, n);  }};

А теперь небольшой пример, как это все работает вместе:

struct Object {  int index;};constexpr size_t kMaxObjectNumber = 10u;StaticPool<Object, kMaxObjectNumber> object_pool {};StaticSharedAllocator<Object, kMaxObjectNumber> object_alloc_ {object_pool};std::shared_ptr<Object> MakeObject() {  return std::allocate_shared<Object>(object_alloc_);}

Все эти изуверства позволили избежать динамического выделения памяти и здесь. Хотя, мне кажется, лучше реорганизовать код так, чтоб не было необходимости использовать shared_ptr вообще. К сожалению, не всегда рефакторинг можно сделать быстро и безболезненно.

std::function

Универсальная полиморфная обертка над функциями или функциональными объектами. Очень удобная штука. Точно была бы полезна в embedded проекте, хотя бы для каких-нибудь функций обратного вызова (callbacks).

Чем мы платим за универсальность?

Во-первых, std::function может использовать динамическую аллокацию памяти.

Небольшой и несколько искусственный пример:

int x[] = {1, 2, 3, 4, 5};    auto sum = [=] () -> int {      int sum = x[0];      for (size_t i = 1u; i < sizeof(x) / sizeof(int); i++) {        sum += x[i];      }      return sum;    };        std::function<int()> callback = sum; 

Когда элементов массива 5, то размер функции 20 байт. В этом случае, когда мы присваиваем переменной callback экземпляр нашей лямбда-функции, будет использована динамическая аллокация.

Дело в том, что в классе нашей универсальной обертки содержится небольшой участок памяти (place holder), где может быть определена содержащаяся функция.

Любая функция в С++ может быть определена с помощью двух указателей максимум. Для свободных функций или функторов достаточно одного указателя, если нужно вызвать метод класса, то нужен указатель на объект и смещение внутри класса. Собственно, у нас есть небольшое укромное местечко для пары указателей. Конечно, небольшие функциональные объекты можно хранить прямо на месте этих указателей! Если размер лямбды, например, не позволяет целиком запихать ее туда, то на помощь снова придет динамическая аллокация.

Для GCC

Опции -specs=nano.specs уже не будет хватать для std::function.

Сразу появится сообщения подобного вида:

abort.c:(.text.abort+0xa): undefined reference to _exit

signalr.c:(.text.killr+0xe): undefined reference to _kill

signalr.c:(.text.getpidr+0x0): undefined reference to _getpid

Правильно, ведь пустая функция должна бросать исключение.

Нужна другая опция -specs=nosys.specs, где включены все необходимые заглушки для всяких системных функций.

Соберем небольшую прошивку, чтоб проверить как повлияет включение std::function на потребление памяти различных видов. Прошивка стандартный пример от ST для подмигивающего светодиода. Изменения в размере секций файла-прошивки в таблице:

text

data

bss

67880

2496

144

Невооруженным взглядом видно, что секция .text выросла просто фантастически (на 67Кб!). Как одна функция могла сделать такое?

Внутри std::function явно вызываются исключения, которые внутри себя используют demangling для имен функций, и это дорогое удовольствие. Нужно ли это в небольших устройствах? Скорее всего, любоваться красивыми именами не придется.

Если заглянуть в получившийся elf-файл, то можно увидеть много новых символов. Отсортируем их по размеру и посмотрим на самые жирные.

00000440cplus_demangle_operators0000049e__gxx_personality_v0000004c4 d_encoding000004fed_exprlist00000574_malloc_r0000060cd_print_mod000007f0d_type00000eec_dtoa_r00001b36_svfprintf_r0000306cd_print_comp

Много функций с префиксом d_* функции из файла cp-demangle.c библиотеки libiberty, которая, как я понимаю, встроена в gcc, и не так просто выставить ее за дверь.

Также имеются функции для обработки исключений (bad_function_call, std::unexpected, std::terminate)

_sbrk, malloc, free функции для работы с динамическим выделением памяти.

Результат ожидаемый флаги -fno-exceptions и -fno-rtti не спасают.

Внедрим второй подобный функциональный объект в другой единице трансляции:

text

data

bss

67992

2504

144

Вторая std::function обошлась не так уж и дорого.

Показательно также то, сколько объектных файлов и из каких библиотек мы используем для этих случаев.

Для случая без std::function список короткий
libc_nano.alibg_nano.alibg_nano.a(lib_a-exit.o)libg_nano.a(lib_a-exit.o) (_global_impure_ptr)libg_nano.a(lib_a-impure.o)libg_nano.a(lib_a-init.o)libg_nano.a(lib_a-memcpy-stub.o)libg_nano.a(lib_a-memset.o)libgcc.alibm.alibstdc++_nano.a
Для случая с std::function список гораздо длиннее
libc.alibg.alibg.a(lib_a-__atexit.o)libg.a(lib_a-__call_atexit.o)libg.a(lib_a-__call_atexit.o) (__libc_fini_array)libg.a(lib_a-__call_atexit.o) (atexit)libg.a(lib_a-abort.o)libg.a(lib_a-abort.o) (_exit)libg.a(lib_a-abort.o) (raise)libg.a(lib_a-atexit.o)libg.a(lib_a-callocr.o)libg.a(lib_a-closer.o)libg.a(lib_a-closer.o) (_close)libg.a(lib_a-ctype_.o)libg.a(lib_a-cxa_atexit.o)libg.a(lib_a-cxa_atexit.o) (__register_exitproc)libg.a(lib_a-dtoa.o)libg.a(lib_a-dtoa.o) (_Balloc)libg.a(lib_a-dtoa.o) (__aeabi_ddiv)libg.a(lib_a-exit.o)libg.a(lib_a-exit.o) (__call_exitprocs)libg.a(lib_a-exit.o) (_global_impure_ptr)libg.a(lib_a-fclose.o)libg.a(lib_a-fflush.o)libg.a(lib_a-findfp.o)libg.a(lib_a-findfp.o) (__sread)libg.a(lib_a-findfp.o) (_fclose_r)libg.a(lib_a-findfp.o) (_fwalk)libg.a(lib_a-fini.o)libg.a(lib_a-fputc.o)libg.a(lib_a-fputc.o) (__retarget_lock_acquire_recursive)libg.a(lib_a-fputc.o) (__sinit)libg.a(lib_a-fputc.o) (_putc_r)libg.a(lib_a-fputs.o)libg.a(lib_a-fputs.o) (__sfvwrite_r)libg.a(lib_a-freer.o)libg.a(lib_a-fstatr.o)libg.a(lib_a-fstatr.o) (_fstat)libg.a(lib_a-fvwrite.o)libg.a(lib_a-fvwrite.o) (__swsetup_r)libg.a(lib_a-fvwrite.o) (_fflush_r)libg.a(lib_a-fvwrite.o) (_free_r)libg.a(lib_a-fvwrite.o) (_malloc_r)libg.a(lib_a-fvwrite.o) (_realloc_r)libg.a(lib_a-fvwrite.o) (memchr)libg.a(lib_a-fvwrite.o) (memmove)libg.a(lib_a-fwalk.o)libg.a(lib_a-fwrite.o)libg.a(lib_a-impure.o)libg.a(lib_a-init.o)libg.a(lib_a-isattyr.o)libg.a(lib_a-isattyr.o) (_isatty)libg.a(lib_a-locale.o)libg.a(lib_a-locale.o) (__ascii_mbtowc)libg.a(lib_a-locale.o) (__ascii_wctomb)libg.a(lib_a-locale.o) (_ctype_)libg.a(lib_a-localeconv.o)libg.a(lib_a-localeconv.o) (__global_locale)libg.a(lib_a-lock.o)libg.a(lib_a-lseekr.o)libg.a(lib_a-lseekr.o) (_lseek)libg.a(lib_a-makebuf.o)libg.a(lib_a-makebuf.o) (_fstat_r)libg.a(lib_a-makebuf.o) (_isatty_r)libg.a(lib_a-malloc.o)libg.a(lib_a-mallocr.o)libg.a(lib_a-mallocr.o) (__malloc_lock)libg.a(lib_a-mallocr.o) (_sbrk_r)libg.a(lib_a-mbtowc_r.o)libg.a(lib_a-memchr.o)libg.a(lib_a-memcmp.o)libg.a(lib_a-memcpy.o)libg.a(lib_a-memmove.o)libg.a(lib_a-memset.o)libg.a(lib_a-mlock.o)libg.a(lib_a-mprec.o)libg.a(lib_a-mprec.o) (_calloc_r)libg.a(lib_a-putc.o)libg.a(lib_a-putc.o) (__swbuf_r)libg.a(lib_a-readr.o)libg.a(lib_a-readr.o) (_read)libg.a(lib_a-realloc.o)libg.a(lib_a-reallocr.o)libg.a(lib_a-reent.o)libg.a(lib_a-s_frexp.o)libg.a(lib_a-sbrkr.o)libg.a(lib_a-sbrkr.o) (_sbrk)libg.a(lib_a-sbrkr.o) (errno)libg.a(lib_a-signal.o)libg.a(lib_a-signal.o) (_kill_r)libg.a(lib_a-signalr.o)libg.a(lib_a-signalr.o) (_getpid)libg.a(lib_a-signalr.o) (_kill)libg.a(lib_a-sprintf.o)libg.a(lib_a-sprintf.o) (_svfprintf_r)libg.a(lib_a-stdio.o)libg.a(lib_a-stdio.o) (_close_r)libg.a(lib_a-stdio.o) (_lseek_r)libg.a(lib_a-stdio.o) (_read_r)libg.a(lib_a-strcmp.o)libg.a(lib_a-strlen.o)libg.a(lib_a-strncmp.o)libg.a(lib_a-strncpy.o)libg.a(lib_a-svfiprintf.o)libg.a(lib_a-svfprintf.o)libg.a(lib_a-svfprintf.o) (__aeabi_d2iz)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpeq)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpun)libg.a(lib_a-svfprintf.o) (__aeabi_dmul)libg.a(lib_a-svfprintf.o) (__aeabi_dsub)libg.a(lib_a-svfprintf.o) (__aeabi_uldivmod)libg.a(lib_a-svfprintf.o) (__ssprint_r)libg.a(lib_a-svfprintf.o) (_dtoa_r)libg.a(lib_a-svfprintf.o) (_localeconv_r)libg.a(lib_a-svfprintf.o) (frexp)libg.a(lib_a-svfprintf.o) (strncpy)libg.a(lib_a-syswrite.o)libg.a(lib_a-syswrite.o) (_write_r)libg.a(lib_a-wbuf.o)libg.a(lib_a-wctomb_r.o)libg.a(lib_a-writer.o)libg.a(lib_a-writer.o) (_write)libg.a(lib_a-wsetup.o)libg.a(lib_a-wsetup.o) (__smakebuf_r)libgcc.alibgcc.a(_aeabi_uldivmod.o)libgcc.a(_aeabi_uldivmod.o) (__aeabi_ldiv0)libgcc.a(_aeabi_uldivmod.o) (__udivmoddi4)libgcc.a(_arm_addsubdf3.o)libgcc.a(_arm_cmpdf2.o)libgcc.a(_arm_fixdfsi.o)libgcc.a(_arm_muldf3.o)libgcc.a(_arm_muldivdf3.o)libgcc.a(_arm_unorddf2.o)libgcc.a(_dvmd_tls.o)libgcc.a(_udivmoddi4.o)libgcc.a(libunwind.o)libgcc.a(pr-support.o)libgcc.a(unwind-arm.o)libgcc.a(unwind-arm.o) (__gnu_unwind_execute)libgcc.a(unwind-arm.o) (restore_core_regs)libm.alibnosys.alibnosys.a(_exit.o)libnosys.a(close.o)libnosys.a(fstat.o)libnosys.a(getpid.o)libnosys.a(isatty.o)libnosys.a(kill.o)libnosys.a(lseek.o)libnosys.a(read.o)libnosys.a(sbrk.o)libnosys.a(write.o)libstdc++.alibstdc++.a(atexit_arm.o)libstdc++.a(atexit_arm.o) (__cxa_atexit)libstdc++.a(class_type_info.o)libstdc++.a(cp-demangle.o)libstdc++.a(cp-demangle.o) (memcmp)libstdc++.a(cp-demangle.o) (realloc)libstdc++.a(cp-demangle.o) (sprintf)libstdc++.a(cp-demangle.o) (strlen)libstdc++.a(cp-demangle.o) (strncmp)libstdc++.a(del_op.o)libstdc++.a(del_ops.o)libstdc++.a(eh_alloc.o)libstdc++.a(eh_alloc.o) (std::terminate())libstdc++.a(eh_alloc.o) (malloc)libstdc++.a(eh_arm.o)libstdc++.a(eh_call.o)libstdc++.a(eh_call.o) (__cxa_get_globals_fast)libstdc++.a(eh_catch.o)libstdc++.a(eh_exception.o)libstdc++.a(eh_exception.o) (operator delete(void*, unsigned int))libstdc++.a(eh_exception.o) (__cxa_pure_virtual)libstdc++.a(eh_globals.o)libstdc++.a(eh_personality.o)libstdc++.a(eh_term_handler.o)libstdc++.a(eh_terminate.o)libstdc++.a(eh_terminate.o) (__cxxabiv1::__terminate_handler)libstdc++.a(eh_terminate.o) (__cxxabiv1::__unexpected_handler)libstdc++.a(eh_terminate.o) (__gnu_cxx::__verbose_terminate_handler())libstdc++.a(eh_terminate.o) (__cxa_begin_catch)libstdc++.a(eh_terminate.o) (__cxa_call_unexpected)libstdc++.a(eh_terminate.o) (__cxa_end_cleanup)libstdc++.a(eh_terminate.o) (__gxx_personality_v0)libstdc++.a(eh_terminate.o) (abort)libstdc++.a(eh_throw.o)libstdc++.a(eh_type.o)libstdc++.a(eh_unex_handler.o)libstdc++.a(functional.o)libstdc++.a(functional.o) (std::exception::~exception())libstdc++.a(functional.o) (vtable for __cxxabiv1::__si_class_type_info)libstdc++.a(functional.o) (operator delete(void*))libstdc++.a(functional.o) (__cxa_allocate_exception)libstdc++.a(functional.o) (__cxa_throw)libstdc++.a(pure.o)libstdc++.a(pure.o) (write)libstdc++.a(si_class_type_info.o)libstdc++.a(si_class_type_info.o) (__cxxabiv1::__class_type_info::__do_upcast(__cxxabiv1::__class_type_info const*, void**) const)libstdc++.a(si_class_type_info.o) (std::type_info::__is_pointer_p() const)libstdc++.a(tinfo.o)libstdc++.a(tinfo.o) (strcmp)libstdc++.a(vterminate.o)libstdc++.a(vterminate.o) (__cxa_current_exception_type)libstdc++.a(vterminate.o) (__cxa_demangle)libstdc++.a(vterminate.o) (fputc)libstdc++.a(vterminate.o) (fputs)libstdc++.a(vterminate.o) (fwrite)

А что IAR?

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

text

ro data

rw data

2958

38

548

О, добавилось всего-то каких-то жалких 3Кб кода! Это успех. Фанат GCC во мне заволновался, почему так мало? Смотрим, что же добавил нам IAR.

Добавились символы из двух новых объектных файлов:

dlmalloc.o 1'404 496

heaptramp0.o 4

Конечно же, появились функции и данные для работы с динамической памятью, как в случае GCC.

Естественно, никаких выделений в куче нет, но IAR приготовился: видно, что он создал структуру gm (global malloc: a malloc_state holds all of the bookkeeping for a space) и некоторые функции для работы с этой структурой.

Объектный файл того юнита, в котором была добавлена функция, тоже ощутимо располнел:

до

main.cpp.obj 3'218 412 36'924

после

main.cpp.obj 4'746 451 36'964

Файл прибавил более 1Кб. Появилась std::function, ее сопряжение с лямбдой, аллокаторы.

Добавление второго такого функционального объекта в другую единицу трансляции дает нам очередной прирост:

text

ro data

rw data

3 998

82

600

Прибавили более 1Кб. Т.е. каждая новая функция добавляет нам по килобайту кода в каждой единице трансляции. Это не слишком помогает экономить: в проекте не один и не два колбэка, больше десятка наберется. Хорошо, что большинство таких функций имеют сигнатуру void(*)(void) или void(*)(uint8_t *, int), мы можем быстро накидать свою реализацию std::function без особых проблем. Что я и сделал.

Убедившись, что моя примитивная реализация function работает и не требует много памяти, перенес ее в проект, заменив все std::function, до которых смог дотянуться. С чувством выполненного долга я отправился на набережную любоваться закатом.

Дома меня поджидало письмо от коллеги, преисполненное благодарности. Он писал, что благодаря отказу от богомерзких std::function проект сильно схуднул, мы все молодцы! Сочившееся из меня самодовольство брызнуло во все стороны. Прилагался также классический рекламно-наглядный график до-после, вопивший об уменьшении размера отладочной версии прошивки аж на 30 процентов. В абсолютных величинах цифра была еще страшнее, это, на минуточку, целых 150 килобайт! Что-о-о-о? Улыбка довольного кота медленно отделилась от лица и стремительным домкратом полетела вниз, пробивая перекрытия. В коде просто нет столько колбэков, чтоб хоть как-то можно было оправдать этот странный феномен. В чем дело?

Смотря на сонное спокойствие темной улицы, раскинувшейся внизу, я твердо решил, что не сомкну глаз, пока не отыщу ответ. Проснувшись утром, в первую очередь сравнил два разных elf-файла: до и после замены std::function. Тут все стало очевидно!

В одном забытом богом и кем-то из разработчиков заголовочном файле были такие строчки:

using Handler = std::function<void()>;static auto global_handlers = std::pair<Handler, Handler> {};

И это в заголовочном файле. Насколько я смог понять, эти функции использовались для создания бледного подобия shared_ptr, увеличивая и уменьшая счетчик ссылок на ресурс в конструкторе и деструкторе соответственно. В некотором роде такой подход даже работал, только для каждой единицы трансляции отдельно. Впрочем, это почти нигде уже не использовалось, но за каждое включение заголовочного файла приходилось платить примерно одним килобайтом памяти. А включений было предостаточно. Да, в релизной версии почти все вырезалось, но в отладочной оптимизация выключена и компилятор честно создавал все эти бесполезные объекты.

Понятно, чего хотел добиться неизвестный мне автор, и это вполне могло получиться. Начиная с 17-го стандарта, в заголовочном файле можно разместить некие глобальные объекты, которые будут видны и в других единицах трансляции. Достаточно вместо static написать inline. Это работает даже для IAR. Впрочем, я не стал изменять себе и просто все убрал.

Вот тут я все же не удержатся от объяснения очевидных вещей

Если у вас несколько единиц трансляции и создание глобального объекта вынесено в заголовочный файл, то при сборке проекта вы неизбежно получите ошибку multiple definition. Ежели добавить static, как сделал неизвестный мне разработчик, то все пройдет гладко, но в итоге будет занято несколько участков памяти и от глобальности ничего не останется.

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

// a.h

#pragma once

int a();

// a.cpp

#include "a.h"

#include "c.hpp"

int a() { return cglob * 2; }

// b.h

#pragma once

int b();

// b.cpp

#include "b.h"

#include "c.hpp"

int b() { return cglob * 4; }

// main.cpp

#include "a.h"

#include "b.h"

int main() { return a() + b(); }

// c.hpp

#pragma once

int c_glob = 0;

Пробуем собрать наш небольшой и бесполезный проект.

$ g++ a.cpp b.cpp main.cpp -o test

/usr/lib/gcc/x8664-pc-cygwin/10/../../../../x8664-pc-cygwin/bin/ld: /tmp/cccXOcPm.o:b.cpp:(.bss+0x0): повторное определение cglob; /tmp/ccjo1M9W.o:a.cpp:(.bss+0x0): здесь первое определение

collect2: ошибка: выполнение ld завершилось с кодом возврата 1

Неожиданно получаем ошибку. Так, теперь меняем содержимое файла c.hpp:

static int c_glob = 0;

Вот теперь все собирается! Полюбуемся на символы:

$ objdump.exe -t test.exe | grep glob | c++filt.exe

[ 48](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000000 c_glob

[ 65](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000010 c_glob

Вот и второй лишний символ, что и требовалось доказать.

А ежели изменить c.hpp таким образом:

inline int c_glob = 0;

Объект c_glob будет единственным, все единицы трансляции будут ссылаться на один и тот же объект.

Вывод будет весьма банален: нужно понимать, что делаешь... и соответствовать стандартам SIL!

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

Всем спасибо, всем удачи!

Подробнее..

Альтернативное собеседование на позицию разработчика ПО

07.04.2021 12:05:29 | Автор: admin

На фоне гремящих здесь на Хабре дискуссий про алгоритмические задачки и собеседования в Яндекс, вспомнил, как один мой приятель, работающий в одно довольно большой (не-российской) компании, как-то раз предлагал мне задачку с собеседований, которые проводят они. Сейчас задачка изменилась (они нашли более крутой пример кода), поэтому с его позволения, публикую старый вариант здесь.

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

Предлагалось сделать следующее:

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

  2. Провести code-review, указать на подозрительные и плохие места и предложить, как можно их улучшить или переделать. Можно задавать любые вопросы и гуглить все что угодно.

Код на C++, хотя большинство ошибок смогут найти даже разработчики на других языках. Код был примерно такой:

class SomeServiceClient{ public:  SomeServiceClient();  virtual ~SomeServiceClient();  bool CallAsync(const std::string& uri,                 const std::string& param,                 const misc::BusServiceClient::ResponseCB& callback);  bool CallSync(const std::string& uri,                const std::string& param,                const misc::BusServiceClient::ResponseCB& callback); private:  misc::BusServiceClient ss_client_;  static const int kSleepMs = 100;  static const int kSleepCountMax = 50;};class SpecificUrlFetcher : public UrlFetcher { public:  SpecificUrlFetcher();  virtual ~SpecificUrlFetcher();  SomeData FetchData(const URL& url, const UrlFetcher::ResponseCB& callback); private:  bool SsResponse_returnValue{false};  char SsResponse_url[1024];  void SsResponseCallback(const std::string& response);  SomeServiceClient* ss_client_;};...static const char ss_getlocalfile_uri[] =    "bus://url_replace_service";namespace net {pthread_mutex_t g_url_change_callback_lock = PTHREAD_MUTEX_INITIALIZER;SomeBusServiceClient::SomeBusServiceClient()    : ss_client_(misc::BusServiceClient::PrivateBus) {}SomeBusServiceClient::~SomeBusServiceClient() {}bool SomeBusServiceClient::CallAsync(    const std::string& uri,    const std::string& param,    const misc::BusServiceClient::ResponseCB& callback) {  bool bRet;  bRet = ss_client_.callASync(uri, param, callback);  return bRet;}bool SomeBusServiceClient::CallSync(    const std::string& uri,    const std::string& param,    const misc::BusServiceClient::ResponseCB& callback) {  boold bRet  bRet = false;  int counter;  pthread_mutex_lock(&g_url_change_callback_lock);   ss_client_.callASync(uri, param, callback);  counter = 0;  for (;;) {    int r = pthread_mutex_trylock(&g_url_change_callback_lock);    if (r == 0) {      bRet = true;      pthread_mutex_unlock(&g_url_change_callback_lock);    } else if (r == EBUSY) {      usleep(kSleepMs);      counter++;      if (counter >= kSleepCountMax) {        pthread_mutex_unlock(&g_url_change_callback_lock);        break;      } else        continue;    }    break;  }  return bRet;}/**************************************************************************/SpecificUrlFetcher::SpecificUrlFetcher() {}SpecificUrlFetcher::~SpecificUrlFetcher() {}void SpecificUrlFetcher::SsResponseCallback(const std::string& response) {  std::unique_ptr<lib::Value> value(lib::JSONReader::Read(response));  if (!value.get() || !value->is_dict()) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  lib::DictionaryValue* response_data =      static_cast<lib::DictionaryValue*>(value.get());  bool returnValue;  if (!response_data->GetBoolean("returnValue", &returnValue) || !returnValue) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  std::string url;  if (!response_data->GetString("url", &url)) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  SsResponse_returnValue = true;  size_t array_sz = arraysize(SsResponse_url);  strncpy(SsResponse_url, url.c_str(), array_sz);  SsResponse_url[array_sz - 1] = 0;  pthread_mutex_unlock(&g_url_change_callback_lock);}SomeData SpecificUrlFetcher::FetchData(const URL& url, const UrlFetcher::ResponseCB& callback) {lib::DictionaryValue dictionary;std::string ss_request_payload;misc::BusServiceClient::ResponseCB response_cb =lib::Bind(&SpecificUrlFetcher::SsResponseCallback, this);SomeBusServiceClient* ss_client_ =new SomeBusServiceClient();dictionary.SetString("url", url.to_string());lib::JSONWriter::Write(dictionary, &ss_request_payload);SsResponse_returnValue = false;SsResponse_url[0] = 0x00;ss_client_->CallSync(ss_getlocalfile_uri, ss_request_payload, response_cb);URL new_url;if (SsResponse_returnValue) {  new_url = URL::from_string(SsResponse_url);}delete ss_client_;return UrlFetcher::FetchData(new_url, callback);}}  // namespace net

Ответы будут под спойлером, нажимайте на него осознанно, пути назад уже нет.

Итак, ответы.
  1. У нас есть какой-то класс UrlFetcher, задача которого, судя по всему -- получать какие-то данные по какому-то URL'у. Унаследованный у него класс делает то же самое, только перед запросом обращается по какой-то шине сообщений к какому-то внешнему сервису, отправляя ему запрошенный URL, и вместо него получает от этого сервиса некий другой URL, который и используется дальше. Этакий паттерн Decorator.

  2. Сначала по мелочам:

    1. ss_getlocalfile_uri - глобальная переменная. Зачем? Можно было объявить ее внутри одного из классов.

    2. В некоторых местах объявляется переменная, а в следущей же строке ей присваеивается значение. Можно совместить.

    3. Странный стиль именования переменных и полей, например SsResponse_returnValue Далее по-серьезнее:

    4. Используется pthread-функции, при том что есть стандартные std::thread, которых в данном случае более чем достаточно.

    5. Используются Си-строки с методами типа strncpy(); по факту тут можно использовать std::string без каких-либо проблем.

    6. ss_client_ хранится в сыром указателе и удаляется вручную. Лучше использовать std::unique_ptr.

    7. Вместо usleep() лучше все-таки использовать std::this_thread::sleep()

    Еще серьезнее:

    8. В цикле в SomeBusServiceClient::CallSync если колбэк с ответом придет менее чем за kSleepMs до kSleepCountMax, то мы откинем ответ и не выполним задачу. Это плохо.

    А теперь еще серьезнее:

    9. Мы отправляем асинхронный запрос в message bus и ждем. Отправленный запрос по истечении таймаута не отменяется. Неизвестно, как работает этот message bus, но если вдруг у класса работы с ним есть какой-то таймаут по умолчанию, то стоит использовать его как kSleepCountMax*kSleepMs, а если ничего такого нет, то нужно как-то отменять уже отправленный запрос когда он нам стал не нужен (возможно callASync возвращает какой-нибудь id запроса?). Потому что если вдруг по какой-то причине ответ придет сильно позже, когда мы уже не ждем, а начали получать следущий URL, то случится полный бардак.

    9. В функции FetchData нет проверки на ошибку, new_url в любом случае передается в метод базового класса, даже если он пустой.

    10. Метод FetchUrl, судя по сигнатуре, изначально асинхронный. В наследуемом классе же по факту из асинхронного метода делается синхронный, потом блокируется до получения ответа, а уже потом вызывает действительно асинхронный метод родительского класса -- WTF? Почему нельзя было сразу сделать все асинхронно?

    11. Судя по логике работы (вызов FetchUrl синхронный и блокирует тред), SsResponseCallback должен выполниться в другом треде. При этом получается, что мы разблокируем мьютекст не в том потоке, где мы его блокировали. Для pthread это явный undefined behavior.

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

Подробнее..

Трансляция кода с C на C работа портера

14.04.2021 14:22:42 | Автор: admin

Привет, Хабр. Некоторое время назад ярассказывало том, как нам удалось наладить ежемесячный выпуск релизов для платформы C++ (на Windows и Linux) библиотек, исходный код которых получается путём автоматической трансляции кода оригинальных продуктов, написанных на C#. Также яписало том, как мы заставили такой транспилированный код выполняться в рамках нативного C++ без сборки мусора.

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


Поколения фреймворка

Прежде чем начать говорить о деталях реализации, мне придётся сказать несколько слов об истории проекта, чтобы стали ясны некоторые отсылки. Как я уже рассказывал в первой статье, наша компания занимается, в основном, разработкой библиотек для платформы .Net на C#. Наша команда разрабатывает и поддерживает решения, предназначенные для выпуска данного кода под другие платформы. При нашем объёме кодовой базы автоматическая трансляция полного кода продукта на целевой язык оказывается существенно дешевле, чем параллельная разработка на нескольких языках. Запуск библиотеки .Net внутри плюсовой оболочки - другой возможный подход, но он также является трудоёмким из-за необходимости корректного проброса всех API, да и работало это не на всех целевых платформах, когда мы начинали.

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

  1. Самый первый портер с C# на Java на основе текстового процессора - устарел, более не используется.

  2. Портер с C# на Java на основе синтаксического анализатора Metaspec - актуален.

  3. Портер с C# на C++ на основе синтаксического анализатора NRefactory - актуален.

  4. Построенный на Roslyn и рефлексии генератор модулей Python, являющихся обёртками над машиной .Net, в которой выполняются оригинальные продукты на C# - актуален.

  5. Портеры с C# на Java и C++ на основе общего фреймворка, построенного на Roslyn - в разработке.

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

  1. Подготовка кода C# к портированию существующими портерами - например, понижение версии языка до той, которая поддерживается синтаксическими анализаторами Metaspec (3.0) и/или NRefactory (5.0).

  2. Анализ кода C# на удовлетворение требованиям, накладываемым процедурами портирования.

  3. Трансляция аспектов кода C#, плохо покрываемых существующими портерами (документация, примеры использования и т. д.).

Архитектура

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

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

Когда такого подхода перестало хватать, была начата разработка первого портера на базе синтаксического анализатора Metaspec. В этой реализации код исходного проекта на C# загружается анализатором и преобразуется в AST-представление (отдельное дерево для каждого файла с исходным кодом). Кроме того, Metaspec строит семантическую модель, позволяющую разрешать имена типов и обращения к членам. Используя эти две модели, портер выполняет две стадии: анализ кода C# и генерацию кода Java. Стадия анализа нужна для поиска нетранслируемых конструкций и вывода соответствующих ошибок и предупреждений, а также для сбора дополнительной информации, влияющей на кодогенерацию. На стадии кодогенерации происходит непосредственное формирование выходного кода.

Каждая стадия предполагает один или несколько проходов по синтаксическому дереву. На этапе анализа нас, как правило, интересуют конкретные синтаксические конструкции - их поиск проще всего осуществляется с использованиемшаблона "Посетитель". При этом поиск различных конструкций легко инкапсулируется в отдельные классы посетителей и выполнятся в виде отдельных проходов по дереву. Кодогенерация, напротив, осуществляется в один проход, при котором вложенные узлы обходятся рекурсивно, а соседние - последовательно, поскольку трансляция конкретного узла зачастую сильно зависит от состава родительских узлов. На обоих этапах дерево кода C# остаётся неизменным.

Архитектура портера с C# на C++, построенного на базе синтаксического анализатора NRefactory несколькими годами позже, во многом подобна описанной выше. После загрузки кода в AST-представление и построения семантической модели по дереву совершается несколько проходов посетителями для сбора предварительной информации, после чего генерируется код C++ - опять же, в один проход. Дерево кода C# остаётся неизменным и в этой модели. Отличия касаются, прежде всего, декомпозиции кода и разделения обязанностей на этапе кодогенерации, хотя полностью изолировать алгоритмы и избавиться от божественных объектов не удалось и на этой итерации.

Недостатком подобного однопроходного подхода к кодогенерации является сильная связность кода, осуществляющего разбор синтаксических конструкций и собственно создание текста программы на целевом языке, поскольку на обработку одного и того же узла может влиять огромное количество факторов. Так, способ генерации кода класса C++ на основе класса C# определяется следующим:

  1. Был ли данный класс исключён из портирования атрибутом или указанием его имени в соответствующем разделе конфигурационного файла.

  2. Является ли данный класс обобщённым типом.

  3. Если да, существуют ли другие классы с тем же полным именем, но другим набором параметров типа (перегрузка по числу аргументов шаблона в C++ не поддерживается, в отличие от C#).

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

  5. Заданы ли для класса или его членов атрибуты, либо заданы ли глобальные опции, влекущие изменение области видимости членов класса.

  6. Является ли хоть один из классов, внешних по отношению к текущему, обобщённым.

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

  8. Является ли класс коллекцией тестов (TestFixture или Theory).

  9. Является ли класс абстрактным.

  10. Заданы ли для класса атрибуты, влекущие его переименование в выходном коде.

  11. Какие базовые типы есть у класса, какие из них удалены или добавлены атрибутами, влияющими на поведение портера.

  12. Заданы ли для обобщённых параметров класса ограничения.

  13. Является ли класс наследником System.Exception.

  14. Удовлетворены ли условия для добавления к классу конструкторов или деструктора, отсутствующих в исходном коде.

  15. Есть ли в базовом классе члены, которые становятся определениями для членов реализуемых данным классом интерфейсов.

  16. Относится ли класс к цепочкам наследования, для которых в коде присутствуют вызовы Clone() или MemberwiseClone(), которые нужно эмулировать отдельно.

  17. Существуют ли условия для добавления к методам выходного класса перегрузок, отсутствующих в исходном классе.

  18. Зависят ли инициализаторы констант класса друг от друга.

  19. Включена ли для данного класса (или для всех классов) поддержка рефлексии.

  20. Есть ли у класса комментарий, является ли он описанием в формате XML, и какие опции заданы для его обработки.

  21. Прочие условия.

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

Архитектура общего движка, на базе которого в настоящее время пишутся портеры с C# на Java и на C++, была пересмотрена с учётом данной проблемы, а также дополнительных возможностей, которые даёт Roslyn. Дерево кода в этой структуре более не является неизменным и последовательно модифицируется различными алгоритмами на новой стадии - стадии трансформации. Данные алгоритмы пишутся в соответствии с правилом единственной обязанности. На стадии трансформации выполняется столько работы, сколько возможно: синтаксический сахар C# заменяется конструкциями, доступными в C++ и Java, производится переименование типов и членов, удаляются сущности, исключённые из портирования, изменяются области видимости, модифицируются списки базовых типов, и так далее. В итоге логика кодогенерации существенно упрощается. С другой стороны, появляются дополнительные накладные расходы по управлению доступными алгоритмами модификации дерева и очерёдностью их выполнения.

Стадия анализа слилась со стадией трансформации. Алгоритмы стадии кодогенерации подверглись дополнительной декомпозиции: теперь за обработку отдельного типа узла отвечает, как правило, отдельный класс. Кроме того, было сделано большое количество других полезных изменений: улучшена подсистема конфигурирования, пересмотрен механизм замены типов C# типами целевых языков, поддержана работа не только в виде приложения (командной строки или оконного), но и в виде плагина для Visual Studio, работающего непосредственно с загруженным решением и встроенными средствами диагностики, и так далее.

Операции над исходным кодом

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

  1. Анализ кода C# на портируемость.
    Продукты разрабатываются программистами, редко знающими в подробностях процедуру портирования кода на другие языки и связанные с ней ограничения. В результате возникают ситуации, когда корректные с точки зрения C# изменения, сделанные продуктовыми разработчиками, ломают процедуру выпуска релизов для других языков. Например, на сегодняшний день ни один из наших портеров не имеет поддержки оператора yield, и его использование в коде C# приведёт к генерации некорректного кода Java или C++.
    За время развития проекта нами были испробованы несколько способов автоматизации обнаружения таких проблем.

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

    2. Проблема может быть обнаружена в среде CI (мы используем Jenkins и SonarQube). Таким образом, о проблеме узнают разработчики C# перед слиянием в общую ветку или после такого слияния, в зависимости от принятых конкретной командой практик. Это увеличивает оперативность исправления проблем, но требует программирования дополнительных проверок в инфраструктуре портера или в сторонних утилитах.

    3. Проблема может быть обнаружена локально разработчиком C# при запуске специализированных инструментов - например, портера в режиме анализатора. Это удобно, но требует разработки дополнительных утилит и дисциплины. Кроме того, это скрывает информацию о том, была ли проверка на самом деле запущена и пройдена.

    4. Проблема может быть обнаружена локально при работе в IDE. Установка плагина к Visual Studio позволяет разработчику C# обнаруживать проблемы в реальном времени. Это по-прежнему требует дополнительных затрат на разработку экосистемы, зато предоставляет наиболее оперативный способ обнаружения проблем. В этом смысле интеграция Roslyn в современные версии Visual Studio особенно удобна, так как позволяет использовать одни и те же анализаторы как в контексте загруженного в данный момент решения, так и в ином окружении - например, в среде CI.

  2. Понижение версии языка C#.
    Как уже говорилось выше, мы ограничены в использовании версий языка C#: 3.0 для портирования на Java и 5.0 для портирования на C++. Это требует дисциплины от программистов C# и во многих случаях неудобно. Чтобы обойти эти ограничения, портирование можно провести в два этапа: сначала заменить конструкции современных версий языка C# поддерживаемыми аналогами из прошлых стандартов, затем приступить непосредственно к портированию.
    При использовании портеров, основанных на устаревших синтаксических анализаторах, понижение может быть выполнено только путём использования внешних инструментов (например, утилит, написанных на базе Roslyn). С другой стороны, портеры, основанные на Roslyn, выполняют оба этапа последовательно, что позволяет использовать один и тот же код как при портировании кода ими, так и при подготовке кода к портированию более старыми инструментами.

  3. Подготовка примеров использования портированных библиотек.
    Это похоже на портирование кода продуктов, однако подразумевает несколько иные требования. При портировании библиотеки на десятки миллионов строк важно, прежде всего, максимально строгое следование поведению оригинального кода даже в ущерб читаемости: более простой, но отличающийся по эффектам код отлаживать придётся дольше. С другой стороны, примеры использования нашего портированного кода должны выглядеть максимально просто, давая понять, как пользоваться нашим кодом в C++, даже если это не соответствует поведению оригинальных примеров, написанных на C#.
    Так, при создании временных объектов программисты C# часто пользуютсяusing statement, чтобы избежать утечки ресурсов и строго задать момент их высвобождения, не полагаясь на GC. Строгое портирование using даёт достаточно сложный код C++ (см. ниже) из-за множества нюансов вида "если в блоке using statement вылетает исключение и из Dispose тоже вылетает исключение, какое из них попадёт в перехватывающий контекст?". Такой код лишь введёт в заблуждение программиста C++, создав впечатление, что использовать библиотеку сложно, однако на самом деле умного указателя на стеке, в нужный момент удаляющего объект и высвобождающего ресурсы, вполне достаточно.

  4. Подготовка документации к коду.
    Наши библиотеки предоставляют богатый API, задокументированный через XML-комментарии в соответствии с практиками C#. Перенос комментариев в C++ (мы используем Doxygen) - задача отнюдь не тривиальная: помимо разметки, необходимо заменить ссылки на типы (в C# полные имена записываются через точку, в C++ - через пару двоеточий) и их члены (а в случае использования свойств - ещё и понять, идёт ли речь о геттере или сеттере), а также оттранслировать фрагменты кода (которые лишены семантики и могут быть неполными).
    Эта задача решается как средствами самого портера, так и внешними утилитами - например, анализирующими сгенерированную XML-документацию и дополнительно подготовленные фрагменты вроде примеров использования методов.

Правила трансляции кода с C# на C++

Поговорим о том, каким образом синтаксические конструкции языка C# отображаются на C++. Эти правила не зависят от поколения продукта, поскольку иное существенно затруднило бы процесс миграции. Данный список не будет исчерпывающим ввиду ограниченного объёма статьи, но я постараюсь уделить внимание хотя бы основным моментам.

Проекты и единицы компиляции

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

Один проект C# преобразуется в один или два проекта C++. Первый проект (приложение или библиотека) аналогичен проекту C#, второй представляет собой googletest-приложение для запуска тестов (если они присутствуют в исходном проекте). Тип выходной библиотеки (статическая или динамическая) задаётся опциями портера. Для каждого входного проекта портер генерирует файл CMakeLists.txt, который позволяет создавать проекты для большинства сборочных систем. Зависимости между оттранслированными проектами настраиваются вручную в конфигурации портера или скриптах Cmake.

В большинстве случаев одному файлу .cs соответствует один файл .h и один файл .cpp. Имена файлов по возможности сохраняются (хотя из-за особенностей некоторых сборочных систем для C++ портер старается не допускать присутствия файлов с одинаковыми именами, пусть и в разных каталогах). Обычно определения типов попадают в заголовочный файл, а определения методов - в файл исходного кода, но это не так для шаблонных типов, весь код которых остаётся в заголовочных файлах. Файлы .cpp, в которые не попадает никакого кода, опускаются за ненадобностью.

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

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

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

Общая структура исходного кода

Пространства имён C# отображаются в пространства имён C++. Операторы использования пространств имён превращаются в аналоги из C++, по умолчанию попадая лишь в файлы .cpp (если опциями портирования не задано иное). Комментарии переносятся как есть, кроме документации к типам и методам, обрабатываемой отдельно. Форматирование сохраняется частично. Директивы препроцессора не переносятся (максимум - добавляются соответствующие комментарии), поскольку при построении синтаксического дерева необходимо уже задать все константы.

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

Метаданные к типам генерируются портером в виде специальных структур данных, доступных во время выполнения. Поскольку безусловная генерация метаданных существенно увеличивает объём скомпилированных библиотек, она обычно включается вручную для отдельных типов по мере необходимости.

Определения типов

Псевдонимы типов транслируются с использованием синтаксиса "using <typename> = ...". Перечисления C# транслируются в перечисления C++14 (синтаксис enum class).

Делегаты преобразуются в псевдонимы для специализаций класса System::MulticastDelegate:

public delegate int IntIntDlg(int n);
using IntIntDlg = System::MulticastDelegate<int32_t(int32_t)>;

Классы и структуры C# отображаются на классы C++. Интерфейсы превращаются в абстрактные классы. Структура наследования соответствует таковой в C# (неявное наследование от System.Object становится явным), если атрибутами не задано иное (например, для создания компактной структуры данных без лишних наследований и виртуальных функций). Свойства и индексаторы разбиваются на геттеры и сеттеры, представленные отдельными методами.

Виртуальные функции C# отображаются на виртуальные функции C++. Реализация интерфейсов также производится с использованием механизма виртуальных функций. Обобщённые (generic) типы и методы превращаются в шаблоны C++. Финализаторы переходят в деструкторы. Всё это вместе задаёт несколько ограничений:

  1. Трансляция виртуальных обобщённых методов не поддерживается.

  2. Реализация интерфейсных методов виртуальна, даже если в исходном коде это не так.

  3. Введение новых (new) методов с именами и сигнатурами, повторяющими имена и сигнатуры существующих виртуальных и/или интерфейсных методов, невозможно (но портер позволяет переименовывать такие методы).

  4. Если методы базового класса используются для реализации интерфейсов дочернего класса, в дочернем классе появляются дополнительные определения, которых нет в C#.

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

Понятно, что строгая имитация поведения C# требовала бы несколько иного подхода, и, если бы речь шла о трансляции приложений, это было бы оправдано. Тем не менее, мы предпочли следовать именно такой логике, поскольку в этом случае API портированных библиотек в наиболее полной мере соответствует парадигмам C++. Приведённый ниже пример демонстрирует эти особенности.

Код C#:

using System;public class Base{    public virtual void Foo1()    { }    public void Bar()    { }}public interface IFoo{    void Foo1();    void Foo2();    void Foo3();}public interface IBar{    void Bar();}public class Child : Base, IFoo, IBar{    public void Foo2()    { }    public virtual void Foo3()    { }    public T Bazz<T>(object o) where T : class    {        if (o is T)            return (T)o;        else            return default(T);    }}

Заголовочный файл C++:

#pragma once#include <system/object_ext.h>#include <system/exceptions.h>#include <system/default.h>#include <system/constraints.h>class Base : public virtual System::Object{    typedef Base ThisType;    typedef System::Object BaseType;        typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;    RTTI_INFO_DECL();    public:    virtual void Foo1();    void Bar();    };class IFoo : public virtual System::Object{    typedef IFoo ThisType;    typedef System::Object BaseType;        typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;    RTTI_INFO_DECL();    public:    virtual void Foo1() = 0;    virtual void Foo2() = 0;    virtual void Foo3() = 0;    };class IBar : public virtual System::Object{    typedef IBar ThisType;    typedef System::Object BaseType;        typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;    RTTI_INFO_DECL();    public:    virtual void Bar() = 0;    };class Child : public Base, public IFoo, public IBar{    typedef Child ThisType;    typedef Base BaseType;    typedef IFoo BaseType1;    typedef IBar BaseType2;        typedef ::System::BaseTypesInfo<BaseType, BaseType1, BaseType2> ThisTypeBaseTypesInfo;    RTTI_INFO_DECL();    public:    void Foo1() override;    void Bar() override;    void Foo2() override;    void Foo3() override;    template <typename T>    T Bazz(System::SharedPtr<System::Object> o)    {        assert_is_cs_class(T);                if (System::ObjectExt::Is<T>(o))        {            return System::StaticCast<typename T::Pointee_>(o);        }        else        {            return System::Default<T>();        }    }    };

Исходный код C++:

#include "Class1.h"RTTI_INFO_IMPL_HASH(788057553u, ::Base, ThisTypeBaseTypesInfo);void Base::Foo1(){}void Base::Bar(){}RTTI_INFO_IMPL_HASH(1733877629u, ::IFoo, ThisTypeBaseTypesInfo);RTTI_INFO_IMPL_HASH(1699913226u, ::IBar, ThisTypeBaseTypesInfo);RTTI_INFO_IMPL_HASH(3787596220u, ::Child, ThisTypeBaseTypesInfo);void Child::Foo1(){    Base::Foo1();}void Child::Bar(){    Base::Bar();}void Child::Foo2(){}void Child::Foo3(){}

Серия псевдонимов и макросов в начале каждого портированного класса нужна для эмуляции некоторых механизмов C# (прежде всего, GetType, typeof и is). Хэш-коды из файла .cpp используются для быстрого сравнения типов. Все функции, реализующие интерфейсы, виртуальны, хотя в C# это не так.

Члены классов

Как было показано выше, методы классов ложатся на C++ напрямую. Это также касается статических методов и конструкторов. В некоторых случаях в них может появляться дополнительный код - например, чтобы эмулировать вызовы статических конструкторов или чтобы избежать обнуления счётчика ссылок на объект до завершения его конструирования. Впрочем, явный вызов статических конструкторов затратен и потому используется редко; чаще мы переносим код статического конструктора в конструктор закрытого статического поля.

Экземплярные поля C# становятся экземплярными полями C++. Статические поля также остаются без изменений (кроме случаев, когда важен порядок инициализации - это исправляется портированием таких полей в виде синглтонов).

Свойства разбиваются на метод-геттер и метод-сеттер (или что-то одно, если второй метод отсутствует). Для автоматических свойств к ним добавляется также закрытое поле. Статические свойства распадаются на статические геттер и сеттер. Индексаторы обрабатываются по той же логике.

События транслируются в поля (экземплярные или статические), тип которых соответствует нужной специализации System::Event. Трансляция в виде трёх методов (add, remove и invoke) была бы более правильной и, к тому же, позволила бы поддержать абстрактные и виртуальные события. Возможно, в будущем мы придём к такой модели, однако на данный момент вариант с классом Event полностью покрывает потребности нашего кода.

Методы расширения и операторы транслируются в статические методы и вызываются явно. Финализаторы становятся деструкторами.

Следующий пример иллюстрирует описанные выше правила. Как обычно, я удалил незначащую часть кода C++.

public abstract class Generic<T>{    private T m_value;    public Generic(T value)    {        m_value = value;    }    ~Generic()    {        m_value = default(T);    }    public string Property { get; set; }    public abstract int Property2 { get; }    public T this[int index]    {        get        {            return index == 0 ? m_value : default(T);        }        set        {            if (index == 0)                m_value = value;            else                throw new ArgumentException();        }    }    public event Action<int, int> IntIntEvent;}
template<typename T>class Generic : public System::Object{public:    System::String get_Property()    {        return pr_Property;    }    void set_Property(System::String value)    {        pr_Property = value;    }        virtual int32_t get_Property2() = 0;        Generic(T value) : m_value(T())    {        m_value = value;    }        T idx_get(int32_t index)    {        return index == 0 ? m_value : System::Default<T>();    }    void idx_set(int32_t index, T value)    {        if (index == 0)        {            m_value = value;        }        else        {            throw System::ArgumentException();        }    }        System::Event<void(int32_t, int32_t)> IntIntEvent;        virtual ~Generic()    {        m_value = System::Default<T>();    }private:    T m_value;    System::String pr_Property;};

Переменные и поля

Константные и статические поля транслируются в статические поля, статические константы (в некоторых случаях - constexpr) либо в статические методы (дающие доступ к синглтону). Экземплярные поля C# преобразуются в экземплярные поля C++, при этом все сколько-нибудь сложные инициализаторы переносятся в конструкторы (иногда для этого приходится явно добавлять конструкторы по умолчанию там, где их не было в C#). Переменные на стеке переносятся как есть. Аргументы методов - тоже, за исключением того, что и ref-, и out-аргументы становятся ссылочными (благо, IL их всё равно не различает, и потому перегрузка по ним запрещена).

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

Кроме того, ссылочные типы оборачиваются в SmartPtr (ранее яписало том, что он по большей части следует семантике intrusive_ptr, но позволяет переключать режим ссылки - слабая или сильная - во время выполнения). Значимые типы подставляются как есть. Поскольку аргументы-типы могут быть как значимыми, так и ссылочными, они также подставляются как есть, но при инстанциировании ссылочные аргументы оборачиваются в SharedPtr (таким образом,List<int>транслируется какList<int32_t>, ноList<Object>становитсяList<SmartPtr<Object>>. В некоторых исключительных случаях ссылочные типы портируются как значимые (например, наша реализация System::String написана на базе типа UnicodeString из ICU и оптимизирована для хранения на стеке).

Для примера портируем следующий класс:

public class Variables{    public int m_int;    private string m_string = new StringBuilder().Append("foobazz").ToString();    private Regex m_regex = new Regex("foo|bar");    public object Foo(int a, out int b)    {        b = a + m_int;        return m_regex.Match(m_string);    }}

После портирования он принимает следующий вид (я удалил код, не относящийся к делу):

class Variables : public System::Object{public:    int32_t m_int;    System::SharedPtr<System::Object> Foo(int32_t a, int32_t& b);    Variables();private:    System::String m_string;    System::SharedPtr<System::Text::RegularExpressions::Regex> m_regex;};System::SharedPtr<System::Object> Variables::Foo(int32_t a, int32_t& b){    b = a + m_int;    return m_regex->Match(m_string);}Variables::Variables()    : m_int(0)    , m_regex(System::MakeObject<System::Text::RegularExpressions::Regex>(u"foo|bar")){    this->m_string = System::MakeObject<System::Text::StringBuilder>()->        Append(u"foobazz")->ToString();}

Управляющие структуры

Подобие основных управляющих структур сыграло нам на руку. Такие операторы, как if, else, switch, while, do-while, for, try-catch, return, break и continue в большинстве случаев переносятся как есть. Исключением в данном списке является разве что switch, требующий пары специальных обработок. Во-первых, C# допускает его использование со строковым типом - в C++ мы в этом случае генерируем последовательность if-else if. Во-вторых, относительно недавно добавилась возможность сопоставлять проверяемое выражение шаблону типа - что, впрочем, также легко разворачивается в последовательность ifов.

Интерес представляют конструкции, которых нет в C++. Так, оператор using даёт гарантию вызова метода Dispose() при выходе из контекста - в C++ мы эмулируем это поведение, создавая объект-часового на стеке, который вызывает нужный метод в своём деструкторе. Перед этим, правда, нужно перехватить исключение, вылетевшее из кода, бывшего телом using, и сохранить exception_ptr в поле часового - если Dispose() не бросит своё исключение, будет переброшено то, которое мы сохранили. Это как раз тот редкий случай, когда вылет исключения из деструктора оправдан и не является ошибкой. Блок finally транслируется по похожей схеме, только вместо метода Dispose() вызывается лямбда-функция, в которую портер обернул его тело.

Ещё один оператор, которого нет в C# и который мы вынуждены эмулировать, - это foreach. Изначально мы портировали его в эквивалентный while(), вызывающий метод MoveNext() у перечислителя, что универсально, но довольно медленно. Поскольку в большинстве своём плюсовые реализации контейнеров из .Net используют структуры данных STL, мы пришли к тому, чтобы там, где это возможно, использовать их оригинальные итераторы, конвертируя foreach в range-based for. В тех случаях, когда оригинальные итераторы недоступны (например, контейнер реализован на чистом C#), используются итераторы-обёртки, внутри себя работающие с перечислителями. Раньше за выбор нужного способа итерации отвечала внешняя функция, написанная с использованием техники SFINAE, сейчас мы близки к тому, чтобы иметь правильные версии методов begin-end во всех контейнерах (в т. ч. портированных).

Операторы unsafe и unchecked в нашем коде практически не используются, так что их портер попросту игнорирует.

Операторы

Как и в случае с управляющими структурами, большинство операторов (по крайней мере, арифметических, логических и присваивания) не требуют особой обработки. Тут, правда, есть тонкий момент: в C# порядок вычисления частей выражения детерминирован, тогда как в C++ в некоторых случаях возникает неопределённое поведение. Например, следующий портированный код ведёт себя неодинаково после компиляции разными инструментами:

auto offset32 = block[i++] + block[i++] * 256 + block[i++] * 256 * 256 +    block[i++] * 256 * 256 * 256;

К счастью, подобные проблемы возникают достаточно редко. У нас есть планы научить портер бороться с такими моментами, но из-за сложности анализа, выявляющего выражения с побочными эффектами, это пока не было реализовано.

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

obj1.Property = obj2.Property;string s = GetObj().Property += "suffix";
obj1->set_Property(obj2->get_Property());System::String s = System::setter_add_wrap(static_cast<MyClass*>(GetObj().GetPointer()),    &MyClass::get_Property, &MyClass::set_Property, u"suffix")

В первой строке замена оказалась тривиальной. Во второй пришлось использовать обёртку setter_add_wrap, гарантирующую, что функция GetObj() будет вызвана всего один раз, а результат конкатенации вызова get_Property() и строкового литерала будет передан не только в метод set_Property() (который возвращает void), но и далее для использования в выражении. Тот же подход применяются при обращении к индексаторам.

Операторы C#, которых нет в C++ (as, is, typeof, default, ??, ?., и так далее), эмулируются при помощи библиотечных функций. В тех случаях, когда требуется избежать двойного вычисления аргументов (например, чтобы не разворачивать "GetObj()?.Invoke()" в "GetObj() ? GetObj().Invoke() : nullptr)", используется подход, подобный показанному выше.

Оператор доступа к члену (.) в зависимости от контекста может заменяться на аналог из C++: на оператор разрешения области видимости (::) или на "стрелку" (->). При доступе к членам структур такая замена не требуется.

Исключения

Эмуляция поведения C# в аспекте работы с исключениями является весьма нетривиальной. Дело в том, что в C# и в C++ исключения ведут себя по-разному:

  • В C# исключения создаются на куче и удаляются сборщиком мусора.

  • В C++ исключения в разные моменты копируются между стеком и выделенной для них областью памяти.

Здесь возникает противоречие. Если транслировать типы исключений C# как ссылочные, работая с ними по голым указателям (throw new ArgumentException), это приведёт к утечкам памяти (или большим проблемам с определением точек их удаления). Если транслировать их как ссылочные, но владеть ими по умному указателю (throw SharedPtr<ArgumentException>(MakeObject<ArgumentException>())), исключение будет невозможно перехватить по его базовому типу (потому что SharedPtr<ArgumentException> не наследует SharedPtr<Exception>). Если же размещать объекты исключений на стеке, они будут корректно перехватываться по базовому типу, но при сохранении в переменную базового типа информация о конечном типе будет усекаться (к сожалению, у нас есть даже код, хранящий коллекции исключений, так что это не пустая тревога).

Для решения этой проблемы мы создали специальный тип умных указателей ExceptionWrapper. Его ключевая особенность заключается в том, что, если класс ArgumentException наследуется от Exception, то и ExceptionWrapper<ArgumentException> наследуется от ExceptionWrapper<Exception>. Экземпляры ExceptionWrapper используются для управления временем жизни экземпляров классов исключений, при этом усечение типа ExceptionWrapper не приводит к усечению типа связанного Exception. За выброс исключений отвечает виртуальный метод, переопределяемый наследниками Exception, который создаёт ExceptionWrapper, параметризованный конечным типом исключения, и выбрасывает его. Виртуальность позволяет выбросить правильный тип исключения, даже если тип ExceptionWrapper был усечён ранее, а связь между объектом исключения и ExceptionWrapper предотвращает утечку памяти.

Создание объектов и инициализация

Для создания объектов ссылочных типов, кроме нескольких специальных случаев, мы используем функцию MakeObject (аналог std::make_shared), которая создаёт объект оператором new и сразу оборачивает его в SharedPtr. Кроме того, MakeObject инкапсулирует некую сервисную логику. Использование этой функции позволило избежать проблем, привносимых голыми указателями, однако породило проблему прав доступа: поскольку она находится вне всех классов, она не имела доступа к закрытым конструкторам, даже будучи вызванной из самих классов или их друзей. Объявление этой функции в качестве друга классов с непубличными конструкторами эффективно открывало эти конструкторы для всех контекстов. В результате внешняя версия этой функции была ограничена использованием с публичными конструкторами, а для непубличных конструкторов были добавлены статические методы MakeObject, имеющие тот же уровень доступа и те же аргументы, что и проксируемый конструктор.

Литералы часто приходится менять при портировании: так, @"C:\Users" превращается в u"C:\\Users", а 15L - в INT64_C(15).

Программисты C# часто используют инициализаторы свойств в составе выражения создания объекта. Соответствующий синтаксис приходится оборачивать в лямбда-функции, поскольку в противном случае записать инициализаторы в составе одного выражения не получается:

Foo(new MyClass() { Property1 = "abc", Property2 = 1, Field1 = 3.14 });
Foo([&]{ auto tmp_0 = System::MakeObject<MyClass>();        tmp_0->set_Property1(u"abc");        tmp_0->set_Property2(1);        tmp_0->Field1 = 3.14;        return tmp_0;    }());

Вызовы, делегаты и анонимные методы

Вызовы методов переносятся как есть. При наличии перегруженных методов иногда приходится явно приводить типы аргументов, поскольку правила разрешения перегрузок в C++ отличаются от таковых в C#. Рассмотрим, например, следующий код:

class MyClass<T>{    public void Foo(string s) { }    public void Bar(string s) { }    public void Bar(bool b) { }    public void Call()    {        Foo("abc");        Bar("def");    }}

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

template<typename T>class MyClass : public System::Object{public:    void Foo(System::String s)    {        ASPOSE_UNUSED(s);    }    void Bar(System::String s)    {        ASPOSE_UNUSED(s);    }    void Bar(bool b)    {        ASPOSE_UNUSED(b);    }    void Call()    {        Foo(u"abc");        Bar(System::String(u"def"));    }};

Обратите внимание: вызовы методов Foo и Bar внутри метода Call записаны по-разному. Это связано с тем, что без явного вызова конструктора String была бы вызвана перегрузка Bar, принимающая bool, т. к. такое приведение типа имеет более высокий приоритет по правилам C++. В случае метода Foo такой неоднозначности нет, и портер генерирует более простой код.

Ещё один пример, когда C# и C++ ведут себя по-разному - это разворачивание шаблонов. В C# подстановка типов-параметров производится уже в рантайме и не влияют на разрешение вызовов внутри обобщённых методов. В C++ подстановка аргументов шаблонов происходит в момент вызова, так что поведение C# приходится эмулировать. Например, рассмотрим следующий код:

class GenericMethods{    public void Foo<T>(T value) { }    public void Foo(string s) { }    public void Bar<T>(T value)    {        Foo(value);    }    public void Call()    {        Bar("abc");    }}
class GenericMethods : public System::Object{public:    template <typename T>    void Foo(T value)    {        ASPOSE_UNUSED(value);    }    void Foo(System::String s);    template <typename T>    void Bar(T value)    {        Foo<T>(value);    }    void Call();};void GenericMethods::Foo(System::String s){}void GenericMethods::Call(){    Bar<System::String>(u"abc");}

Здесь стоит обратить внимание на явное указание аргументов шаблона при вызове Foo и Bar. В первом случае это необходимо, потому что иначе при инстанциировании версии для T=System::String будет вызвана нешаблонная версия, что отличается от поведения C#. Во втором случае аргумент нужен, поскольку в противном случае он будет выведен на основе типа строкового литерала. Вообще, явно указывать аргументы шаблона портеру приходится почти всегда, чтобы избежать неожиданного поведения.

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

В .Net встречаются методы, которые поддерживают перегрузку по числу и типу аргументов через синтаксис params, через указание object в качестве типа аргумента, либо через то и другое сразу - например, подобные перегрузки есть у StringBuilder.Append() и у Console.WriteLine(). Прямой перенос таких конструкций показывает плохую производительность из-за боксирования и создания временных массивов. В таких случаях мы добавляем перегрузку, принимающую переменное число аргументов произвольных типов с использованием вариативных шаблонов, и заставляем портер транслировать аргументы как есть, без приведений типов и объединений в массивы. В результате удаётся поднять производительность таких вызовов.

Делегаты транслируются в специализации шаблона MulticastDelegate, который, как правило, содержит внутри себя контейнер экземпляров std::function. Их вызов, хранение и присваивание осуществляются тривиально. Анонимные методы превращаются в лямбда-функции.

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

Тесты

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

Программисты C# используют фреймворки NUnit и xUnit. Портер переводит соответствующие тестовые примеры на GoogleTest, заменяя синтаксис проверок и вызывая методы, помеченные флагом Test или Fact, из соответствующих тестовых функций. Поддерживаются как тесты без аргументов, так и входные данные вроде TestCase или TestCaseData. Пример портирования тестового класса приведён ниже.

[TestFixture]class MyTestCase{    [Test]    public void Test1()    {        Assert.AreEqual(2*2, 4);    }    [TestCase("123")]    [TestCase("abc")]    public void Test2(string s)    {        Assert.NotNull(s);    }}
class MyTestCase : public System::Object{public:    void Test1();    void Test2(System::String s);};namespace gtest_test{class MyTestCase : public ::testing::Test{protected:    static System::SharedPtr<::ClassLibrary1::MyTestCase> s_instance;    public:    static void SetUpTestCase()    {        s_instance = System::MakeObject<::ClassLibrary1::MyTestCase>();    };        static void TearDownTestCase()    {        s_instance = nullptr;    };    };System::SharedPtr<::ClassLibrary1::MyTestCase> MyTestCase::s_instance;} // namespace gtest_testvoid MyTestCase::Test1(){    ASSERT_EQ(2 * 2, 4);}namespace gtest_test{TEST_F(MyTestCase, Test1){    s_instance->Test1();}} // namespace gtest_testvoid MyTestCase::Test2(System::String s){    ASSERT_FALSE(System::TestTools::IsNull(s));}namespace gtest_test{using MyTestCase_Test2_Args = System::MethodArgumentTuple<decltype(    &ClassLibrary1::MyTestCase::Test2)>::type;struct MyTestCase_Test2 : public MyTestCase, public ClassLibrary1::MyTestCase,    public ::testing::WithParamInterface<MyTestCase_Test2_Args>{    static std::vector<ParamType> TestCases()    {        return        {            std::make_tuple(u"123"),            std::make_tuple(u"abc"),        };    }};TEST_P(MyTestCase_Test2, Test){    const auto& params = GetParam();    ASSERT_NO_FATAL_FAILURE(s_instance->Test2(std::get<0>(params)));}INSTANTIATE_TEST_SUITE_P(, MyTestCase_Test2,     ::testing::ValuesIn(MyTestCase_Test2::TestCases()));} // namespace gtest_test

Проблемы

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

  1. Синтаксис C# не имеет прямых аналогов на C++. Это относится, например, к операторам using и yeild.
    В таких случаях нам приходится писать довольно сложный код для эмуляции поведения оригинального кода - как в портере, так и в библиотеке - либо отказываться от поддержки таких конструкций.

  2. Конструкции C# не переводятся на C++ в рамках принятых нами правил портирования. Например, в исходном коде присутствуют виртуальные обобщённые методы, или конструкторы, использующие виртуальные функции.
    В подобных случаях нам не остаётся ничего, кроме как переписывать такой проблемный код в терминах, допускающих портирование на C#. К счастью, обычно подобные конструкции составляют относительно небольшой объём кода.

  3. Работа кода C# зависит от окружения, специфичного для .Net. Это включает, например, ресурсы, рефлексию, динамическое подключение сборок и импорт функций.
    В таких случаях нам, как правило, приходится эмулировать соответствующие механизмы. Это включает в себя поддержку ресурсов (которые внедряются в сборку в виде статических массивов и затем читаются через специализированные реализации потоков) и рефлексию. С другой стороны, очевидно, что напрямую подключать сборки .Net к коду C++ или импортировать функции из динамических библиотек Windows при выполнении на другой платформе мы не можем - подобный код приходится урезать либо переписывать.

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

  5. Работа библиотечного кода отличается от работы оригинальных классов из .Net.
    В каких-то случаях речь идёт о простых ошибках в имплементации - как правило, их несложно исправить. Гораздо хуже дело обстоит, когда разница в поведении лежит на уровне подсистем, используемых библиотечным кодом. Например, многие наши библиотеки активно используют классы из библиотеки System.Drawing, построенной на GDI+. Версии этих классов, разработанных нами для C++, используют Skia в качестве графического движка. Поведение Skia зачастую отличается от такового в GDI+, особенно под Linux, и на то, чтобы добиться одинаковой отрисовки, нам приходится тратить значительные ресурсы. Аналогично, libxml2, на которой построена наша реализация System::Xml, ведёт себя в иных случаях не так, и нам приходится патчить её или усложнять свои обёртки.

  6. Портированный код порой работает медленнее оригинала.
    Программисты на C# оптимизируют свой код под те условия, в которых он выполняется. В то же время, многие структуры начинают работать медленнее в необычном для себя окружении. Например, создание большого количества мелких объектов в C# обычно работает быстрее, чем в C++, из-за иной схемы работы кучи (даже с учётом сборки мусора). Динамическое приведение типов в C++ также несколько медленнее. Подсчёт ссылок при копировании указателей - ещё один источник накладных расходов, которых нет в C#. Наконец, использование вместо встроенных, оптимизированных концепций C++ (итераторы) переводных с C# (перечислители) также замедляет работу кода.
    Способ устранения бутылочных горлышек во многом зависит от ситуации. Если библиотечный код сравнительно легко оптимизировать, то сохранить поведение портированных концепций и в то же время оптимизировать их работу в чуждом окружении порой не так-то просто.

  7. Портированный код не соответствует духу C++. Например, в публичном API присутствуют методы, принимающие SharedPtr<Object>, у контейнеров отсутствуют итераторы, методы для работы с потоками принимают System::IO::Stream вместо istream, ostream или iostream, и так далее.
    Мы последовательно расширяем портер и библиотеку таким образом, чтобы нашим кодом было удобно пользоваться программистам C++. Например, портер уже умеет генерировать методы begin-end и перегрузки, работающие со стандартными потоками.

  8. Портированный код обнажает наши алгоритмы. В заголовочные файлы попадают типы и имена закрытых полей, а также полный код шаблонных методов. Эта информация обычно обфусцируется при выпуске релизов для .Net.
    Мы стараемся исключить лишнюю информацию при помощи сторонних утилит, а также специальными режимами работы самого портера, однако это не всегда возможно. Например, удаление закрытых статических полей и невиртуальных методов не влияет на работу клиентского кода, однако удалить или переименовать виртуальные методы без потери функциональности невозможно. Поля могут быть переименованы, а их тип - заменён на заглушку того же размера, при условии, что конструкторы и деструкторы экспортированы из кода, собранного с полными заголовочными файлами. В то же время, как-либо скрыть код публичных шаблонных методов не представляется возможным.

Планы развития проекта

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

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

Помимо решения текущих проблем и плановых улучшений, мы заняты переводом портеров с C# на Java и C++ на современный синтаксический анализатор (Roslyn). Это небыстрый процесс, ведь количество случаев, которые продукт должен обрабатывать, весьма велико. Мы начинаем с поддержки наиболее общих структур, а затем переходим ко всё более редким случаям. Для этого у нас есть большое количество тестов: тесты на вывод портера, тесты на вывод портированного приложения, тесты в портированных проектах. В какой-то момент происходит переход от специально подготовленных тестов к тестированию на реальных продуктах, содержащих сотни тысяч и даже десятки миллионов строк кода, что неизбежно вскрывает какие-то недоработки.

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

Наконец, мы думаем о том, чтобы замахнуться на расширение числа поддерживаемых языков - как целевых, так и исходных. Адаптировать решения, основанные на Roslyn, для чтения кода VB будет относительно легко - тем более, что библиотеки для C++ и Java уже готовы. С другой стороны, подход, который мы применили для поддержки Python, во многом проще, и по аналогии можно поддержать иные скриптовые языки - например, PHP.

Подробнее..

Шаблоны и концепты в С20

15.04.2021 14:13:15 | Автор: admin

Привет, Хабр!

Недавно Егор Суворов, преподаватель курса по С++ в Питерской Вышке, прочитал лекцию о некоторых особенностях языка для участников Всероссийской олимпиады школьников по информатике. Егор рассказал о шаблонах в C++, а также где и зачем они возникают: обобщённое программирование структур данных и алгоритмов, функторы и лямбда-функции, как можно повысить уровень абстракций и упростить код.

Важное уточнение: эта лекция не попытка объять необъятное, а краткий экскурс по полезным возможностям C++ для членов олимпиадного сообщества: от извлечения кода в класс до внутренних механизмов работы лямбда-функций и щепотки ограничений (constraints) из C++20. Если интересно, приглашаем к просмотру.

Подробные таймкоды

00:53 Что нужно знать перед просмотром лекции

02:00 Особенности С++

03:10 Хорошие источники знаний и практик в C++

04:45 Классы. Стек с минимумом

06:21 Создание своей структуры

09:03 Запрещаем прямой доступ

09:53 Упрощаем отладку

10:29 Шаблоны классов

11:24 Статический полиморфизм в разных языках

12:03 Оптимизация

12:27 Ошибки компиляции и инстанцирование

13:40 Ограничения (С++20)

15:01 Шаблоны функций

15:27 Автовывод параметров

16:21 Class Template Argument Deduction (CTAD, С++17)

16:56 Ошибки компиляции и инстанцирование

17:47 Обобщенное программирование

19:12 Вложенные типы

20:10 Продвинутые техники

20:33 Функторы

21:00 Функциональные объекты

21:56 Как параметр шаблона

22:30 Функторы с состоянием

23:26 Функторы с состоянием для контейнеров

24:42 Лямбда-выражения

25:38 Расшифровка лямбды

26:28 Сохранение в переменную

27:27 Рекурсия не поддерживается

27:56 Захваты по значению и ссылке

29:18 Захват с инициализатором

30:29 Комбинированные захваты

31:16 Применение функторов

32:15 IIFE

33:18 Вектор лямбд и стирание типов (type erasure)

34:36 Функтор как параметр функции

35:51 Функтор как поле класса

37:45 Более сложные структуры данных (декартово дерево, дерево отрезков)

38:34 За кадром: лямбды-компараторы

39:48 За кадром: более сложные шаблоны

41:23 Студенческие проекты на C++ (в прошлом году рассказывали о проектах наших первокурсниках)

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

Подробнее..

Пишем плагин отладки для SNES игр в IDA v7

16.04.2021 04:04:08 | Автор: admin


Приветствую,


Моя очень старая мечта сбылась я написал модуль-отладчик, с помощью которого можно отлаживать SNES (Super Nintendo) игры прямо в IDA! Если интересно узнать, как я это сделал, "прошу под кат" (как тут принято говорить).


Введение


Я давно увлекаюсь реверс-инжинирингом. Сначала это было просто хобби, затем стало работой (и при этом хобби никуда не делось). Только на работе "всё серьёзно", а дома это баловство в виде обратной разработки игр под ретро-приставки: Sega Mega Drive / Genesis, PS1, AmigaOS. Задача обычно стоит следующая: понять как работает игра, если есть сжатие победить его, понять как строится уровень, как размещаются враги на уровне и т.д.


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


Мне удалось разреверсить один очень крутой shoot'em-up: Thunder Force 3 (а именно благодаря этой игре я и познакомился с Идой). Я написал редактор уровней, разреверсил игру до исходников на ассемблере, и всё это попутно создавая и улучшая инструмент, который в последствии и облегчал данную работу плагин-отладчик сеговских ромов для IDA, который я назвал просто Gensida (т.к. в основе лежал один очень популярный эмулятор этой платформы GENS, а точнее его модификация).



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

Со временем я узнал, что у Thunder Force 3 есть и версия для SNES Thunder Spirits, которая имеет несколько новых уровней и некоторые изменения в интерфейсе. Так вот, мне захотелось портировать всё это на Сегу, дополнив игру. Но, знаний как о самой Super Nintendo, так и о том, как её реверсить, у меня не было. Я пошёл гуглить и понял, что как-то всё плохо с отладкой у "сеги подороже". На данный момент существует всего ДВА (!) эмулятора SNES с отладкой, и у одного нет исходников, а второй второй имеет настолько убогий исходный код, что я боялся даже с ним работать.


Тем не менее, овладев некоторыми знаниями и умениями, и переборов желание не ввязываться в такой ужасный код (эмулятора), я смог написать и Snesida отладчик SNES ромов для под IDA. И, я считаю, что теперь то уж настал тот момент, когда я готов рассказать о том, как создать более-менее полноценный отладчик для этого ревёрсерского инструмента.


Что нам потребуется


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


  1. IDA v7.x
  2. IDA SDK
  3. Эмулятор-отладчик (можно и без отладки, главное с исходниками, которые захочется допилить)
  4. Thrift (да, я выбрал его за сериализацию и RPC прямо "из коробки")
  5. Умение писать на C++

Думаю, список достаточно простой и понятный. Если чего-то из этого у вас нет, то плагин не получится, увы.


А теперь пишем код


Прежде чем начать, советую ознакомиться со статьёй "Модернизация IDA Pro. Отладчик для Sega Mega Drive (часть 2)", т.к. многие моменты здесь будут повторяться, но будут и некоторые новые (т.к. SDK Иды обновляется, и то, что работало раньше, теперь не применимо).


Собственно, написание любого плагина для IDA всегда начинается с создания кода-шаблона. Я использую для этого Visual Studio (на данный момент самой свежей является версия 2019).


Открываем Студию, создаём новый проект DLL, и прописываем в следующие пути к библиотекам в свойствах Linker для проекта:


  • d:\idasdk76\lib\x64_win_vc_32\ это для плагина, который будет работать с 32-битными приложениями (открываться в ida.exe)
  • d:\idasdk76\lib\x64_win_vc_64\ это для плагина, который будет работать с 64-битными приложениями (открываться в ida64.exe)
  • Если у вас не Windows и компилятор не Visual Studio, посмотрите другие имеющиеся папки в d:\idasdk76\lib\

В линкуемые библиотеки добавляем ida.lib. Теперь создаём пустой cpp-файл, чтобы VS показала свойства C/C++ компилятора и указываем:


  • d:\idasdk76\include\ в спискок путей к инклудам
  • Меняем /MDd и /MD на /MTd и /MT соответственно в свойствах Code Generation просто, чтобы не зависеть от лишних библиотек, которые не всегда установлены
  • __NT__;__IDP__;__X64__; в Preprocessor Definitions компилятора
  • __EA64__; дополнительно к предыдущим флагам, если плагин будет работать с 64-битными приложениями
  • Убираем SDL Checks с ним будет сложнее писать код

С подготовкой вроде бы всё. Теперь начнём писать код.


Плагин


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


ida_plugin.cpp
#include <ida.hpp>#include <idp.hpp>#include <dbg.hpp>#include <loader.hpp>#include "ida_plugin.h"extern debugger_t debugger;static bool plugin_inited;static bool init_plugin(void) {    return (ph.id == PLFM_65C816);}static void print_version(){    static const char format[] = NAME " debugger plugin v%s;\nAuthor: DrMefistO [Lab 313] <newinferno@gmail.com>.";    info(format, VERSION);    msg(format, VERSION);}static plugmod_t* idaapi init(void) {    if (init_plugin()) {        dbg = &debugger;        plugin_inited = true;        print_version();        return PLUGIN_KEEP;    }    return PLUGIN_SKIP;}static void idaapi term(void) {    if (plugin_inited) {        plugin_inited = false;    }}static bool idaapi run(size_t arg) {    return false;}char comment[] = NAME " debugger plugin by DrMefistO.";char help[] =    NAME " debugger plugin by DrMefistO.\n"    "\n"    "This module lets you debug SNES roms in IDA.\n";plugin_t PLUGIN = {    IDP_INTERFACE_VERSION,    PLUGIN_PROC | PLUGIN_DBG,    init,    term,    run,    comment,    help,    NAME " debugger plugin",    ""};

Здесь мы описываем наш плагин, инициализируем структуру dbg, т.к. мы отладчик, и указываем, что работаем мы только с платформой PLFM_65C816 (в моём случае). Более подробно в статье про отладчик для Сеги.


Следом идёт ida_plugin.h. Тут всё просто константы для cpp-файла плагина:


#pragma once#define NAME "snesida"#define VERSION "1.0"

Код самого отладчика


Собственно, пока у нас в голове только идея отладчика, и мы ей горим, всё что мы можем пока написать, это базовый код, который будем постепенно дополнять. Начиная с этой части, если сравнивать с предыдущей статьёй, появились значительные изменения в коде и концепции в написании отладчика, поэтому читаем внимательно:


ida_debug.cpp
#include <ida.hpp>#include <dbg.hpp>#include <auto.hpp>#include <deque>#include <mutex>#include "ida_plugin.h"#include "ida_debmod.h"#include "ida_registers.h"static ::std::mutex list_mutex;static eventlist_t events;static const char* const p_reg[] ={    "CF",    "ZF",    "IF",    "DF",    "XF",    "MF",    "VF",    "NF",};static register_info_t registers[] = {    {"A", 0, RC_CPU, dt_word, NULL, 0},    {"X", 0, RC_CPU, dt_word, NULL, 0},    {"Y", 0, RC_CPU, dt_word, NULL, 0},    {"D", 0, RC_CPU, dt_word, NULL, 0},    {"DB", 0, RC_CPU, dt_byte, NULL, 0},    {"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},  {"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},    {"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},  {"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},  {"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},    {"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},};static const char* register_classes[] = {    "General Registers",    NULL};static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf){  return DRC_OK;}static drc_t idaapi term_debugger(void){  return DRC_OK;}static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {  process_info_t info;  info.name.sprnt("bsnes");  info.pid = 1;  procs->add(info);  return DRC_OK;}static drc_t idaapi s_start_process(const char* path,  const char* args,  const char* startdir,  uint32 dbg_proc_flags,  const char* input_path,  uint32 input_file_crc32,  qstring* errbuf = NULL){  ::std::lock_guard<::std::mutex> lock(list_mutex);  events.clear();  return DRC_OK;}static drc_t idaapi prepare_to_pause_process(qstring* errbuf){  return DRC_OK;}static drc_t idaapi emul_exit_process(qstring* errbuf){  return DRC_OK;}static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms){  while (true)  {    ::std::lock_guard<::std::mutex> lock(list_mutex);    // are there any pending events?    if (events.retrieve(event))    {      return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;    }    if (events.empty())      break;  }  return GDE_NO_EVENT;}static drc_t idaapi continue_after_event(const debug_event_t* event){  dbg_notification_t req = get_running_notification();  switch (event->eid())  {  case PROCESS_SUSPENDED:    break;  case PROCESS_EXITED:    break;  }  return DRC_OK;}static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread{  switch (resmod)  {  case RESMOD_INTO:    ///< step into call (the most typical single stepping)    break;  case RESMOD_OVER:    ///< step over call    break;  }  return DRC_OK;}static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf){  if (clsmask & RC_CPU)  {    }    return DRC_OK;}static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf){  if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {    }    return DRC_OK;}static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf){  memory_info_t info;  info.start_ea = 0x0000;  info.end_ea = 0x01FFF;  info.sclass = "STACK";  info.bitness = 0;  info.perm = SEGPERM_READ | SEGPERM_WRITE;  areas.push_back(info);  // Don't remove this loop  for (int i = 0; i < get_segm_qty(); ++i)  {    segment_t* segm = getnseg(i);    info.start_ea = segm->start_ea;    info.end_ea = segm->end_ea;    qstring buf;    get_segm_name(&buf, segm);    info.name = buf;    get_segm_class(&buf, segm);    info.sclass = buf;    info.sbase = get_segm_base(segm);    info.perm = segm->perm;    info.bitness = segm->bitness;    areas.push_back(info);  }  // Don't remove this loop    return DRC_OK;}static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf){  return size;}static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf){  return size;}static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len){  switch (type)  {  case BPT_EXEC:  case BPT_READ:  case BPT_WRITE:  case BPT_RDWR:    return BPT_OK;  }  return BPT_BAD_TYPE;}static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf){  for (int i = 0; i < nadd; ++i)  {    ea_t start = bpts[i].ea;    ea_t end = bpts[i].ea + bpts[i].size - 1;    bpts[i].code = BPT_OK;  }  for (int i = 0; i < ndel; ++i)  {    ea_t start = bpts[nadd + i].ea;    ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;    bpts[nadd + i].code = BPT_OK;  }  *nbpts = (ndel + nadd);  return DRC_OK;}static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {  drc_t retcode = DRC_NONE;  qstring* errbuf;  switch (msgid)  {  case debugger_t::ev_init_debugger:  {    const char* hostname = va_arg(va, const char*);    int portnum = va_arg(va, int);    const char* password = va_arg(va, const char*);    errbuf = va_arg(va, qstring*);    QASSERT(1522, errbuf != NULL);    retcode = init_debugger(hostname, portnum, password, errbuf);  }  break;  case debugger_t::ev_term_debugger:    retcode = term_debugger();    break;  case debugger_t::ev_get_processes:  {    procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = s_get_processes(procs, errbuf);  }  break;  case debugger_t::ev_start_process:  {    const char* path = va_arg(va, const char*);    const char* args = va_arg(va, const char*);    const char* startdir = va_arg(va, const char*);    uint32 dbg_proc_flags = va_arg(va, uint32);    const char* input_path = va_arg(va, const char*);    uint32 input_file_crc32 = va_arg(va, uint32);    errbuf = va_arg(va, qstring*);    retcode = s_start_process(path,      args,      startdir,      dbg_proc_flags,      input_path,      input_file_crc32,      errbuf);  }  break;  case debugger_t::ev_get_debapp_attrs:  {    debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);    out_pattrs->addrsize = 3;    out_pattrs->is_be = false;    out_pattrs->platform = "bsnes";    out_pattrs->cbsize = sizeof(debapp_attrs_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_rebase_if_required_to:  {    ea_t new_base = va_arg(va, ea_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_request_pause:    errbuf = va_arg(va, qstring*);    retcode = prepare_to_pause_process(errbuf);    break;  case debugger_t::ev_exit_process:    errbuf = va_arg(va, qstring*);    retcode = emul_exit_process(errbuf);    break;  case debugger_t::ev_get_debug_event:  {    gdecode_t* code = va_arg(va, gdecode_t*);    debug_event_t* event = va_arg(va, debug_event_t*);    int timeout_ms = va_arg(va, int);    *code = get_debug_event(event, timeout_ms);    retcode = DRC_OK;  }  break;  case debugger_t::ev_resume:  {    debug_event_t* event = va_arg(va, debug_event_t*);    retcode = continue_after_event(event);  }  break;  case debugger_t::ev_thread_suspend:  {    thid_t tid = va_argi(va, thid_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_thread_continue:  {    thid_t tid = va_argi(va, thid_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_set_resume_mode:  {    thid_t tid = va_argi(va, thid_t);    resume_mode_t resmod = va_argi(va, resume_mode_t);    retcode = s_set_resume_mode(tid, resmod);  }  break;  case debugger_t::ev_read_registers:  {    thid_t tid = va_argi(va, thid_t);    int clsmask = va_arg(va, int);    regval_t* values = va_arg(va, regval_t*);    errbuf = va_arg(va, qstring*);    retcode = read_registers(tid, clsmask, values, errbuf);  }  break;  case debugger_t::ev_write_register:  {    thid_t tid = va_argi(va, thid_t);    int regidx = va_arg(va, int);    const regval_t* value = va_arg(va, const regval_t*);    errbuf = va_arg(va, qstring*);    retcode = write_register(tid, regidx, value, errbuf);  }  break;  case debugger_t::ev_get_memory_info:  {    meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = get_memory_info(*ranges, errbuf);  }  break;  case debugger_t::ev_read_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = read_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_write_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    const void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = write_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_check_bpt:  {    int* bptvc = va_arg(va, int*);    bpttype_t type = va_argi(va, bpttype_t);    ea_t ea = va_arg(va, ea_t);    int len = va_arg(va, int);    *bptvc = is_ok_bpt(type, ea, len);    retcode = DRC_OK;  }  break;  case debugger_t::ev_update_bpts:  {    int* nbpts = va_arg(va, int*);    update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);    int nadd = va_arg(va, int);    int ndel = va_arg(va, int);    errbuf = va_arg(va, qstring*);    retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);  }  break;  default:    retcode = DRC_NONE;  }  return retcode;}debugger_t debugger{    IDD_INTERFACE_VERSION,    NAME,    0x8000 + 6581, // (6)    "65816",    DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |    DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,    DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,    register_classes,    RC_CPU,    registers,    qnumber(registers),    0x1000,    NULL,    0,    0,    DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,    NULL,    idd_notify};

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


Вторым важным изменением стало введением "стандартизированных" кодов возврата у функций отладчика drc_t. Тут всё просто: если функция отработала без ошибок, возвращаем DRC_OK, иначе DRC_FAILED.


Остальные инклуды:


ida_registers.h
#pragma once#define RC_CPU (1 << 0)#define RC_PPU (1 << 1)enum class SNES_REGS : uint8_t{    SR_A,    SR_X,    SR_Y,    SR_D,    SR_DB,    SR_PC,    SR_S,    SR_P,    SR_MFLAG,    SR_XFLAG,    SR_EFLAG,};

ida_debmod.h
#pragma once#include <deque>#include <ida.hpp>#include <idd.hpp>//--------------------------------------------------------------------------// Very simple class to store pending eventsenum queue_pos_t{    IN_FRONT,    IN_BACK};struct eventlist_t : public std::deque<debug_event_t>{private:    bool synced;public:    // save a pending event    void enqueue(const debug_event_t &ev, queue_pos_t pos)    {        if (pos != IN_BACK)            push_front(ev);        else            push_back(ev);    }    // retrieve a pending event    bool retrieve(debug_event_t *event)    {        if (empty())            return false;        // get the first event and return it        *event = front();        pop_front();        return true;    }};

В ida_registers.h мы просто перечисляем список регистров для удоства обращений к ним в коде, а в ida_debmod.h описан формат eventlist_t, который мы будем использовать для хранения событий, с которыми будет работать IDA.


Подготовка завершена


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


  1. Эмулятор с функцией отладки должен уметь реагировать на запросы Иды "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  2. Эмулятор также должен: уведомлять IDA о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"
  3. Ида должна уметь сообщать эмулятору о том, что есть необходимость: "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  4. Ида должна реагировать на сообщения от эмулятора о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"

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


  1. IDA => эмулятор
  2. Эмулятор => IDA

Учитывая это, можно, опять же, пойти по стопам предыдущей статьи про сеговский отладчик, а можно захотеть использовать "модные и современные" технологии для реализации RPC и сериализации любых данных. Мой выбор пал в сторону Thrift, т.к. с ним работать гораздо удобнее, и он практически не требует дополнительной подготовки (как, например, доклеивание RPC в protobuf, но тут, скорее, на любителя). Единственная сложность, это компиляция сего зверя, но, я оставлю это за рамками данной статьи.


Thrift пишем прототип RPC


Давайте ещё раз посмотрим на те 4 пункта, которые я описал выше, и которые мы всё ещё держим в голове, откроем блокнот, и напишем что-то вроде этого:


service IdaClient {  oneway void start_event(),  oneway void add_visited(1:set<i32> visited, 2:bool is_step),  oneway void pause_event(1:i32 address),  oneway void stop_event(),}

Как видим, в Thrift нету ничего сложного. Здесь мы описали сервис IdaClient, которым будет пользоваться эмулятор, и обработчик которого будет располагаться в IDA. Все эти методы помечены ключевым словом oneway, т.к., по сути, нам не нужно дожидаться их выполнения, и в принципе ожидать, что их обработают.


start_event() будет сообщать Иде о том, что ром выбрал и его эмуляция началась.


add_visited() метод, с помощью которого мы будем сообщать в Иду о том коде, который был выполнен эмулятором. Это полезно при отладке как раз таки ретро-платформ, т.к. в ромах для них код часто перемежается с данными. Если таковой функции в выбранном вами эмуляторе нет, её можно также пропустить и в протоколе.


pause_event() этим методом мы будем сообщать Иде о том, что произошла пауза эмуляции по какой-либо причине: будь то брейкпоинт, завершился шаг при StepInto или StepOver или какой-то другой причине. В качестве нагрузки данный метод будет также передавать адрес, где именно произошла остановка.


stop_event() думаю, тут всё понятно. Эмуляция завершилась, например, по причине завершения процесса эмуляции.


С этим разобрались, теперь часть посложнее отладочный RPC:


service BsnesDebugger {  i32 get_cpu_reg(1:BsnesRegister reg),  BsnesRegisters get_cpu_regs(),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),}

Здесь у нас описана серверная часть, которая будет крутиться в эмуляторе, и к которой Ида время от времени будет приставать. Давайте разберём её более детально:


  i32 get_cpu_reg(1:BsnesRegister reg),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),

Эти методы мы будем использовать тогда, когда нам потребуется прочитать или записать один регистр. Использованный enum BsnesRegister выглядит так:


enum BsnesRegister {  pc,  a,  x,  y,  s,  d,  db,  p,  mflag,  xflag,  eflag,}

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


Т.к. IDA сама никогда не запрашивает по одному регистру, а требует все сразу, напишем метод, который будет их все сразу и отдавать:


struct BsnesRegisters {  1:i32 pc,  2:i32 a,  3:i32 x,  4:i32 y,  5:i32 s,  6:i32 d,  7:i16 db,  8:i16 p,  9:i8 mflag,  10:i8 xflag,  11:i8 eflag,}service BsnesDebugger {  ...  BsnesRegisters get_cpu_regs(),  ...}

Здесь я завёл одну общую структуру под регистры, указав их размеры и указал её в качестве возвращаемого значения для метода get_cpu_regs().


Теперь работа с памятью:


enum DbgMemorySource {  CPUBus,  APUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  CartROM,  CartRAM,  SA1Bus,  SFXBus,  SGBBus,  SGBROM,  SGBRAM,}service BsnesDebugger {  ...  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  ...}

Здесь мы использовали встроенный в Thrift тип данных binary, и указали различные области памяти, которые могут быть прочитаны (взято из эмулятора).


Теперь пришла очередь брейкпоинтов:


enum BpType {  BP_PC = 1,  BP_READ = 2,  BP_WRITE = 4,}enum DbgBptSource {  CPUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  SA1Bus,  SFXBus,  SGBBus,}struct DbgBreakpoint {  1:BpType type,  2:i32 bstart,  3:i32 bend,  4:bool enabled,  5:DbgBptSource src,}service BsnesDebugger {  ...  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  ...}

Т.к. список областей памяти, которые можно читать, и на которые можно ставить брейкпоинты отличаются, заводим отдельный список DbgBptSource. Также указываем тип брейкпоинта BpType и адрес его начала/конца bstart/bend. Ещё нам может понадобиться включать брейкпоинт не сразу enabled.


С основными сложными частями протокола закончили, теперь можно описать более простые:


service BsnesDebugger {  ...  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),  ...}

Метод pause() будет приостанавливать процесс отладки по запросу от IDA, resume() продолжать.


start_emulation() нужен для того, чтобы IDA могла сообщить эмулятору, что она начала процесс отладки, и ожидает от него какие-либо события. Фактически, используется в качестве синхронизации начала эмуляции между плагином-отладчиком и собственно эмулятором.


exit_emulation() на случай, если мы захотим остановить отладку из IDA, а не из эмулятора.


step_into() и step_over() пошаговая отладка.


Итоговый debug_proto.thrift
enum BsnesRegister {  pc,  a,  x,  y,  s,  d,  db,  p,  mflag,  xflag,  eflag,}struct BsnesRegisters {  1:i32 pc,  2:i32 a,  3:i32 x,  4:i32 y,  5:i32 s,  6:i32 d,  7:i16 db,  8:i16 p,  9:i8 mflag,  10:i8 xflag,  11:i8 eflag,}enum BpType {  BP_PC = 1,  BP_READ = 2,  BP_WRITE = 4,}enum DbgMemorySource {  CPUBus,  APUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  CartROM,  CartRAM,  SA1Bus,  SFXBus,  SGBBus,  SGBROM,  SGBRAM,}enum DbgBptSource {  CPUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  SA1Bus,  SFXBus,  SGBBus,}struct DbgBreakpoint {  1:BpType type,  2:i32 bstart,  3:i32 bend,  4:bool enabled,  5:DbgBptSource src,}service BsnesDebugger {  i32 get_cpu_reg(1:BsnesRegister reg),  BsnesRegisters get_cpu_regs(),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),}service IdaClient {  oneway void start_event(),  oneway void add_visited(1:set<i32> changed, 2:bool is_step),  oneway void pause_event(1:i32 address),  oneway void stop_event(),}

От RPC-прототипа к реализации


На этом процесс написания RPC-прототипа завершён. Чтобы сгенерировать из него код для языка C++, качаем Thrift-компилятор, выполняем из командной строки следующее:


thrift --gen cpp debug_proto.thrift

На выходе мы получим каталог gen-cpp, в котором нас будут ждать не только файлики, которые нужно будет компилировать вместе с проектом, но и шаблон кода каждого из сервисов IdaClient и BsnesDebugger.



Добавляем сгенерированные файлы в студийный проект (кроме файлов *_server.skeleton.cpp). Также необходимо слинковать наш проект плагина (и эмулятора) со скомпилированными статичными библиотеками thrift-а и libevent-а (мы будем использовать "nonblocking" вариант Thrift). У этих библиотек имеется CMake вариант сборки, который значительно упрощает процесс.


Код IdaClient хэндлера


Теперь давайте напишем шаблон кода, реализующий IdaClient-сервис:


Необходимые инклуды и адресные пространства
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;::std::shared_ptr<BsnesDebuggerClient> client;::std::shared_ptr<TNonblockingServer> srv;::std::shared_ptr<TTransport> cli_transport;

Реализация серверной части IdaClient
static void pause_execution(){  try {    if (client) {      client->pause();    }  }  catch (...) {  }}static void continue_execution(){  try {    if (client) {      client->resume();    }  }  catch (...) {  }}static void stop_server() {  try {    srv->stop();  }  catch (...) {  }}static void finish_execution(){  try {    if (client) {      client->exit_emulation();    }  }  catch (...) {  }  stop_server();}class IdaClientHandler : virtual public IdaClientIf {public:    void pause_event(const int32_t address) override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = address | 0x800000;    ev.handled = true;    ev.set_eid(PROCESS_SUSPENDED);    events.enqueue(ev, IN_BACK);    }    void start_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = BADADDR;    ev.handled = true;    ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");    ev.set_modinfo(PROCESS_STARTED).base = 0;    ev.set_modinfo(PROCESS_STARTED).size = 0;    ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;    events.enqueue(ev, IN_BACK);    }    void stop_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.handled = true;    ev.set_exit_code(PROCESS_EXITED, 0);    events.enqueue(ev, IN_BACK);    }  void add_visited(const std::set<int32_t>& changed, bool is_step) override {  }};

В этом коде мы реагируем на события эмуляции и сообщаем о них Иде, добавляя эти события в список. Более подробно о них можно прочитать в той же статье про отладчик для Сеги. Код add_visited() пока оставляем пустым. О нём позже.


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


init_ida_server и init_emu_client
static void init_ida_server() {    try {    ::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());    ::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));    ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));    ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());    srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));    ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());    ::std::shared_ptr<Thread> thread = tf->newThread(srv);    thread->start();    } catch (...) {    }}static void init_emu_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));  show_wait_box("Waiting for BSNES-PLUS emulation...");  while (true) {    if (user_cancelled()) {      break;    }    try {      cli_transport->open();      break;    }    catch (...) {    }  }  hide_wait_box();}

Осталось дополнить имеющийся шаблон ida_debug.cpp кодом для работы со Thrift. Вот что получилось:


Полный код ida_debug.cpp
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;#include <ida.hpp>#include <dbg.hpp>#include <auto.hpp>#include <deque>#include <mutex>#include "ida_plugin.h"#include "ida_debmod.h"#include "ida_registers.h"::std::shared_ptr<BsnesDebuggerClient> client;::std::shared_ptr<TNonblockingServer> srv;::std::shared_ptr<TTransport> cli_transport;static ::std::mutex list_mutex;static eventlist_t events;static const char* const p_reg[] ={    "CF",    "ZF",    "IF",    "DF",    "XF",    "MF",    "VF",    "NF",};static register_info_t registers[] = {    {"A", 0, RC_CPU, dt_word, NULL, 0},    {"X", 0, RC_CPU, dt_word, NULL, 0},    {"Y", 0, RC_CPU, dt_word, NULL, 0},    {"D", 0, RC_CPU, dt_word, NULL, 0},    {"DB", 0, RC_CPU, dt_byte, NULL, 0},    {"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},  {"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},    {"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},  {"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},  {"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},    {"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},};static const char* register_classes[] = {    "General Registers",    NULL};static struct apply_codemap_req : public exec_request_t {private:  const std::set<int32_t>& _changed;  const bool _is_step;public:  apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};  int idaapi execute(void) override {    auto m = _changed.size();    if (!_is_step) {      show_wait_box("Applying codemap: %d/%d...", 1, m);    }    auto x = 0;    for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {      if (!_is_step && user_cancelled()) {        break;      }      if (!_is_step) {        replace_wait_box("Applying codemap: %d/%d...", x, m);      }      ea_t addr = (ea_t)(*i | 0x800000);      auto_make_code(addr);      plan_ea(addr);      show_addr(addr);      x++;    }    if (!_is_step) {      hide_wait_box();    }    return 0;  }};static void apply_codemap(const std::set<int32_t>& changed, bool is_step){  if (changed.empty()) return;  apply_codemap_req req(changed, is_step);  execute_sync(req, MFF_FAST);}static void pause_execution(){  try {    if (client) {      client->pause();    }  }  catch (...) {  }}static void continue_execution(){  try {    if (client) {      client->resume();    }  }  catch (...) {  }}static void stop_server() {  try {    srv->stop();  }  catch (...) {  }}static void finish_execution(){  try {    if (client) {      client->exit_emulation();    }  }  catch (...) {  }  stop_server();}class IdaClientHandler : virtual public IdaClientIf {public:    void pause_event(const int32_t address) override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = address | 0x800000;    ev.handled = true;    ev.set_eid(PROCESS_SUSPENDED);    events.enqueue(ev, IN_BACK);    }    void start_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = BADADDR;    ev.handled = true;    ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");    ev.set_modinfo(PROCESS_STARTED).base = 0;    ev.set_modinfo(PROCESS_STARTED).size = 0;    ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;    events.enqueue(ev, IN_BACK);    }    void stop_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.handled = true;    ev.set_exit_code(PROCESS_EXITED, 0);    events.enqueue(ev, IN_BACK);    }  void add_visited(const std::set<int32_t>& changed, bool is_step) override {    apply_codemap(changed, is_step);  }};static void init_ida_server() {    try {    ::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());    ::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));    ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));    ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());    srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));    ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());    ::std::shared_ptr<Thread> thread = tf->newThread(srv);    thread->start();    } catch (...) {    }}static void init_emu_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));  show_wait_box("Waiting for BSNES-PLUS emulation...");  while (true) {    if (user_cancelled()) {      break;    }    try {      cli_transport->open();      break;    }    catch (...) {    }  }  hide_wait_box();}static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf){  return DRC_OK;}static drc_t idaapi term_debugger(void){  finish_execution();  return DRC_OK;}static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {  process_info_t info;  info.name.sprnt("bsnes");  info.pid = 1;  procs->add(info);  return DRC_OK;}static drc_t idaapi s_start_process(const char* path,  const char* args,  const char* startdir,  uint32 dbg_proc_flags,  const char* input_path,  uint32 input_file_crc32,  qstring* errbuf = NULL){  ::std::lock_guard<::std::mutex> lock(list_mutex);  events.clear();  init_ida_server();  init_emu_client();  try {    if (client) {      client->start_emulation();    }  }  catch (...) {    return DRC_FAILED;  }  return DRC_OK;}static drc_t idaapi prepare_to_pause_process(qstring* errbuf){  pause_execution();  return DRC_OK;}static drc_t idaapi emul_exit_process(qstring* errbuf){  finish_execution();  return DRC_OK;}static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms){  while (true)  {    ::std::lock_guard<::std::mutex> lock(list_mutex);    // are there any pending events?    if (events.retrieve(event))    {      return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;    }    if (events.empty())      break;  }  return GDE_NO_EVENT;}static drc_t idaapi continue_after_event(const debug_event_t* event){  dbg_notification_t req = get_running_notification();  switch (event->eid())  {  case STEP:  case PROCESS_SUSPENDED:    if (req == dbg_null || req == dbg_run_to) {      continue_execution();    }    break;  case PROCESS_EXITED:    stop_server();    break;  }  return DRC_OK;}static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread{  switch (resmod)  {  case RESMOD_INTO:    ///< step into call (the most typical single stepping)    try {      if (client) {        client->step_into();      }    }    catch (...) {      return DRC_FAILED;    }    break;  case RESMOD_OVER:    ///< step over call    try {      if (client) {        client->step_over();      }    }    catch (...) {      return DRC_FAILED;    }    break;  }  return DRC_OK;}static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf){  if (clsmask & RC_CPU)  {        BsnesRegisters regs;    try {      if (client) {        client->get_cpu_regs(regs);                values[static_cast<int>(SNES_REGS::SR_PC)].ival = regs.pc | 0x800000;                values[static_cast<int>(SNES_REGS::SR_A)].ival = regs.a;                values[static_cast<int>(SNES_REGS::SR_X)].ival = regs.x;                values[static_cast<int>(SNES_REGS::SR_Y)].ival = regs.y;                values[static_cast<int>(SNES_REGS::SR_S)].ival = regs.s;                values[static_cast<int>(SNES_REGS::SR_D)].ival = regs.d;                values[static_cast<int>(SNES_REGS::SR_DB)].ival = regs.db;                values[static_cast<int>(SNES_REGS::SR_P)].ival = regs.p;        values[static_cast<int>(SNES_REGS::SR_MFLAG)].ival = regs.mflag;        values[static_cast<int>(SNES_REGS::SR_XFLAG)].ival = regs.xflag;                values[static_cast<int>(SNES_REGS::SR_EFLAG)].ival = regs.eflag;      }    }    catch (...) {      return DRC_FAILED;    }    }    return DRC_OK;}static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf){  if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {    try {      if (client) {        client->set_cpu_reg(static_cast<BsnesRegister::type>(regidx), value->ival & 0xFFFFFFFF);      }    }    catch (...) {      return DRC_FAILED;    }    }    return DRC_OK;}static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf){  memory_info_t info;  info.start_ea = 0x0000;  info.end_ea = 0x01FFF;  info.sclass = "STACK";  info.bitness = 0;  info.perm = SEGPERM_READ | SEGPERM_WRITE;  areas.push_back(info);  // Don't remove this loop  for (int i = 0; i < get_segm_qty(); ++i)  {    segment_t* segm = getnseg(i);    info.start_ea = segm->start_ea;    info.end_ea = segm->end_ea;    qstring buf;    get_segm_name(&buf, segm);    info.name = buf;    get_segm_class(&buf, segm);    info.sclass = buf;    info.sbase = get_segm_base(segm);    info.perm = segm->perm;    info.bitness = segm->bitness;    areas.push_back(info);  }  // Don't remove this loop    return DRC_OK;}static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf){  std::string mem;  try {    if (client) {      client->read_memory(mem, DbgMemorySource::CPUBus, (int32_t)ea, (int32_t)size);      memcpy(&((unsigned char*)buffer)[0], mem.c_str(), size);    }  }  catch (...) {    return DRC_FAILED;  }  return size;}static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf){  std::string mem((const char*)buffer);  try {    if (client) {      client->write_memory(DbgMemorySource::CPUBus, (int32_t)ea, mem);    }  }  catch (...) {    return 0;  }  return size;}static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len){  DbgMemorySource::type btype = DbgMemorySource::CPUBus;  switch (btype) {  case DbgMemorySource::CPUBus:  case DbgMemorySource::APURAM:  case DbgMemorySource::DSP:  case DbgMemorySource::VRAM:  case DbgMemorySource::OAM:  case DbgMemorySource::CGRAM:  case DbgMemorySource::SA1Bus:  case DbgMemorySource::SFXBus:    break;  default:    return BPT_BAD_TYPE;  }  switch (type)  {  case BPT_EXEC:  case BPT_READ:  case BPT_WRITE:  case BPT_RDWR:    return BPT_OK;  }  return BPT_BAD_TYPE;}static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf){  for (int i = 0; i < nadd; ++i)  {    ea_t start = bpts[i].ea;    ea_t end = bpts[i].ea + bpts[i].size - 1;    DbgBreakpoint bp;    bp.bstart = start;    bp.bend = end;    bp.enabled = true;    switch (bpts[i].type)    {    case BPT_EXEC:      bp.type = BpType::BP_PC;      break;    case BPT_READ:      bp.type = BpType::BP_READ;      break;    case BPT_WRITE:      bp.type = BpType::BP_WRITE;      break;    case BPT_RDWR:      bp.type = BpType::BP_READ;      break;    }    DbgMemorySource::type type = DbgMemorySource::CPUBus;    switch (type) {    case DbgMemorySource::CPUBus:      bp.src = DbgBptSource::CPUBus;      break;    case DbgMemorySource::APURAM:      bp.src = DbgBptSource::APURAM;      break;    case DbgMemorySource::DSP:      bp.src = DbgBptSource::DSP;      break;    case DbgMemorySource::VRAM:      bp.src = DbgBptSource::VRAM;      break;    case DbgMemorySource::OAM:      bp.src = DbgBptSource::OAM;      break;    case DbgMemorySource::CGRAM:      bp.src = DbgBptSource::CGRAM;      break;    case DbgMemorySource::SA1Bus:      bp.src = DbgBptSource::SA1Bus;      break;    case DbgMemorySource::SFXBus:      bp.src = DbgBptSource::SFXBus;      break;    default:      continue;    }    try {      if (client) {        client->add_breakpoint(bp);      }    }    catch (...) {      return DRC_FAILED;    }    bpts[i].code = BPT_OK;  }  for (int i = 0; i < ndel; ++i)  {    ea_t start = bpts[nadd + i].ea;    ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;    DbgBreakpoint bp;    bp.bstart = start;    bp.bend = end;    bp.enabled = true;    switch (bpts[i].type)    {    case BPT_EXEC:      bp.type = BpType::BP_PC;      break;    case BPT_READ:      bp.type = BpType::BP_READ;      break;    case BPT_WRITE:      bp.type = BpType::BP_WRITE;      break;    case BPT_RDWR:      bp.type = BpType::BP_READ;      break;    }    DbgMemorySource::type type = DbgMemorySource::CPUBus;    switch (type) {    case DbgMemorySource::CPUBus:      bp.src = DbgBptSource::CPUBus;      break;    case DbgMemorySource::APURAM:      bp.src = DbgBptSource::APURAM;      break;    case DbgMemorySource::DSP:      bp.src = DbgBptSource::DSP;      break;    case DbgMemorySource::VRAM:      bp.src = DbgBptSource::VRAM;      break;    case DbgMemorySource::OAM:      bp.src = DbgBptSource::OAM;      break;    case DbgMemorySource::CGRAM:      bp.src = DbgBptSource::CGRAM;      break;    case DbgMemorySource::SA1Bus:      bp.src = DbgBptSource::SA1Bus;      break;    case DbgMemorySource::SFXBus:      bp.src = DbgBptSource::SFXBus;      break;    default:      continue;    }    try {      if (client) {        client->del_breakpoint(bp);      }    }    catch (...) {      return DRC_FAILED;    }    bpts[nadd + i].code = BPT_OK;  }  *nbpts = (ndel + nadd);  return DRC_OK;}static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {  drc_t retcode = DRC_NONE;  qstring* errbuf;  switch (msgid)  {  case debugger_t::ev_init_debugger:  {    const char* hostname = va_arg(va, const char*);    int portnum = va_arg(va, int);    const char* password = va_arg(va, const char*);    errbuf = va_arg(va, qstring*);    QASSERT(1522, errbuf != NULL);    retcode = init_debugger(hostname, portnum, password, errbuf);  }  break;  case debugger_t::ev_term_debugger:    retcode = term_debugger();    break;  case debugger_t::ev_get_processes:  {    procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = s_get_processes(procs, errbuf);  }  break;  case debugger_t::ev_start_process:  {    const char* path = va_arg(va, const char*);    const char* args = va_arg(va, const char*);    const char* startdir = va_arg(va, const char*);    uint32 dbg_proc_flags = va_arg(va, uint32);    const char* input_path = va_arg(va, const char*);    uint32 input_file_crc32 = va_arg(va, uint32);    errbuf = va_arg(va, qstring*);    retcode = s_start_process(path,      args,      startdir,      dbg_proc_flags,      input_path,      input_file_crc32,      errbuf);  }  break;  case debugger_t::ev_get_debapp_attrs:  {    debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);    out_pattrs->addrsize = 3;    out_pattrs->is_be = false;    out_pattrs->platform = "snes";    out_pattrs->cbsize = sizeof(debapp_attrs_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_rebase_if_required_to:  {    ea_t new_base = va_arg(va, ea_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_request_pause:    errbuf = va_arg(va, qstring*);    retcode = prepare_to_pause_process(errbuf);    break;  case debugger_t::ev_exit_process:    errbuf = va_arg(va, qstring*);    retcode = emul_exit_process(errbuf);    break;  case debugger_t::ev_get_debug_event:  {    gdecode_t* code = va_arg(va, gdecode_t*);    debug_event_t* event = va_arg(va, debug_event_t*);    int timeout_ms = va_arg(va, int);    *code = get_debug_event(event, timeout_ms);    retcode = DRC_OK;  }  break;  case debugger_t::ev_resume:  {    debug_event_t* event = va_arg(va, debug_event_t*);    retcode = continue_after_event(event);  }  break;  case debugger_t::ev_thread_suspend:  {    thid_t tid = va_argi(va, thid_t);    pause_execution();    retcode = DRC_OK;  }  break;  case debugger_t::ev_thread_continue:  {    thid_t tid = va_argi(va, thid_t);    continue_execution();    retcode = DRC_OK;  }  break;  case debugger_t::ev_set_resume_mode:  {    thid_t tid = va_argi(va, thid_t);    resume_mode_t resmod = va_argi(va, resume_mode_t);    retcode = s_set_resume_mode(tid, resmod);  }  break;  case debugger_t::ev_read_registers:  {    thid_t tid = va_argi(va, thid_t);    int clsmask = va_arg(va, int);    regval_t* values = va_arg(va, regval_t*);    errbuf = va_arg(va, qstring*);    retcode = read_registers(tid, clsmask, values, errbuf);  }  break;  case debugger_t::ev_write_register:  {    thid_t tid = va_argi(va, thid_t);    int regidx = va_arg(va, int);    const regval_t* value = va_arg(va, const regval_t*);    errbuf = va_arg(va, qstring*);    retcode = write_register(tid, regidx, value, errbuf);  }  break;  case debugger_t::ev_get_memory_info:  {    meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = get_memory_info(*ranges, errbuf);  }  break;  case debugger_t::ev_read_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = read_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_write_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    const void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = write_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_check_bpt:  {    int* bptvc = va_arg(va, int*);    bpttype_t type = va_argi(va, bpttype_t);    ea_t ea = va_arg(va, ea_t);    int len = va_arg(va, int);    *bptvc = is_ok_bpt(type, ea, len);    retcode = DRC_OK;  }  break;  case debugger_t::ev_update_bpts:  {    int* nbpts = va_arg(va, int*);    update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);    int nadd = va_arg(va, int);    int ndel = va_arg(va, int);    errbuf = va_arg(va, qstring*);    retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);  }  break;  default:    retcode = DRC_NONE;  }  return retcode;}debugger_t debugger{    IDD_INTERFACE_VERSION,    NAME,    0x8000 + 6581, // (6)    "65816",    DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |    DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,    DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,    register_classes,    RC_CPU,    registers,    qnumber(registers),    0x1000,    NULL,    0,    0,    DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,    NULL,    idd_notify};

Дабы не описывать весь этот код, здесь я опишу лишь типичный код для работы со Thrift со стороны IDA:


    try {      if (client) {        client->step_over();      }    }    catch (...) {      return DRC_FAILED;    }    return DRC_OK;

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


Код BsnesDebugger хэндлера


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


remote_debugger.cpp
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;#include "../ui-base.hpp"static ::std::shared_ptr<IdaClientClient> client;static ::std::shared_ptr<TNonblockingServer> srv;static ::std::shared_ptr<TTransport> cli_transport;static ::std::mutex list_mutex;::std::set<int32_t> visited;static void send_visited(bool is_step) {  const auto part = visited.size();  ::std::lock_guard<::std::mutex> lock(list_mutex);  try {    if (client) {      client->add_visited(visited, is_step);    }  }  catch (...) {  }  visited.clear();}static void stop_client() {  try {    if (client) {      send_visited(false);      client->stop_event();    }    cli_transport->close();  }  catch (...) {  }}static void init_ida_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9091));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<IdaClientClient>(new IdaClientClient(protocol));  while (true) {    try {      cli_transport->open();      break;    }    catch (...) {      Sleep(10);    }  }  atexit(stop_client);}static void toggle_pause(bool enable) {  application.debug = enable;  application.debugrun = enable;  if (enable) {    audio.clear();  }}class BsnesDebuggerHandler : virtual public BsnesDebuggerIf {public:  int32_t get_cpu_reg(const BsnesRegister::type reg) override {    switch (reg) {    case BsnesRegister::pc:    case BsnesRegister::a:    case BsnesRegister::x:    case BsnesRegister::y:    case BsnesRegister::s:    case BsnesRegister::d:    case BsnesRegister::db:    case BsnesRegister::p:      return SNES::cpu.getRegister((SNES::CPUDebugger::Register)reg);    case BsnesRegister::mflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;    case BsnesRegister::xflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;    case BsnesRegister::eflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;    }  }  void get_cpu_regs(BsnesRegisters& _return) override {    _return.pc = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterPC);    _return.a = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterA);    _return.x = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterX);    _return.y = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterY);    _return.s = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterS);    _return.d = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterD);    _return.db = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterDB);    _return.p = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterP);    _return.mflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;    _return.xflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;    _return.eflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;  }  void set_cpu_reg(const BsnesRegister::type reg, const int32_t value) override {    switch (reg) {    case BsnesRegister::pc:    case BsnesRegister::a:    case BsnesRegister::x:    case BsnesRegister::y:    case BsnesRegister::s:    case BsnesRegister::d:    case BsnesRegister::db:    case BsnesRegister::p:      SNES::cpu.setRegister((SNES::CPUDebugger::Register)reg, value);    }  }  void add_breakpoint(const DbgBreakpoint& bpt) override {    SNES::Debugger::Breakpoint add;    add.addr = bpt.bstart;    add.addr_end = bpt.bend;    add.mode = bpt.type;    add.source = (SNES::Debugger::Breakpoint::Source)bpt.src;    SNES::debugger.breakpoint.append(add);  }  void del_breakpoint(const DbgBreakpoint& bpt) override {    for (auto i = 0; i < SNES::debugger.breakpoint.size(); ++i) {      auto b = SNES::debugger.breakpoint[i];      if (b.source == (SNES::Debugger::Breakpoint::Source)bpt.src && b.addr == bpt.bstart && b.addr_end == bpt.bend && b.mode == bpt.type) {        SNES::debugger.breakpoint.remove(i);        break;      }    }  }  void read_memory(std::string& _return, const DbgMemorySource::type src, const int32_t address, const int32_t size) override {    _return.clear();    SNES::debugger.bus_access = true;    for (auto i = 0; i < size; ++i) {      _return += SNES::debugger.read((SNES::Debugger::MemorySource)src, address + i);    }    SNES::debugger.bus_access = false;  }  void write_memory(const DbgMemorySource::type src, const int32_t address, const std::string& data) override {    SNES::debugger.bus_access = true;    for (auto i = 0; i < data.size(); ++i) {      SNES::debugger.write((SNES::Debugger::MemorySource)src, address, data[i]);    }    SNES::debugger.bus_access = false;  }  void exit_emulation() override {    try {      if (client) {        send_visited(false);        client->stop_event();      }    }    catch (...) {    }    application.app->exit();  }  void pause() override {    step_into();  }  void resume() override {    toggle_pause(false);  }  void start_emulation() override {    init_ida_client();    try {      if (client) {        client->start_event();        visited.clear();        client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));      }    }    catch (...) {    }  }  void step_into() override {    SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;    application.debugrun = true;    SNES::debugger.step_cpu = true;  }  void step_over() override {    SNES::debugger.step_type = SNES::Debugger::StepType::StepOver;    SNES::debugger.step_over_new = true;    SNES::debugger.call_count = 0;    application.debugrun = true;    SNES::debugger.step_cpu = true;  }};static void stop_server() {  srv->stop();}void init_dbg_server() {  ::std::shared_ptr<BsnesDebuggerHandler> handler(new BsnesDebuggerHandler());  ::std::shared_ptr<TProcessor> processor(new BsnesDebuggerProcessor(handler));  ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9090));  ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());  ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());  srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));  ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());  ::std::shared_ptr<Thread> thread = tf->newThread(srv);  thread->start();  atexit(stop_server);  SNES::debugger.breakpoint.reset();  SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;  application.debugrun = true;  SNES::debugger.step_cpu = true;}void send_pause_event(bool is_step) {  try {    if (client) {      client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));      send_visited(is_step);    }  }  catch (...) {  }}

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


Часть объектов и методов я не делал статичными, т.к. к ним нам нужно будет обращаться из других участков кода эмулятора. Эти методы и объекты представлены в следующем списке:


  • ::std::set<int32_t> visited; сюда мы будем добавлять код, который выполнялся во время эмуляции, и который мы будем отправлять в Иду
  • void init_dbg_server() будем запускать RPC-сервер не при запуске эмулятора, а при запуске эмуляции выбранного рома
  • void send_pause_event(bool is_step) данный метод я использую не только для уведомления Иды о том, что эмуляция приостановлена, но и для отправки перед этим карты кода (codemap). Подробнее про параметр bool is_step и codemap я расскажу чуть позже

Теперь остаётся найти, где же эмулятору стоит сообщать о паузе, где начинается эмуляция, и где заполняется карта кода. Вот эти места:


Выполнение одной инструкции:


alwaysinline uint8_t CPUDebugger::op_readpc() {  extern std::set<int32_t> visited; // я решил не использовать отдельный header  visited.insert(regs.pc); // вставляем в карту кода текущее значение регистра PC  usage[regs.pc] |= UsageExec;  int offset = cartridge.rom_offset(regs.pc);  if (offset >= 0) cart_usage[offset] |= UsageExec;  // execute code without setting read flag  return CPU::op_read((regs.pc.b << 16) + regs.pc.w++);}

Открытие SNES рома:



Пошаговое исполнение:



Реакция на срабатывание брейкпоинта:



Хитрости применения codemap в Иде


Осталось рассказать о хитростях работы с функциями анализатора в IDA, и затем со спокойной (но переживающей "сомпилируется ли") душой нажать на Build Solution.


Оказалось, что просто так взять и в цикле выполнять функции, которые меняют IDB (файлы проектов в IDA) во время отладки нельзя будет вылетать через раз, и доводить своим непостоянством до сумасшествия. Нужно делать по-умному, например, вот так:


Как правильно менять IDB во время отладки
static struct apply_codemap_req : public exec_request_t {private:  const std::set<int32_t>& _changed;  const bool _is_step;public:  apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};  int idaapi execute(void) override {    auto m = _changed.size();    if (!_is_step) {      show_wait_box("Applying codemap: %d/%d...", 1, m);    }    auto x = 0;    for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {      if (!_is_step && user_cancelled()) {        break;      }      if (!_is_step) {        replace_wait_box("Applying codemap: %d/%d...", x, m);      }      ea_t addr = (ea_t)(*i | 0x800000);      auto_make_code(addr);      plan_ea(addr);      show_addr(addr);      x++;    }    if (!_is_step) {      hide_wait_box();    }    return 0;  }};static void apply_codemap(const std::set<int32_t>& changed, bool is_step){  if (changed.empty()) return;  apply_codemap_req req(changed, is_step);  execute_sync(req, MFF_FAST);}

Если вкратце, то суть в использовании метода execute_sync() и реализации своего варианта структуры exec_request_t и её колбэка int idaapi execute(void). Это рекомендованный разработчиками способ.


Выводы и компиляция


Фактически, мы закончили писать свой собственный плагин-отладчик для IDA. Мне показалось, что как раз для реализации общения между Идой и эмулятором и создания отладчика Thrift подошёл как нельзя кстати. С минимальными усилиями мне удалось написать и серверную и клиентскую часть для обеих сущностей, не городя велосипеды в виде открытия сокетов по разному для разных платформ, и изобретения RPC реализации с нуля.


К тому же, получившийся протокол легко масштабируется под другие методы и структуры и легко переносим.


Всем спасибо!




Подробнее..

Книга C для профи

19.04.2021 16:09:25 | Автор: admin
image Привет, Хаброжители! С++ популярный язык для создания ПО. В руках увлеченного программиста С++ становится прекрасным инструментом для создания лаконичного, эффективного и читаемого кода, которым можно гордиться.

C++ для профи адресован программистам среднего и продвинутого уровней, вы продеретесь сквозь тернии к самому ядру С++. Часть 1 охватывает основы языка С++ от типов и функций до жизненного цикла объектов и выражений. В части II представлена стандартная библиотека C ++ и библиотеки Boost. Вы узнаете о специальных вспомогательных классах, структурах данных и алгоритмах, а также о том, как управлять файловыми системами и создавать высокопроизводительные программы, которые обмениваются данными по сети.


Об этой книге

Современные программисты на C++ имеют доступ к ряду очень качественных книг, например Эффективный современный C++ Скотта Мейерса1 и Язык программирования C++ Бьёрна Страуструпа, 4-е издание2. Однако эти книги написаны для достаточно продвинутых программистов. Доступны также некоторые вводные тексты о C++, но они часто пропускают важные детали, потому что ориентированы на абсолютных новичков в программировании. Опытному программисту непонятно, где можно погрузиться в язык C++.

Я предпочитаю изучать сложные темы осознанно, выстраивая концепции из их основных элементов. Язык C++ имеет пугающую репутацию, потому что его фундаментальные элементы тесно связаны друг с другом, что затрудняет построение полной картины языка. Когда я изучал C++, то изо всех сил пытался сосредоточиться на языке, перескакивая от книг к видео и измученным коллегам. Поэтому и написал такую книгу, которую сам хотел бы иметь пять лет назад.

Кому будет интересна эта книга?

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

В ПОЗНАКОМИТЕСЬ С ОСНОВНМИ ФИШКАМИ СОВРЕМЕННОГО С++:

Базовые типы, ссылочные и пользовательские типы.
Полиморфизм во время компиляции и полиморфизм во время выполнения.
Жизненный цикл объекта, включая длительность хранения, стек вызовов, управление памятью, исключения и парадигму RAII.
Продвинутые выражения, операторы и функции.
Умные указатели, структуры данных, дата и время, числовые данные и др.
Контейнеры, итераторы, строки и алгоритмы.
Потоки и файлы, многозадачность, сетевое программирование и разработка приложений.

Отслеживание жизненного цикла объекта


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

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

Листинг 4.5. Класс Tracer и его конструктор с деструктором

#include <cstdio>struct Tracer {    Tracer(const char* name1) : name{ name }2 {       printf("%s constructed.\n", name); 3    }    ~Tracer() {       printf("%s destructed.\n", name); 4    }private:    const char* const name;};

Конструктор принимает один параметр 1 и сохраняет его в члене name 2. Затем он печатает сообщение, содержащее name 3. Деструктор 4 также выводит сообщение с name.

Рассмотрим программу в листинге 4.6. Четыре различных объекта Tracer имеют разную длительность хранения. Просматривая порядок вывода программы Tracer, вы можете проверить полученные знания о длительности хранения.

Листинг 4.6. Программа, использующая класс Tracer в листинге 4.5 для иллюстрации длительности хранения

#include <cstdio>struct Tracer {    --пропуск--};static Tracer t1{ "Static variable" }; 1thread_local Tracer t2{ "Thread-local variable" }; 2int main() {  const auto t2_ptr = &t2;  printf("A\n"); 3  Tracer t3{ "Automatic variable" }; 4  printf("B\n");  const auto* t4 = new Tracer{ "Dynamic variable" }; 5  printf("C\n");}

Листинг 4.6 содержит Tracer со статической 1, локальной поточной 2, автоматической 4 и динамической 5 длительностью хранения. Между каждой строкой в main выводится символ A, B или C для ссылки 3.

Запуск программы приводит к результату в листинге 4.7.

Листинг 4.7. Пример вывода из листинга 4.6

Static variable constructed.Thread-local variable constructed.A 3Automatic variable constructed.BDynamic variable constructed.CAutomatic variable destructed.Thread-local variable destructed.Static variable destructed.

Перед первой строкой main 3 статические и потоковые локальные переменные t1 и t2 были инициализированы 1 2. Это можно увидеть в листинге 4.7: обе переменные напечатали свои сообщения инициализации до A. Как и для любой автоматической переменной, область видимости t3 ограничена включающей функцией main. Соответственно t3 создается в месте инициализации сразу после A.

После B вы можете видеть сообщение, соответствующее инициализации t4 5. Обратите внимание, что соответствующее сообщение, генерируемое динамическим деструктором Tracer, отсутствует. Причина в том, что вы (намеренно) потеряли память для объекта, на который указывает t4. Поскольку команды delete t4 не было, деструктор никогда не будет вызван.

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

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

Исключения


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

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

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

Ключевое слово throw

Чтобы вызвать исключение, используйте ключевое слово throw, за которым следует бросаемый объект.

Большинство объектов являются бросаемыми. Однако рекомендуется использовать одно из исключений, доступных в stdlib, например std::runtime_error в заголовке <stdеxcept>. Конструктор runtime_error принимает const char* с нулевым символом в конце, описывающий природу состояния ошибки. Это сообщение можно получить с помощью метода what, который не принимает параметров.

Класс Groucho в листинге 4.8 создает исключение всякий раз при вызове метода forget с аргументом, равным 0xFACE.

Листинг 4.8. Класс Groucho

#include <stdexcept>#include <cstdio>struct Groucho {   void forget(int x) {      if (x == 0xFACE) {         throw1 std::runtime_error2{ "I'd be glad to make an exception." };      }      printf("Forgot 0x%x\n", x);    }};

Чтобы вызвать исключение, в листинге 4.8 используется ключевое слово throw 1, за которым следует объект std::runtime_error 2.

Использование блоков try-catch

Блоки try-catch используются для установки обработчиков исключений в блоке кода. Внутри блока try размещается код, который может вызвать исключение. Внутри блока catch указывается обработчик для каждого типа исключений, которые можно обработать.

Листинг 4.9 показывает использование блока try-catch для обработки исключений, генерируемых объектом Groucho.

В методе main создается объект Groucho, а затем устанавливается блок try-catch 1. В части try вызывается метод forget класса groucho с несколькими различными параметрами: 0xC0DE 2, 0xFACE 3 и 0xC0FFEE 4. Внутри части catch обрабатываются любые исключения std::runtime_error 5, выводя сообщение в консоли 6.

Листинг 4.9. Использование try-catch для обработки исключений класса Groucho

#include <stdexcept>#include <cstdio>struct Groucho {      --пропуск--};int main() {   Groucho groucho;   try { 1       groucho.forget(0xC0DE); 2       groucho.forget(0xFACE); 3       groucho.forget(0xC0FFEE); 4    } catch (const std::runtime_error& e5) {       printf("exception caught with message: %s\n", e.what()); 6    }}

При запуске программы в листинге 4.9 вы получите следующий вывод:

Forgot 0xc0deexception caught with message: I'd be glad to make an exception.

При вызове forget с параметром 0xC0DE 2 groucho выводит Forgot0xc0de и завершает выполнение. При вызове forget с параметром 0xFACE 3 groucho выдает исключение. Это исключение остановило нормальное выполнение программы, поэтому forget никогда больше не вызывается 4. Вместо этого исключение в полете перехватывается 5, а его сообщение выводится в консоль 6.

Классы исключений stdlib

Можно организовать классы в родительско-дочерние отношения, используя наследование. Наследование оказывает большое влияние на то, как код обрабатывает исключения. Существует простая и удобная иерархия существующих типов исключений, доступных для использования в stdlib. Стоит попытаться использовать эти типы для простых программ. Зачем изобретать велосипед?

Стандартные классы исключений

stdlib предоставляет стандартные классы исключений в заголовке <stdеxcept>. Они должны стать вашим первым причалом при программировании исключений. Суперклассом для всех стандартных классов исключений является класс std::exception. Все подклассы в std::exception могут быть разделены на три группы: логические ошибки (logic_error), ошибки выполнения (runtime_error) и ошибки языковой поддержки. Ошибки языковой поддержки обычно не относятся к вам как к программисту, но вы наверняка столкнетесь с логическими ошибками и ошибками выполнения. Рисунок 4.1 обобщает их отношения.

image


КРАТКИЙ КУРС ПО НАСЛЕДОВАНИЮ

Прежде чем вводить исключения stdlib, нужно понять простое наследование классов C++ на очень высоком уровне. Классы могут иметь подклассы, которые наследуют функциональность своих суперклассов. Синтаксис в листинге 4.10 определяет это отношение.

Листинг 4.10. Определение суперклассов и подклассов

struct Superclass {    int x;};struct Subclass : Superclass { 1    int y;    int foo() {      return x + y; 2    }};

В Superclass нет ничего особенного. Но вот объявление Subclass 1 является особенным. Оно определяет отношения наследования с использованием синтаксиса: Superclass. Subclass наследует члены от Superclass, которые не помечены как private. Это можно увидеть в действии, когда Subclass использует поле x 2. Это поле принадлежит Superclass, но поскольку Subclass наследует от Superclass, x доступно.

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

Логические ошибки

Логические ошибки происходят из класса logic_error. Как правило, можно избежать эти исключения путем более тщательного программирования. Основной пример логическое предусловие класса не выполняется, например, когда инвариант класса не может быть установлен. (Вспомните из главы 2, что инвариант класса это особенность класса, которая всегда верна.)

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

logic_error имеет несколько подклассов, о которых следует знать:

  • domain_error сообщает об ошибках, связанных с допустимым диапазоном ввода, особенно для математических функций. Например, квадратный корень поддерживает только неотрицательные числа (в реальном случае). Если передается отрицательный аргумент, функция квадратного корня может выдать domain_error.
  • Исключение invalid_argument сообщает, как правило, о неожиданных параметрах.
  • Исключение length_error сообщает, что какое-либо действие нарушит ограничение максимального размера.
  • Исключение out_of_range сообщает, что некоторое значение не находится в ожидаемом диапазоне. Каноническим примером является индексирование с проверкой границ в структуре данных.

Ошибки выполнения

Ошибки выполнения происходят из класса runtime_error. Эти исключения помогают сообщать об ошибках, которые выходят за рамки программы. Как и logic_error, runtime_error имеет несколько подклассов, которые могут оказаться полезными:

  • system_error сообщает, что операционная система обнаружила некоторую ошибку. Такого рода исключения могут тысячи раз встретиться на вашем пути. Внутри заголовка <system_error> находится большое количество кодов ошибок и их состояний. Когда создается system_error, информация об ошибке упаковывается, чтобы можно было определить природу ошибки. Метод .code() возвращает enumclass типа std::errc, который имеет большое количество значений, таких как bad_file_descriptor, timed_out и license_denied,
  • overflow_error и underflow_error сообщают об арифметическом переполнении и потере значимости соответственно.


Другие ошибки наследуются напрямую от exception. Распространенным является исключение bad_alloc, которое сообщает, что new не удалось выделить необходимую память для динамического хранения.

Ошибки языковой поддержки

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

Обработка исключений

Правила обработки исключений основаны на наследовании классов. Когда выбрасывается исключение, блок catch обрабатывает его, если тип выброшенного исключения соответствует типу исключения обработчика или если тип выброшенного исключения наследуется от типа исключения обработчика.

Например, следующий обработчик перехватывает любое исключение, которое наследуется от std::exception, включая std::logic_error:

try {   throw std::logic_error{ "It's not about who wrong "                          "it's not about who right" };} catch (std::exception& ex) {   // Обрабатывает std::logic_error. Поскольку он наследуется от std::exception}

Следующий специальный обработчик перехватывает любое исключение независимо от его типа:

try {  throw 'z'; // Don't do this.} catch (...) {  // Обрабатывает любое исключение, даже 'z'}

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

Можно обрабатывать различные типы исключений, происходящих из одного и того же блока try, объединяя операторы catch, как показано здесь:

try {  // Код, который может вызвать исключение  --пропуск--} catch (const std::logic_error& ex) {  // Запись исключения и завершение работы программы; найдена программная ошибка!  --пропуск--} catch (const std::runtime_error& ex) {  // Делаем все, что можно  --пропуск--} catch (const std::exception& ex) {  // Обработка любого исключения, наследуемого от std:exception,  // которое не является logic_error или runtime_error.  --пропуск--} catch (...) {  // Паника; было сгенерировано непредвиденное исключение  --пропуск--}

Обычно такой код можно увидеть в точке входа в программу.

ПЕРЕБРАСВАНИЕ ИСКЛЮЧЕНИЯ
В блоке catch можно использовать ключевое слово throw, чтобы возобновить поиск подходящего обработчика исключений. Это называется перебрасыванием исключения. Есть несколько необычных, но важных случаев, когда вы, возможно, захотите дополнительно проверить исключение, прежде чем обработать его, как показано в листинге 4.11.

Листинг 4.11. Перебрасывание ошибки

try {  // Код, который может вызвать system_error  --пропуск--} catch(const std::system_error& ex) {   if(ex.code()!= std::errc::permission_denied){   // Ошибка, не связанная с отказом в доступе     throw; 1}  // Восстановление после ошибки   --пропуск--}

В этом примере код, который может выдать system_error, помещается в блок trycatch.
Все системные ошибки обрабатываются, но, если это не ошибка EACCES (в доступе отказано), исключение перебрасывается 1. У этого подхода есть некоторые потери производительности, и полученный код часто оказывается излишне запутанным.

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

Листинг 4.12. Перехват конкретного исключения, но не перебрасывание


try {  // Генерация исключения PermissionDenied  --пропуск--} catch(const PermissionDenied& ex) {  // Восстановление после ошибки EACCES (отказано в доступе) 1  --пропуск--}

Если генерируется std::system_error, обработчик PermissionDenied 1 не поймает его. (Конечно, обработчик std::system_error все равно можно оставить, чтобы перехватывать такие исключения, если это необходимо.)

Пользовательские исключения

Программист может при необходимости определить свои собственные исключения; обычно эти пользовательские исключения наследуются от std::exception. Все классы из stdlib используют исключения, которые происходят от std::exception. Это позволяет легко перехватывать все исключения, будь то из вашего кода или из stdlib, с помощью одного блока catch.

Ключевое слово noexcept

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

bool is_odd(int x) noexcept {  return 1 == (x % 2);}

Функции с пометкой noexcept составляют жесткий контракт. При использовании функции, помеченной как noexcept, вы можете быть уверены, что функция не может вызвать исключение. В обмен на это вы должны быть предельно осторожны, когда помечаете собственную функцию как noexcept, так как компилятор не может это проверить. Если код выдает исключение внутри функции, помеченной как noexcept, это плохо. Среда выполнения C++ вызовет функцию std::terminate, которая по умолчанию завершит работу программы через abort. После такого программа не может быть восстановлена:

void hari_kari() noexcept {   throw std::runtime_error{ "Goodbye, cruel world." };}

Пометка функции ключевым словом noexcept позволяет оптимизировать код, полагаясь на то, что функция не может вызвать исключение. По сути, компилятор освобождается для использования семантики переноса, что может быть выполнено быстрее (подробнее об этом в разделе Семантика перемещения, с. 184).
ПРИМЕЧАНИЕ
Ознакомьтесь с правилом 14 Эффективного использования C++ Скотта Мейерса, чтобы подробно обсудить noexcept. Суть в том, что некоторые конструкторы переноса и операторы присваивания переноса могут выдавать исключение, например если им нужно выделить память, а система не работает. Если конструктор переноса или оператор присваивания переноса не указывает иное, компилятор должен предполагать, что перенос может вызвать исключение. Это отключает определенные оптимизации.

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

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

image

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

Стеки вызовов и обработка исключений

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

Выбрасывание исключений из деструктора

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

Допустим, генерируется исключение и во время размотки стека другое исключение генерируется деструктором во время обычной очистки. Теперь у вас есть два исключения в состоянии полета. Как среда выполнения C++ должна справляться с такой ситуацией?

У вас может быть свое мнение на этот счет, но среда выполнения вызовет функцию terminate (завершение). Рассмотрим листинг 4.13, который показывает, что может произойти при выбрасывании исключений из деструктора:

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

#include <cstdio>#include <stdexcept>struct CyberdyneSeries800 {  CyberdyneSeries800() {   printf("I'm a friend of Sarah Connor."); 1  }  ~CyberdyneSeries800() {    throw std::runtime_error{ "I'll be back." }; 2}};  int main() {    try {      CyberdyneSeries800 t800; 3      thro std::runtime_error{ "Come with me if you want to live." }; 4    } catch(const std::exception& e) { 5      printf("Caught exception: %s\n", e.what()); 6    }}----------------------------------------------------------------------I'm a friend of Sarah Connor. 

ПРИМЕЧАНИЕ
Листинг 4.13 вызывает std::terminate, поэтому в зависимости от операционной среды может быть показано всплывающее окно с уведомлением.

Во-первых, был объявлен класс CyberdyneSeries800, который имеет простой конструктор, который выводит сообщение 1, и воинственный деструктор, который генерирует необработанное исключение 2. В main определяется блок try, в котором инициализируется CyberdyneSeries800 под именем t800 3, и выбрасывается runtime_error 4. В лучшем случае блок catch 5 обработает это исключение, выведет его сообщение 6 и все выйдет изящно. Поскольку t800 это автоматическая переменная в блоке try, она разрушается во время обычного процесса поиска обработчика для исключения, которое было выброшено 4. А поскольку t800 создает исключение в своем деструкторе 2, программа вызывает std::terminate и внезапно завершается.

Как правило, обращайтесь с деструкторами так, как если бы они были noexcept.

Класс SimpleString


Используя расширенный пример, давайте рассмотрим, как конструкторы, деструкторы, члены и исключения объединяются. Класс SimpleString в листинге 4.14 позволяет добавлять строки в стиле C и выводить результат.

Листинг 4.14. Конструктор и деструктор класса SimpleString

#include <stdexcept>struct SimpleString {  SimpleString(size_t max_size) 1    : max_size{ max_size }, 2      length{} { 3    if (max_size == 0) {      throw std::runtime_error{ "Max size must be at least 1." }; 4    }    buffer = new char[max_size]; 5    buffer[0] = 0; 6    }   ~SimpleString() {     delete[] buffer; 7    }--пропуск--private:    size_t max_size;    char* buffer;    size_t length;};

Конструктор 1 принимает один параметр max_size. Это максимальная длина строки, которая включает символ завершения строки. Инициализатор члена 2 сохраняет эту длину в переменной-члене max_size. Это значение также используется в выражении new массива для выделения буфера для хранения данной строки 5. Полученный указатель сохраняется в buffer. Длина инициализируется нулем 3, и это гарантирует, что по крайней мере буфер будет достаточного размера для хранения нулевого байта 4. Поскольку строка изначально пуста, первый байт буфера заполняется нулем 6.
ПРИМЕЧАНИЕ
Поскольку max_size это size_t, он не имеет знака и не может быть отрицательным, поэтому не нужно проверять это фиктивное условие.

Класс SimpleString владеет ресурсом памятью, на которую указывает буфер, которая должна быть освобождена при прекращении использования. Деструктор содержит одну строку 7, которая освобождает buffer. Поскольку распределение и освобождение buffer связаны конструктором и деструктором SimpleString, память никогда не будет потеряна.

Этот шаблон называется получение ресурса есть инициализация (RAII), или получение конструктора освобождение деструктора (CADRe).
ПРИМЕЧАНИЕ
Класс SimpleString все еще имеет неявно определенный конструктор копирования. Несмотря на то что память не может быть потеряна, при копировании класс потенциально освободится вдвое. Вы узнаете о конструкторах копирования в разделеСемантике копирования, с. 176. Просто знайте, что листинг 4.14 это обучающий инструмент, а не рабочий код.

Добавление и вывод

Класс SimpleString пока не очень полезен. В листинг 4.15 добавлена возможность выводить строку и добавлять набор символов в конец строки.

Листинг 4.15. Методы print и append_line для SimpleString

#include <cstdio>#include <cstring>#include <stdexcept>struct SimpleString {  --пропуск--  void print(const char* tag) const { 1    printf("%s: %s", tag, buffer);  }  bool append_line(const char* x) { 2  const auto x_len = strlen3(x);  if (x_len + length + 2 > max_size) return false; 4  std::strncpy(buffer + length, x, max_size - length);  length += x_len;  buffer[length++] = '\n';  buffer[length] = 0;  return true; } --пропуск--};


Первый метод print 1 выводит строку. Для удобства можно предоставить строку tag, чтобы можно было сопоставить вызов print с результатом. Этот метод является постоянным, потому что нет необходимости изменять состояние SimpleString.

Метод append_line 2 принимает строку с нулем в конце и добавляет ее содержимое плюс символ новой строки в buffer. Он возвращает true, если был успешно добавлен, и false, если не было достаточно места. Во-первых, append_line должен определить длину x. Для этого используется функция strlen 3 из заголовка <сstring>, которая принимает строку с нулевым символом в конце и возвращает ее длину:

size_t strlen(const char* str);

strlen используется для вычисления длины x и инициализации x_len с результатом. Этот результат используется для вычисления того, приведет ли добавление x (символов новой строки) и нулевого байта к текущей строке к получению строки с длиной, превышающей max_size 4. Если это так, append_line возвращает false.

Если для добавления x достаточно места, необходимо скопировать его байты в правильное место в buffer. Функция std::strncpy 5 из заголовка <сstring> является одним из подходящих инструментов для этой работы. Она принимает три параметра: адрес назначения, адрес источника и количество символов для копирования:

char* std::strncpy(char* destination, const char* source, std::size_t num);

Функция strncpy будет копировать до num байтов из source в destination. После завершения она вернет значение destination (которое будет отброшено).

После добавления количества байтов x_len, скопированных в buffer, к length работа завершается добавлением символа новой строки \n и нулевого байта в конец buffer. Функция возвращает true, чтобы указать, что введенный х был успешно добавлен в виде строки в конец буфера.
ПРЕДУПРЕЖДЕНИЕ
Используйте strncpy очень осторожно. Слишком легко забыть символ конца строки в исходной строке или не выделить достаточно места в целевой строке. Обе ошибки приведут к неопределенному поведению. Мы рассмотрим более безопасную альтернативу во второй части книги.

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

Листинг 4.16. Методы SimpleString

#include <cstdio>#include <cstring>#include <exception>struct SimpleString {   --пропуск--}int main() {   SimpleString string{ 115 }; 1   string.append_line("Starbuck, whaddya hear?");   string.append_line("Nothin' but the rain."); 2   string.print("A"); 3   string.append_line("Grab your gun and bring the cat in.");   string.append_line("Aye-aye sir, coming home."); 4   string.print("B"); 5   if (!string.append_line("Galactica!")) { 6      printf("String was not big enough to append another message."); 7   }}

Сначала создается SimpleString с max_length=115 1. Метод append_line используется дважды 2, чтобы добавить некоторые данные в строку, а затем вывести содержимое вместе с тегом A 3. Затем добавляется больше текста 4 и снова выводится содержимое, на этот раз с тегом B 5. Когда append_line определяет, что SimpleString исчерпал свободное пространство 6, возвращается false 7. (Вы как пользователь SimpleString несете ответственность за проверку этого условия.)

Листинг 4.17 содержит выходные данные запуска этой программы.

Листинг 4.17. Результат выполнения программы в листинге 4.16

A: Starbuck, whaddya hear? 1Nothin' but the rain.B: Starbuck, whaddya hear? 2Nothin' but the rain.Grab your gun and bring the cat in.Aye-aye sir, coming home.String was not big enough to append another message. 3


Как и ожидалось, строка содержит Starbuck, whaddya hear?\nNothin' but the rain.\nвA 1. (Вспомните из главы 2, что \n это специальный символ новой строки.) После добавления Grab your gun and bring the cat in. и Aye-aye sir, coming home. вы получите ожидаемый результат в B 2.

Когда листинг 4.17 пытается добавить Galactica! в string, append_line возвращает false, поскольку в buffer недостаточно места. Это вызывает вывод сообщения String was not big enough to append another message 3.

Составление SimpleString

Рассмотрим, что происходит при определении класса с членом SimpleString, как показано в листинге 4.18.

Как предполагает инициализатор члена 1, string полностью построена, и ее инварианты класса назначаются после выполнения конструктора SimpleStringOwner. Здесь демонстрируется порядок членов объекта во время создания: члены создаются перед вызовом конструктора окружающего объекта. Смысл есть, а иначе как можно установить инварианты класса без знаний об инвариантах его членов?

Листинг 4.18. Реализация SimpleStringOwner

#include <stdexcept>struct SimpleStringOwner {   SimpleStringOwner(const char* x)     : string{ 10 } { 1     if (!string.append_line(x)) {       throw std::runtime_error{ "Not enough memory!" };    }    string.print("Constructed");  }  ~SimpleStringOwner() {    string.print("About to destroy"); 2  }private:  SimpleString string;};

Деструкторы работают в обратном порядке. Внутри ~SimpleStringOwner() 2 нужно хранить инварианты класса строки, чтобы можно было напечатать ее содержимое. Все члены уничтожаются после вызова деструктора объекта.


В листинге 4.19 используется SimpleStringOwner.

Листинг 4.19. Программа, содержащая SimpleStringOwner

--пропуск--int main() {   SimpleStringOwner x{ "x" };   printf("x is alive\n");}--------------------------------------------------------------------Constructed: х 1x is aliveAbout to destroy: х 2

Как и ожидалось, член string в x 1 создается надлежащим образом, потому что конструкторы членов объекта вызываются перед конструктором объекта, в результате чего появляется сообщение Constructed: x. Как автоматическая переменная x уничтожается непосредственно перед выходом из main, и вы получаете сообщение About to destroy: x 2. Член string все еще доступен в этот момент, потому что деструкторы членов вызываются после деструктора вмещающего объекта.

Размотка стека вызовов

Листинг 4.20 демонстрирует, как обработка исключений и размотка стека работают вместе. Блок try-catch устанавливается в main, после чего выполняется серия вызовов функций. Один из этих вызовов вызывает исключение.

Листинг 4.20. Программа, где используется SimpleStringOwner и размотка стека вызовов

--пропуск--void fn_c() {   SimpleStringOwner c{ "cccccccccc" }; 1}void fn_b() {  SimpleStringOwner b{ "b" };  fn_c(); 2}int main() {  try { 3   SimpleStringOwner a{ "a" };   fn_b(); 4   SimpleStringOwner d{ "d" }; 5 } catch(const std::exception& e) { 6  printf("Exception: %s\n", e.what()); }}

В листинге 4.21 показаны результаты запуска программы из листинга 4.20.

Листинг 4.21. Результат запуска программы из листинга 4.20

Constructed: aConstructed: bAbout to destroy: bAbout to destroy: aException: Not enough memory!

Вы установили блок try-catch 3. Первый экземпляр SimpleStringOwner, a, создается без инцидентов, и в консоль выводится сообщение Constructed: а. Далее вызывается fn_b 4. Обратите внимание, что вы все еще находитесь в блоке try-catch, поэтому любое выброшенное исключение будет обработано. Внутри fn_b другой экземпляр SimpleStringOwner, b, успешно создается, и Constructed: b выводится на консоль. Затем происходит вызов еще одной функции, fn_c 2.

Давайте на минуту остановимся, чтобы разобраться, как выглядит стек вызовов, какие объекты живы и как выглядит ситуация обработки исключений. Сейчас у нас есть два живых и действительных объекта SimpleStringOwner: a и b. Стек вызовов выглядит как main() fn_ () fn_c(), и в main настроен обработчик исключений для обработки любых исключений. Эта ситуация показана на рис. 4.3.

В 1 возникает небольшая проблема. Напомним, что SimpleStringOwner имеет член SimpleString, который всегда инициализируется с max_size 10. При попытке создания c конструктор SimpleStringOwner выдает исключение, потому что вы пытались добавить cccccccccc, который имеет длину 10, что выходит за рамки, потому что нужно еще добавить символы новой строки и завершения строки.

Теперь в полете находится одно исключение. Стек будет раскручиваться до тех пор, пока не будет найден соответствующий обработчик, и все объекты, выпадающие из области видимости в результате этого раскручивания, будут уничтожены. Обработчик доходит до стека 6, поэтому fn_c и fn_b разматываются. Поскольку SimpleStringOwner b это автоматическая переменная в fn_b, она разрушается и в консоль выводится сообщение About to destroy: b. После fn_b автоматические переменные внутри try {} уничтожаются. Это включает в себя SimpleStringOwner a, поэтому в консоль выводится About to destroy: a.

image

Как только исключение происходит в блоке try{}, дальнейшие операторы не выполняются. В результате d никогда не инициализируется 5 и конструктор d не вызывается и не выводится в консоль. После размотки стека вызовов выполнение сразу переходит к блоку catch. В итоге в консоль выводится сообщение Exception: Not enough memory! 6.

Исключения и производительность

Обработка исключений обязательна в программах; ошибки неизбежны. При правильном использовании исключений ошибок не возникает, код работает быстрее, чем код, проверенный вручную. Если ошибка все-таки есть, обработка исключений может иногда выполняться медленнее, но у этого есть огромные преимущества в надежности и удобстве обслуживания по сравнению с альтернативными вариантами. Курт Гантерот, автор Оптимизации программ на C++, хорошо пишет об этом: Использование обработки исключений приводит к программам, которые работают быстрее при нормальном выполнении и ведут себя лучше в случае неудачи. Когда программа на C++ выполняется нормально (без исключений), при проверке исключений не возникает никаких издержек во время выполнения. Вы платите только за исключение.

Надеюсь, вы убедились в центральной роли, которую играют исключения в идиоматических программах на C++. К сожалению, иногда нет возможности использовать исключения. Одним из примеров является встроенная разработка, где требуются гарантии в реальном времени. Инструменты просто не существуют (пока) для этих настроек. Если повезет, это скоро изменится, но сейчас приходится обходиться без исключений в большинстве встроенных контекстов. Другой пример некоторый устаревший код. Исключения изящны из-за того, как они вписываются в объекты RAII. Когда деструкторы отвечают за очистку ресурсов, раскрутка стека является прямым и эффективным способом защиты от утечек памяти. В устаревшем коде можно найти ручное управление ресурсами и обработку ошибок вместо объектов RAII. Это делает использование исключений очень опасным, поскольку размотка стека безопасна только для объектов RAII. Без них можно с легкостью допустить утечку ресурсов.

Альтернативы для исключений

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

struct HumptyDumpty {   HumptyDumpty();   bool is_together_again();  --пропуск--};

В идиоматическом C++ вы бы просто сгенерировали исключение в конструкторе, но здесь следует помнить о проверке и обработке ситуации как условии ошибки в вызывающем коде:

bool send_kings_horses_and_men() {  HumptyDumpty hd{};  if (hd.is_together_again()) return false;  // Использование инвариантов класса hd гарантировано.  // HumptyDumpty с треском проваливается.  --пропуск--  return true;}

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

Листинг 4.22. Фрагмент кода с объявлением структурированной привязки

struct Result { 1   HumptyDumpty hd;   bool success;   };  Result make_humpty() { 2    HumptyDumpty hd{};    bool is_valid;    // Проверка правильности hd и установка соответствующего значения is_valid    return { hd, is_valid };   }bool send_kings_horses_and_men() {   auto [hd, success] = make_humpty();    if(!success) return false;   // Установка инвариантов класса   --пропуск--   return true;}

Сначала объявляется POD, который содержит HumptyDumpty и флаг success 1. Затем определяется функция make_humpty 2, которая создает и проверяет HumptyDumpty. Такие методы называются фабричными, поскольку их целью является инициализация объектов. Функция make_humpty оборачивает его и флаг success в Result при возврате. Синтаксис в точке вызова 3 показывает, как можно распаковать Result, получив несколько переменных с определением типа при помощи auto.
ПРИМЕЧАНИЕ
Более подробное описание структурированных привязок приведено в подразделе Структурированные привязки, с. 289.


Об авторе

Джош Лоспинозо (Josh Lospinoso) доктор философии и предприниматель, прослуживший 15 лет в армии США. Джош офицер, занимающийся вопросами кибербезопасности. Написал десятки программ для средств информационной безопасности и преподавал C++ начинающим разработчикам. Выступает на различных конференциях, является автором более 20 рецензируемых статей и стипендиатом Родса, а также имеет патент. В 2012 году стал соучредителем успешной охранной компании. Джош ведет блог и активно участвует в разработке ПО с открытым исходным кодом.

О научном редакторе

Кайл Уиллмон (Kyle Willmon) разработчик информационных систем с 12-летним опытом в C++. В течение 7 лет работал в сообществе по информационной безопасности, используя C++, Python и Go в различных проектах. В настоящее время является разработчиком в команде Sony Global Threat Emulation.

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

Для Хаброжителей скидка 25% по купону C++

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Перевод Как в Runescape ловят пользователей ботов, и почему они не поймали меня

19.04.2021 18:14:05 | Автор: admin

Автоматизация игроков всегда была большой проблемой в глобальных многопользовательских онлайновых ролевых играх (MMORPG), таких как World of Warcraft и Runescape, и этот вид взлома игр значительно отличается от традиционных читов, например в стрелялках. Однажды в выходные я решил взглянуть на системы обнаружения, созданные компанией Jagex для предотвращения автоматизации игроков в Runescape и вот что из этого вышло.


Использование ботов

Последние несколько месяцев игрок с учётной записью sch0u играл в World 67 круглосуточно, выполняя обычные задачи, такие как убийство мобов или сбор ресурсов. На первый взгляд игрок с этой учётной записью выглядит так же, как и любой другой игрок, но есть одно ключевое отличие это бот.

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

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

Эвристика!

Я начал с анализа клиента Runescape, чтобы подтвердить эту теорию, и быстро заметил глобально вызываемую переменную hhk, которая задаётся вскоре после запуска.

const auto module_handle = GetModuleHandleA(0);hhk = SetWindowsHookExA(WH_MOUSE_LL, rs::mouse_hook_handler, module_handle, 0);

В результате на мышь устанавливается низкоуровневая hook-точка, добавляя её к общесистемной цепочке hook-точек. Это позволяет приложениям в Windows перехватывать все события мыши, независимо от того, связаны они с вашим приложением или нет. Низкоуровневые hook-точки часто используются клавиатурными шпионами, однако есть и законные случаи использования, такие как эвристика, подобная вышеупомянутой hook-точке мыши.

Обработчик мыши Runescape довольно прост по своей сути (следующий псевдокод красиво переписан вручную):

LRESULT __fastcall rs::mouse_hook_handler(int code, WPARAM wParam, LPARAM lParam){  if ( rs::client::singleton )  {      // Call the internal logging handler      rs::mouse_hook_handler_internal(rs::client::singleton->window_ctx, wParam, lParam);  }  // Pass the information to the next hook on the system  return CallNextHookEx(hhk, code, wParam, lParam);}void __fastcall rs::mouse_hook_handler_internal(rs::window_ctx *window_ctx, __int64 wparam, _DWORD *lparam){  // If the mouse event happens outside of the Runescape window, don't log it.  if (!window_ctx->event_inside_of_window(lparam))  {    return;  }  switch (wparam)  {    case WM_MOUSEMOVE:      rs::heuristics::log_movement(lparam);      break;case WM_LBUTTONDOWN:case WM_LBUTTONDBLCLK:case WM_RBUTTONDOWN:case WM_RBUTTONDBLCLK:case WM_MBUTTONDOWN:case WM_MBUTTONDBLCLK:  rs::heuristics::log_button(lparam);  break;  }}

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

Эти данные события позже анализируются функцией rs::heuristics::process, которая вызывается каждым фреймом в основном цикле рендеринга.

void __fastcall rs::heuristics::process(rs::heuristic_engine *heuristic_engine){  // Don't process any data if the player is not in a world  auto client = heuristic_engine->client;  if (client->state != STATE_IN_GAME)  {    return;  }  // Make sure the connection object is properly initialised  auto connection = client->network->connection;  if (!connection || connection->server->mode != SERVER_INITIALISED)  {    return;  }  // The following functions parse and pack the event data, and is later sent  // by a different component related to networking that has a queue system for  // packets.  // Process data gathered by internal handlers  rs::heuristics::process_source(&heuristic_engine->event_client_source);  // Process data gathered by the low level mouse hook  rs::heuristics::process_source(&heuristic_engine->event_hook_source);}

Вдали от клавиатуры?

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

Запретив игре вызывать функцию rs::heuristics::process, я сразу ничего не заметил, но ровно через пять минут вышел из игры. По-видимому, Runescape принимает решение о неактивности игрока просто по эвристическим данным, отправленным клиентом на сервер, хотя вы можете просто отлично играть в эту игру. Это породило новый вопрос: если сервер не считает, что я играю, то считает ли он, что я использую бота?

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

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

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

Другие профессии и курсы

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

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

Категории

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

© 2006-2021, personeltest.ru