Не тот велик, кто никогда не падал, а тот велик, кто падал и вставал!
Конфуций

Меню навигации для мобильных

Как работают побитовые операции на уровне железа?

Автор Nikopol, 06 Нояб., 2022, 17:06

« предыдущая - следующая »

Nikopol

Если никто не против, попробуем набрать что-то типа шпаргалки. По принципу вопрос ответ.
Вопрос первый:
Как работает этот код на уровне железа?
if ((91 & (1<<4)) != 0)

Как я это понимаю:
1. При компиляции кода число вноситься в регистр который закрепляется за ним.
2. Выделяется область памяти для выполнения операции включающая регистр для результата и регистр для числа маски.
3. Выполняется операция, результат попадает в регистр результата.
4. Какая-то команда смотрит значение регистра и на его основе выводит результат true или falce.

Но хотелось бы понимать это поглубже.
1. Как выбираются адреса регистров для хранения чисел и где эти адреса хранятся?
2. Как происходит выделение памяти?
3. Как непосредственно, по шагам, выполняется операция?
4. Какая операция (команда процессора) определяет значение true или falce? Как конкретные числа в регистрах переводятся в абстрактные понятия для програмиста (для программы они всё равно остаются числами)?

З.Ы.
Мне кажется если будут отвечать посыплется куча кода из ассемблера )) Если не сложно можно с подробными пояснениями, как для чайника.  ;D

Slabovik

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

Что здесь такое?
if ((91 & (1<<4)) != 0)
{ какой-то код ещё }

Надо бы разобрать.

(1<<4) - это константа, определяемая выражением: число '1' сдвинуть побитово четырежды влево (что эквивалентно умножению на два в степени 4, т.е. 16).
00000001bin << 4 = 00010000bin = 10hex = 16

Далее константа 91, представленная по умолчанию в десятичном виде. 91dec = 5Bhex = 10011011bin
Далее операция "побитовое И" сделает
10011011bin
&
00010000bin
=
00010000bin

И да, это всё пока вычисление константы (потому что константа +/-!* константа = константа и никак иначе)
Остаётся последнее действие. Всё, что вычислено в скобках - это число 16dec = 10hex = 00010000bin
Препроцессор выполняет сравнение 16 с нулём, и если 16 не ноль - производит переход на подчинённый код, который в скобках.
А так как константа 16 по определению не является константой 0, препроцессор, вычислив всё это, не породит в этом месте никакого кода, т.к. он и не требуется. Сразу пойдёт {какой-то код ещё} без всяких условий.

Все глобальные переменные хранятся в ячейках памяти ОЗУ. Компилятор самостоятельно "рассаживает" переменные в память, составляя "у себя" таблицу их адресов (суть-констант). Когда в программе происходит какая-то операция с переменной, её значение, используя адреса из "таблицы рассадки" копируется из ОЗУ в рабочие регистры процессора. После обработки, если это необходимо, происходит обратная операция сохранения нового значения в ОЗУ.

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

Выделение памяти происходит по мере составления "таблицы адресов" при компиляции программы (при этом мы не рассматриваем сейчас динамическое распределение памяти, которое может происходить либо по инициативе компилятора для размещения локальных переменных ограниченной видимости, либо по инициативе программиста).

True, False и др. определяется не конкретно какой-то командой процессора, а состоянием флагов (битов Status Register), индицирующим тот или иной результат выполненных действий в регистрах процессора и последующим исполнением какого-либо из условных переходов (команда перехода, исполняемая только при определённом состоянии какого-либо флага). Например, сравнение регистра с константой в цикле "for (i=1, i<18, i++){что-то там}"

LDI R16,1 ; загрузка начального значения для переменной i
JMP Метка_2 ; прыг на запись начального значения в память по адресу, определённому компилятором для переменной i
Метка_1:
{здесь может быть много разных команд тела цикла}
LDS  R16,My_i_address ; загрузка в R16 переменной, расположенной в ОЗУ по адресу My_i_address, при этом My_i_address - это константа, определённая компилятором для переменной пользователя i
INC R16 ; инкрементирование значения
Метка_2:
STS My_i_address,R16 ; сохранение (увеличенной на 1 в цикле) переменной i
SUBI R16,12h ; сравнение i с константой 18dec (методом вычитания) и если i станет больше, выйти из цикла
BRCS Метка_1 ; если i достигло 18 (12h), то флаг уже C не будет установлен и условного перехода по условию "C is Set" не произойдёт
{здесь цикл закончился}

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

Slabovik

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

Nikopol

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

Nikopol

С кодом вроде разобрался. Да пример привел не совсем верный, но суть вы уловили спасибо, что ответили.
Задаю возникающие вопросы.:

