Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум на Исходниках.RU > Assembler FAQ > Некоторые возможности макроязыка TASM


Автор: Eretic 23.11.19, 14:15
Некоторые возможности макроязыка TASM.


Введение.

Данная статья не является учебником по макроязыку Turbo Assembler, скорее это забавный пример его практического использования, предполагающий знания основ макроязыка.
Когда-то мне понадобилось защитить часть кода загрузчика CD-ROM. Вариант шифрования кода и данных отбросил сразу, поскольку аналогичная защита на диске «Reanimator 2K» была снята мной менее чем за день. И тогда вспомнил про старый добрый Форт, с его прямым шитым кодом, который и без шифрования создает трудный для понимания код. Вот его мы и создадим, используя макроязык Turbo Assembler.
Нам понадобится TASM последней версии, то есть 5.3. Есть еще версия 5.4, но она отличается от предыдущей только копирайтом.

Имена.

Основная проблема, которую нужно решить, заключается в том, что в Форте для имён допустимы любые символы, включая те, что TASM запрещает использовать в именах меток и идентификаторов. Вдобавок, имена в Форте могут начинаться с цифры. Поэтому, строку вида:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    >R R@ R> ;
ассемблер не воспримет. Единственным выходом является создание макроса, которому будет передаваться строка с командами Форта, а он разберет её на составляющие и скомпилирует в код, примерно так:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    _  >R R@ R> EXIT

Первым делом необходимо изъять из строки некоторые спецсимволы, которые, которые макроязык ассемблера интерпретирует особым образом. Это символы ‘>’, ‘<’ и ‘!’. Для начала определим макрос подмены одного символа в строке:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $SWAP new, old: rest
        ??ch equ &old&
        ??pos instr ??NAME, ??ch
        :while
        if ??pos
          ??prev substr ??NAME, 1, ??pos-1
          ??next substr ??NAME, ??pos+1
          ??NAME catstr ??prev, <new>, ??next
          ??pos  instr  ??NAME, ??ch
          goto while
        endif
    endm
Поиск производится в строке ??NAME, штатной функцией INSTR, а строка, в случае успешного поиска символа old, разбивается функцией SUBSTR на две части – до символа (??prev) и после (??next). Затем строки склеиваются функцией CATSTR, а между ними, вместо символа old, вставляется строка new.
Теперь можно написать макрос предварительной обработки строки:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $PRENAME name: rest
        ??NAME   equ &name&
        $SWAP    less, <<
        $SWAP    more, >
        $SWAP    save, !
    endm
Макрос подменяет символы ‘>’, ‘<’ и ‘!’ на строки ‘more’, ‘less’ и ‘save’ соответственно. Это поможет корректно разбить строку на отдельные слова, но не избавит от проблем с именами подпрограмм, поскольку остаются другие недопустимые символы. Поэтому нам понадобится еще один макрос, завершающий подмену остальных символов:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $POSTNAME name
        ??NAME   equ &name&
        $SWAP    plus, +
        $SWAP    minus, -
        $SWAP    mul, *
        $SWAP    eq, =
        $SWAP    div, /
        $SWAP    dot, .
        $SWAP    tld, ~
    endm
Он будет использоваться на завершающем этапе формирования имен, когда уже будут отсеяны числа и символы. Почему не сразу? Проще это понять на примере. Допустим, имеется строка:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    54 -2 * ‘*’ !
Если бы мы подменяли все символы сразу, то получили бы строку:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    54 minus2 mul ‘mul’ save
То есть, вместо числа (-2) идентификатор (minus2), а вместо символа (‘*’) целую строку (‘mul’), что очевидно неправильно. Если же использовать сначала $PRENAME, то получим для разбора строку:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    54 -2 * ‘*’ save
В этой строке мы легко выберем числа и символы, а оставшиеся слова пропустим через $POSTNAME и получим допустимые идентификаторы:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    mul save