1. Я правильно понимаю, что в Си взаимодействие с регистром статуса (SREG) происходит в основном на уровне компилятора и программист редко обращается к нему напрямую?
2. Результат операции if ({переменная} != {константа}) заноситься в этот бит (если происходит SUBI (XOR)):
↓ спойлер ↓
• Bit 1 – Z: Zero Flag
The Zero Flag Z indicates a zero result in an arithmetic or logic operation. See the "Instruction
Set Description" for detailed information.
[свернуть]
[свернуть]
И следующей частью кода читается уже от туда?
3. Если да, то считать взведённое состояние этого бита true или falce зависит уже от контекста?
В примере выше взведённое состояние будет falce, а вот так  if ({переменная} == {константа}) будетtrue.
4. Что такое регистры указатели и как они работают?
5. Можно подробнее описать как происходит SUBI и что означает вот эта надпись из даташита напротив этой команды Rd ← Rd - K ?


Slabovik

1. Используя язык высокого уровня (а Си таковым считается, хотя и с некоторыми оговорками), программист вообще не знает, что такое регистры и как они взаимодействуют друг с другом. Это всё потому, что он работает на другом уровне абстракции т.е. выше. Потому и язык "высокого уровня" :) И, хотя в Си можно встретить механизмы, специально вводимые в него для конкретных платформ, дающие возможность программисту чутка поработать на более низком уровне, эти механизмы очень ограничены в возможностях. Поэтому по-умолчанию всё-таки можно считать, что непосредственно с АЛУ (регистрами) Си-программист всё-таки не работает.

2. Тут смотря с какой точки зрения смотреть. С точки зрения программиста, программа использует "внутренние механизмы", некий чёрный ящик. С точки зрения механики (более низкого уровня), да - АЛУ процессора работает так, что флаги в регистре SREG меняются после каждой (почти) исполненной операции с регистрами и именно по ним определяется логический результат той или иной операции. Однако True он или False - решается всё-таки программистом: на ассемблере путём прямого анализа флагов исходя из логики команд, на Си - посредством "чёрного ящика" (ну это просто потому, что для разных типов условий результирующий код тоже не будет одинаковым, а абстрактные True или False для Си-программиста одни и те же).
Чисто технически - да, результат операции (хотя точнее, не сам результат, а свойства этого результата, потому что результат - это такой же байт, или даже много, а его свойства, нулевой он, чётный, отрицательный, был ли перенос - это немного другое) находится в битах (флагах) регистра SREG. По состоянию этих битов можно производить условные переходы, есть целый набор разных команд-кодов для этого.
Т.е. чисто технически "if" реализуется какой-либо командой условного перехода, расположенной сразу за операцией сравнения "!=" (чтобы флаги ы SREG были актуальными). Команда условного перехода, если в SREG флаг установлен в положение "да, переходим", отработает эквивалентно команде JMP, а если флаг установлен в положение "нет", то никаких действий выполнено не будет и программа продолжится, как будто бы ничего не было.
Надо понимать, что "да" и "нет" тоже условность, т.к. например для флага Z есть две команды: BREQ (BRanch if EQual) и BRNE (BRanch if Not Equal), первая выполняет переход, если Z=1, вторая выполняет переход если Z=0
(у Intel мнемоника всё-таки лучше, JZ и JNZ, т.е. Jump if Z, и Jump if Not Z, но они её запатентовали, поэтому другие вынуждены изобретать, чтобы не было похоже).

4. Регистры указатели так называются, потому что их содержимое АЛУ может использовать в качестве адреса операнда. Как правило, это адрес в ОЗУ (но не обязательно). Самые главные регистры-указатели - это регистр счётчика команд PC. Он содержит адрес исполняемой команды. И регистр SP - указатель стека. (зы: команды перехода, условные или безусловный JMP, по сути являются командами загрузки в PC нового значения-адреса).
Кроме них у AVR три регистровых пары, которые можно использовать как указатели X, Y и Z. X - это пара r26:r27, Y - r28:r29, Z - r30:r31. Указывают они на ОЗУ. Удобны при обработке массивов.
Например, команда LD r16,X+ загрузит в r16 байт из ячейки ОЗУ с адресом, который находится в r26:r27, после чего X (т.е. 16-битовое число в регистрах r26:r27) будет инкрементирован на 1.
Жаль, у AVR действия по указателям ограничиваются загрузкой-выгрузкой. У Intel, например, регистр M (ячейка памяти по адресу из регистовой пары HL) по набору доступных операций эквивалентен другим регистрам из АЛУ.

5. SUBI - вычитание из регистра непосредственного значения (константы).
Например, SUBI R18,7 означает, что из значения в регистре R18 будет вычтено число 7, ну а результат логично будет в R18 же. Именно это вот эта запись "Rd ← Rd - K" и означает. Её можно переписать как "Rd←(Rd-K)" или вообще (Rd-K)→Rd, но изначальная запись соответствует канонам ассемблера:
Команда операнд1,операнд2
с командой понятно, а вот операнд1 - это операнд, с которым производится работа, в нём же результат работы, а операнд2 даёт значение, которое будет применено к операнду1, при этом операнд 2 остаётся неизменным. Например
ADD A,C означает, что значение из регистра A будет сложено со значением из регистра C, а результат будет помещён в регистре A.

Кстати, наверняка заметил, что у AVR регистры не совсем равнозначны. Регистрам R0..R15 недоступны некоторые операции (точнее, операции с непосредственными значениями). Та же команда SUBI работает только с R16..R31, а вот просто SUB работает со всеми регистрами.
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.