Поскольку числа-константы, символы и подпрограммы компилируются разными способами, нужен макрос для их распознавания. В макроязыке нет предусмотренных средств для определения типа идентификатора, а что есть – не работает. Придется сделать макрос идентификации чисел и символов самостоятельно:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $ISNUM n
        ??ISNUM = 0
        ??NUM equ &n&
        @len sizestr ??NUM
        ;; проверяем первый символ
        @cfirst SubStr ??NUM, 1, 1
        @IsFirst InStr <''>, @cfirst
        if (@IsFirst ne 0)
            ;; это символ, обрабатывается как число
            ??ISNUM = 1
            exitm
        endif
        ;; проверяем первый символ на наличие знака числа
        @IsFirst InStr <-+>, @cfirst
        if (@IsFirst ne 0)
            ;; первым идет знак числа, убираем его
            ??NUM SubStr ??NUM, 2, @len-1
            @cfirst SubStr ??NUM, 1, 1
            @len sizestr ??NUM
        endif
        if (@len eq 0)
            ;; это не число
            exitm
        endif
        ;; проверяем основание числа по последнему символу
        @numstr CatStr <0123456789>
        @clast SubStr ??NUM, @len, 1
        @IsLast InStr <bBhH>, @clast
        if (@IsLast ne 0)
            ;; это основание числа, убираем его
            if (@IsLast gt 2)
                ;; это шестнадцатеричное число
                @numstr CatStr @numstr,<ABCDEFabcdef>
            else
                ;; это двоичное число, допустимы только 0 и 1
                @numstr CatStr <01>
            endif
            ??NUM SubStr ??NUM, 1, @len-1
            @len sizestr ??NUM
        endif
        ;; первым символом, после знака, должна быть цифра
        @IsFirst InStr <0123456789>, @cfirst
        if (@len eq 0) or (@IsFirst eq 0)
            exitm
        endif
        ;; сканируем число, без учета символов знака и основания
        rept @len
            @nchr SubStr ??NUM, 1, 1
            ??NUM SubStr ??NUM, 2, @len
            @isDigit InStr @numstr, @nchr
            if (@isDigit eq 0)
                exitm
            endif
        endm
        if (@isDigit ne 0)
            ??ISNUM = 1
        endif
    endm
Вначале пропускаем возможные символы знака числа. Затем, убираем символ основания числа в конце строки и задаем набор допустимых цифр для данного числа в идентификатор @numstr. И в заключение, проверяем оставшуюся строку на предмет соответствия цифрам из строки @numstr. Результат возвращаем в глобальном идентификаторе ??ISNUM. Все просто.

Компиляция кода

Теперь можно приступать к макросу компиляции Форт-строки. Цель макроса – разбить строку на отдельные слова, идентифицировать их и скомпилировать в шитый код. Сам макрос выглядит проще некуда:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro _ listcmd: rest
        $PRENAME listcmd
    %    $parsewords ??NAME
    endm
То есть, подменяем в строке недопустимые символы и результат передаем в макрос $parsewords, который и произведет разбивку строки на слова. Обращаем внимание, что параметр макроса определен как REST, чтобы строка передавалась в него в неизменном виде, без предварительной обработки транслятором ассемблера. Однако есть нюанс. Если первым символом идет ‘<’, то его нужно продублировать ‘<<’, иначе ассемблер воспримет его, как начало текстовой строки и естественно отбросит. В других местах стоки прокатит и одиночный символ ‘<’.
Макрос разбивки строки на слова вполне стандартен:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $parsewords name: vararg
        irp @i, <name>
            $COMPILE @i
        endm
    endm
Параметр задан через VARARG, поэтому строку на слова разобьет уже сам транслятор ассемблера и макрос получит готовый массив слов, который он поочередно, с помощью IRP, скормит макросу $COMPILE:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
     macro $COMPILE name
        $ISNUM <&name>
        if ??ISNUM
            dw      _LIT
            dw      &name
        else
          $POSTNAME name
    %     $COMPILENAME ??NAME
        endif
    endm
А вот это и есть компилятор слов Форта. Если слово распознано как число или символ, то оно компилируется в связке с подпрограммой LIT, которая, во время выполнения, извлекает его из кода и помещает на арифметический стек. В противном случае считаем слово идентификатором и, завершив подмену спецсимволов макросом $POSTNAME, компилируем его в код:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $COMPILENAME name
        ifdef _&name&_F
          _&name                  ;; немедленно выполняем слово
        else
          dw      _&name          ;; просто компилируем слово в код
        endif
    endm
Тут две тонкости. Во первых, к имени добавляется префикс ‘_’, для решения проблем с именами, начинающимися с цифры. Во вторых, есть два типа подпрограмм – немедленного исполнения (immediate) и простые. С простыми все ясно, их адрес просто добавляется в код, т.е. компилируется. А вот с immediate сложнее. Они не компилируются, а сразу же выполняются, то есть это некий аналог макросов. Такие подпрограммы используются, в основном, в командах ветвления, или циклах, где необходимо сразу же расставить метки для переходов. Поэтому, реализуем их в виде макросов, а для отличия от простых слов, параллельно им будем определять идентификатор, с тем же именем, но с постпрефиксом ‘_F’. Подробнее о подпрограммах немедленного исполнения будет ниже.


Подпрограммы

Настало время определиться со стеками и регистрами. Стеков будет два, арифметический и возвратов. На верхушку первого будет указывать регистр SP, а второй реализуем через регистр BP. Регистр SI задействуем в качестве некоего аналога IP, он будет указывать на адрес следующей исполняемой подпрограммы. Регистры CS, DS и SS должны быть настроены на один сегмент. Остальные регистры свободны.
Подпрограммы бывают двух видов – высокого и низкого уровня. Начнем с простейших. Низкоуровневые подпрограммы – это почти обычные ассемблерные вставки. Для их объявления достаточно преобразовать имя в допустимый вид и объявить метку:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $CODE cname: rest
        $PRENAME cname
    %   $POSTNAME ??NAME
        $LABEL <%??NAME>
    endm
     
    macro $LABEL name
        _&name&:
    endm
Для завершения ассемблерных вставок нельзя использовать команду RET, вместо этого нужно передать управление на следующую подпрограмму. Адрес следующей подпрограммы хранится в слове, на которое указывает регистр SI. Для удобства оформим макрос:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $NEXT
             lodsw
             jmp     ax
    endm
Таким образом, низкоуровневая подпрограмма будет выглядеть следующим образом:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    $CODE >R      ; ( x -- )(R: -- x )    переносит x с ариф. стека на стек возвратов
            pop     ax
            xchg    bp, sp
            push    ax
            xchg    bp, sp
            $NEXT

Подпрограммы высокого уровня реализуются прямым шитым кодом. Это значит, что для их выполнения нужна специальная функция. Назовем её DOLST. В качестве аргумента ей передается адрес подпрограммы высокого уровня, которую она и запустит на выполнение:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    $CODE DOLST   ; ( addr -- )
            xchg    bp, sp
            push    si
            xchg    bp, sp
            pop     si
            $NEXT
Функция сохраняет в стек возвратов текущее значение регистра SI и загружает в регистр новое, с арифметического стека. Этим значением будет указатель на массив адресов подпрограмм, что составляют тело исполняемой подпрограммы. С помощью макроса $NEXT запускаем выполнение первой подпрограммы из массива, а она передаст управление следующей и т.д., пока не будет достигнут конец массива, то есть особая функция завершения подпрограммы. В Форте имя этой подпрограммы – точка с запятой. Ассемблер не даст нам использовать такое имя, поэтому выберем другое:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    $CODE EXIT
            mov     si, [bp]
            add     bp, 2
            $NEXT
Подпрограмма восстанавливает значение регистра SI из стека возвратов и таким образом возвращает управление туда, откуда и была вызвана подпрограмма, точнее на следующий адрес.
Теперь можно написать макрос для объявления подпрограмм высокого уровня:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $COLN cname: rest
        $CODE cname
            call    _DOLST
    endm
Для примера:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    $COLN NEG     ; ( n -- -n )           n = -n
            _       NOT 1+ EXIT
сгенерирует код:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    03C5                _NEG:
    03C5  E8 FD93         call    _DOLST
    03C8  0539r           dw      _NOT
    03CA  03F2r           dw      _1plus
    03CC  011Dr           dw      _EXIT
Инструкция CALL кладет на арифметический стек адрес первого элемента массива подпрограмм, что составляют тело текущей подпрограммы (dw _NOT, _1plus и т.д.), а DOLST запускает выполнение.

Подпрограммы немедленного исполнения

Как уже упоминалось ранее, для реализации ветвления кода и циклов необходимы подпрограммы немедленного исполнения, так называемые immediate. Все дело в том, что еще на этапе генерации кода необходимо расставить команды переходов на код, который еще даже не объявлен. Возьмем для примера простейшую конструкцию: IF – ELSE - THEN (так в Форте называется ENDIF).
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    IF
      if (param == FALSE) then GOTO (ELSE or THEN)
     
          <code>
     
          jmp THEN
    [ELSE]
     
          <code>
     
    THEN
При выполнении макроса IF придется сгенерировать код проверки условия и возможный переход либо на конец условия (THEN), либо на возможную ветку ELSE, на данном этапе просто невозможно определить, куда именно будет переход. Поэтому, мы сгенерируем переход на некую метку и занесем ее имя в список. Последующий макрос, THEN или ELSE, извлечет это имя из списка, и сгенерируют метку в коде.
Список меток организуем по принципу стека, последним пришел – первым ушел. Всего нам понадобится три макроса, $PUSH – для генерации новой метки и помещению ее в список, $POP – соответственно для извлечения метки из списка, и $TOP – для получения метки со списка, без ее извлечения из этого списка. Примерно так:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro $PUSH variable
      local @@lab
            ??STACK CatStr <@@lab>, <,>, ??STACK
            ifnb <variable>
              variable CatStr <@@lab>
            endif
    endm
     
    macro $POP variable: req
            variable equ <>
            @pos InStr ??STACK, <,>
            if @pos
                    @pos     = @pos-1
                    variable SubStr ??STACK, 1, @pos
                    @pos     = @pos+2
                    ??STACK  SubStr ??STACK, @pos
            endif
    endm
     
    macro $TOP variable: req
            variable equ <>
            @pos InStr ??STACK, <,>
            if @pos
                    @pos     = @pos-1
                    variable SubStr ??STACK, 1, @pos
            endif
    endm
В параметре макросам передается имя идентификатора, в который будет сохранено имя метки. Имена меток нам генерирует сам ассемблер, посредством своего макроса LOCAL. Это гарантирует уникальность имени. В остальном, думаю все понятно, новое значение добавляется в начало строки, чтобы при извлечении снова быть первым.
Теперь непосредственно сами макрокоманды условия. Первым делом объявляем их IMMEDIATE:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    IMMEDIATE       = 1             ; флаг слов немедленного исполнения
     
      _IF_F         = IMMEDIATE
      _THEN_F       = IMMEDIATE
      _ELSE_F       = IMMEDIATE
Сам макрос условия:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro _IF                       ; ( bool -- )
            $PUSH   <??LABEL>
            dw      _?BRANCH
    %       dw      ??LABEL
    endm
Генерирует метку и условный переход на нее, если на верхушке арифметического стека находится значение FALSE (подпрограмма ?BRANCH).
Макрос завершения тела условия:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro _THEN
            $POP    <??LABEL>
    %  ??LABEL:
    endm
Просто извлекает из списка метку, что сгенерировал макрос IF или ELSE, и ставит в конец условия.
Макрос альтернативного выбора:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro _ELSE
            $POP    <??LABEL>
            $PUSH   <??NEWLAB>
            dw      _BRANCH
    %       dw      ??NEWLAB
    %  ??LABEL:
    endm
Вначале генерирует код безусловного перехода на конец условия (BRANCH), то есть на метку, что сгенерирует THEN. Этим самым завершается выполнение ветки IF. После этого объявляем метку, что сгенерировал макрос IF. Теперь именно сюда и перейдет управление, в случае если не сработает условие оператора IF.
Похожим образом строятся конструкции CASE – OF – ENDCASE, а также конструкции циклов BEGIN – UNTIL, BEGIN – WHILE – REPEAT, DO – LOOP. Приводить их реализацию не буду, все есть во вложении.


Данные

В Форте нет переменных и констант, в том виде, что есть в других языках программирования. Любая переменная или константа - это подпрограмма. При вызове переменной, она оставляет на стеке адрес связанной с ней ячейки памяти, куда можно что-то записать, или считать из нее. В отличие от переменной, константа оставляет на стеке не адрес ячейки, а её значение. При желании, значение константы можно менять по ходу выполнения программы, хотя вряд ли в этом есть смысл. В большинстве реализаций Форта для создания переменных и констант предусмотрены слова VARIABLE и CONSTANT. Не будем и мы отбиваться от большинства. Начнем с констант:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro CONSTANT name, imm:req
        $CODE <&name>
            push    &imm
            $NEXT
    endm
Макрос создает подпрограмму, которая кладет на арифметический стек значение константы.
При объявлении переменной нужно учесть возможность зарезервировать место под массив, а так же возможность объявления инициированных данных:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro VARIABLE name: req, len, imm
        $CODE <&name>
        @name CatStr <$$_>,??NAME
    %        push offset @name
             $NEXT
      ifnb <imm>
        align 2
        ifnb <len>
    %        @name dw len dup(imm)
        else
    %        @name dw imm
        endif
      else
        udataseg
        align 2
        ifnb <len>
    %        @name dw len dup(?)
        else
    %        @name dw ?
        endif
        codeseg
      endif
    endm
Макрос создает подпрограмму, которая кладет на арифметический стек адрес зарезервированной памяти, из LEN ячеек. Если LEN не задана, то резервируется одна ячейка. Если задано значение параметра IMM, то создаются инициализированные данные.
Что касается строк, то в Форте предусмотрены средства для работы с паскаль-строками, но я не стал реализовывать определяющие строки слова. Вместо этого создал отдельный макрос для объявления строк:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    macro STRING name, imm:rest
      local @endstr
        $CODE <&name>
        @name CatStr <$$_>,??NAME
    %        push offset @name
            $NEXT
            align 2
    %    label @name byte
            db @endstr - @name - 1          ;; байт длины строки
            db &imm&
         label @endstr byte
    endm
Это удобнее чем в Форте, так можно задавать строки с любым набором символов. Например:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    STRING sAnyKey, "Press any key",13,10,"to continue",'$'
В стандартном Форте пришлось бы выводить это сообщение в три приема.

Практика

Макросы готовы, ядро написано и заняло всего 2 Кб. Осталось проверить. Я не сторонник искусственных тестов, поэтому поискал нечто более комплексное. Не помню, откуда, но завалялись у меня старые исходники тетриса на Форте. Добавил цвет и получил годный тетрис под DosBox. Полностью приводить код не буду, он есть в аттаче, разве что небольшой кусочек для наглядности:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    $COLN PLAY-GAME ; ( -- )
          _         BEGIN
          _             NEW-BRICK
          _             -1 3 INSERT-BRICK
          _             NEXT-DRAW
          _         WHILE
          _             BEGIN
          _                 4 0 DO
          _                     HELP.X HELP.Y GOTO-XY
          _                     KEY?
          _                     IF
          _                         BEGIN
          _                             KEY KEY?
          _                         WHILE
          _                             DROP
          _                         REPEAT
          _                         ON-KEY 0=         ; обрабатываем нажатия клавиш
          _                         IF
          _                             UNLOOP EXIT   ; прерываем игру
          _                         THEN
          _                     THEN
          _                     GAME.SPEED @ DELAY
          _                 LOOP
                            ; перемещаем фигуру вниз
          _                 1 0 MOVE-BRICK  0=
          _             UNTIL
                        ; фигура достигла "дна", проверяем и удаляем зап. линии
          _             PIT-CHECKFULL
          _             SHOW-SCORES
          _             CALCK-SPEED
          _         REPEAT
                    ; запрашиваем новую игру, или выходим
          _         sNew GET-INPUT NOT GAME.CONTINUE !
          _         EXIT
Если бы не символ подчеркивания, ничем бы не отличалось от кода на «натуральном» Форте. Что собственно и требовалось.

Заключение

Понятно, что я далеко не первый и не последний, кто занимался «исследованиями» на тему возможностей макроязыка. Уже на заре увлечения ассемблером имел удовольствие ознакомиться с макробиблиотекой “Macro Language Version 2.0”. Просто из примера:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
     Include "system.h"
     
    Program ArrayDemo;
     
    Var
      array ARR1[10*20] of shortint;
      array ARR2 of integer = <10,9,8,7,6,5,4,3,2,1>;
      words i, j;
     
    Begin
      lea si,[ARR2];
      for i = 1 to 10 do
          asm lodsw  \  j = ax;
          WriteLn<'ARR2['>,i,<'] = '>,j;
      next i;
    End_
Если кто-нибудь вам скажет, что это не ассемблер, плюньте ему в глаза © «Эл Стоу».
Пожалуй, всё. К статье, помимо «Форта», прикладываю вышеупомянутую макробиблиотеку «Macro Language Version 2.0». Заодно выложу инклюды к Turbo Assembler. Начинал их еще AndNot, а я доработал и опробовал в нескольких небольших проектах, пока окончательно не перешел на Си. В них тоже есть немножко макросов, облегчающих жизнь при работе со строками – text, invoke и move. В качестве примера их использования приложу вывод фрактала папоротника, написанный по мотивам книги Никулина «Компьютерная геометрия и алгоритмы машинной графики». Пример в папке EXAMPLE, для компиляции нужно в батнике настроить пути к TASM’у. В папке TOOLS конвертеры инклюдов, детали в файле ‘READ.ME’.

FORTH.ZIP (, : 342)
INCLUDE.rar (, : 340)
LIB.rar (, : 338)
TASM_Macro_Language.part1.rar (, : 347)
TASM_Macro_Language.part2.rar (, : 344)
TASM_Macro_Language.part3.rar (, : 339)

Автор: Kopa 02.10.21, 04:38
Дополню тему такой статьёй в применении ассемблера. (Fasm)
Разместить FORTH в 512 байтах

P.S. Проект MiniForth на Github

Powered by Invision Power Board (https://www.invisionboard.com)
© Invision Power Services (https://www.invisionpower.com)