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

Дата-центр "миран"

Перевод FritzFrog новое поколение ботнетов

24.08.2020 18:21:28 | Автор: admin

Краткое содержание


  • Guardicore обнаружили сложный ботнет пиринговой (P2P) сети FritzFrog, который еще с января 2020 года активно взламывал SSH серверы.
  • Вредоносное ПО на Golang: FritzFrog исполняет модульный, мультипоточный и безфайловый вредоносный код на Golang, который не оставляет следов на жестком диске зараженного устройства.
  • Активное таргетирование государственных, образовательных, финансовых и прочих ресурсов: FritzFrog пытался брутфорсить и распространяться на десятках миллионов IP адресов правительственных офисов, образовательных учреждений, медицинских центров, банков и множества телекоммуникационных компаний. Среди них успешно подвержены атаке оказались более чем 500 серверов, включая известные университеты США и Европы, и одну железнодорожную компанию.
  • Сложность: FritzFrog полностью проприетарен, его имплементация P2P написана с нуля, что говорит о высоком уровне профессионализма его создателей в области разработки ПО.
  • Перехват: Guardcore Labs разработали клиентскую программу на Golang, способную перехватывать P2P соединения FritzFrog и подключаться к сети как пир.
  • Принадлежность: мы не смогли определить конкретную группу, ответственную за создание FritzFrog, однако текущий ботнет частично похож на ранее известный ботнет Rakos.

Введение


FritzFrog это очень изощренный пиринговый ботнет, который активно взламывает SSH серверы по всему миру. Благодаря своей децентрализованной структуре он распределяет контроль по всем своим узлам. В этой сети нет единой точки отказа, и пиры постоянно общаются друг с другом, чтобы поддерживать ее в устойчивом, обновляемом и постоянно активном состоянии. P2P соединение проводится через зашифрованный канал с использованием AES для симметричного шифрования и протокола Диффи-Хеллмана для обмена ключами.

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

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

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

Guardicore Labs предоставили доступ к Github репозиторию со скриптом для обнаружения этого вредоносного ПО и списком индикаторов компрометации (IoC) его деятельности.


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

Исследование FritzFrog


Впервые Guardcore Labs обратили внимание на деятельность FritzFrog в ходе исследования Botnet Encyclopedia. 9 января были обнаружены новые атаки с исполнением вредоносных процессов ifconfig и nginx. Мы начали отслеживать стабильный и значимый рост вредоносной деятельности, которая вскоре достигла 13 тысяч атак на Guardcore Global Sensors Network (GGSN). За все время мы отследили 20 различных версий бинарников FritzFrog.


График демонстрирует количество атак FritzFrog на GGSN.

Удивительным оказалось то, что вредоносный код на первый взгляд не связывался с каким-либо сервером командования и контроля (CNC). Только когда мы начали серьезно исследовать ботнет, мы поняли что никакого сервера не было и в помине.

Для перехвата сети ботнета Guardcore Labs разработали на Golang клиент, способный обмениваться ключами с вредоносным ПО, а так же отправлять команды и получать ответы. Эта программа, которую мы затем назвали фроггер, позволила нам исследовать природу и задачи сети, и благодаря фроггеру мы добавили в сеть наши собственные узлы, сумев подсоединиться к ботнету, и поучаствовали в передаче данных активного P2P трафика.

FritzFrog брутфорсил миллионы IP адресов, среди которых оказались правительственные офисы, образовательные учреждения, медицинские центры, банки и множество телекоммуникационных компаний. Из них успешно подвержены атаке оказались более чем 500 серверов, включая известные университеты США и Европы, и одну железнодорожную компанию.

Новое поколение P2P


Почему Новое поколение?


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

  • Безфайловость: FritzFrog работает без рабочей директории, а обмен файлами происходит прямо в памяти через массивы двоичных данных (BLOB).
  • Постоянные обновления: базы данных целей и пораженных устройств обновляются плавно и органично.
  • Агрессивность: брутфорс ведется с использованием обширного словаря. Для примера, недавно обнаруженный P2P ботнет DDG в поле логина использовал только root.
  • Эффективность: Цели равномерно распределены между узлами.
  • Проприетарность: P2P протокол ботнета полностью проприетарен и не основывается на каком-либо из известных P2P протоколов, например TP.

Как только жертва оказывается успешно взломана, на ней запускается UPX-запакованный вредоносный код, который затем тут же сам себя удаляет. Для минимизации подозрений вредоносные процессы исполняются под наименованиями ifconfig и nginx. В самом начале своей работы вредоносный код прослушивает порт 1234 в ожидании команд. Первые полученные команды синхронизируют жертву с базой данных пиров сети и целей брутфорса.


Кластер узлов сети FritzFrog. Каждый узел это зараженный SSH сервер. Размер узлов демонстрирует их связность с остальной сетью.

Трафик на нестандартном порте, например 1234, легко заметить и заблокировать файерволом либо любой другой системой защиты. Поэтому разработчики FritzFrog подошли к проблеме творчески и вместо прямой передачи команд через порт 1234 злоумышленник подключается к жертве через SSH и запускает на устройстве netcat клиент, который в свою очередь соединяется с сервером ботнета. Таким образом, любая команда будет передана через SSH как ввод netcat и без труда достигнет вредоносного кода.


FritzFrog туннелирует свои P2P команды через классический SSH порт, для чего пользуется локальным netcat клиентом зараженного устройства.

Злоумышленники FritzFrog внедрили зашифрованный командный канал с более чем 30 различными командами. Параметры команд и отклики передаются в указанных структурах данных и выпускаются (мобилизуются) в формате JSON. Перед отправлением данные зашифровываются симметричным шифрованием AES и кодируются в Base64. Для обмена ключами участвующие в передаче данных узлы используют протокол Диффи-Хеллмана.



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

Погружение во вредоносный код


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

FritzFrog определяет состояния управления жертвой и целевым устройством следующим образом:
  1. Target (цель): устройство из запроса цели будет затем передано модулю Cracker, который в свою очередь постарается просканировать и взломать его.
  2. Deploy (развертывание): успешно взломанное устройство встает в очередь на заражение вредоносным кодом через модуль DeployMgmt.
  3. Owned (владение): успешно зараженное устройство будет добавлено в P2P сеть модулем Owned.




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


Рабочая функция в дизассемблере. Каждая ветка соответствует поддерживаемому P2P функционалу.

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

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJYZIsncBTFc+iCRHXkeGfFA67j+kUVf7h/IL+sh0RXJn7yDN0vEXz7ig73hC//2/71sND+x+Wu0zytQhZxrCPzimSyC8FJCRtcqDATSjvWsIoI4j/AJyKk5k3fCzjPex3moc48TEYiSbAgXYVQ62uNhx7ylug50nTcUH1BNKDiknXjnZfueiqAO1vcgNLH4qfqIj7WWXu8YgFJ9qwYmwbMm+S7jYYgCtD107bpSR7/WoXSr1/SJLGX6Hg1sTet2USiNevGbfqNzciNxOp08hHQIYp2W9sMuo02pXj9nEoiximR4gSKrNoVesqNZMcVA0Kku01uOuOBAOReN7KJQBt

Вредоносный файл прогоняет всевозможные команды оболочки на локальном устройстве, некоторые по несколько раз, для отслеживания состояния системы. Например, он прогоняет free m для проверки доступной оперативной памяти, uptime, journalctl s @0 u sshd для отслеживания SSH логинов, и прочие команды для вывода статистики нагрузки процессора. Эта статистика оказывается доступна другим узлам в сети, и используется для принятия различных решений, например запускать ли криптомайнер на устройстве или нет. Если решение принято, вредоносный код запускает отдельный процесс, libexec, для майнинга Monero. Этот майнер основан на популярном майнере XMRig и связывается с публичным пулом web.xmrpool.eu через порт 5555.

Злобная торрентоподобная сеть


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

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

Когда узел А хочет получить файл от своего пира, узла B, он моет выслать узлу В запрос getblobstats чтобы узнать какими массивами он владеет. Затем узел А может получить конкретный массив через его хэш, как с помощью P2P команды getbin, так и с помощью HTTP по адресу http://1234/. Как только узел А получает все массивы, он собирает файл через модуль Assemble и запускает его.


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

Присвоение


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

Даже при сравнении с другими P2P ботнетами FritzFrog остается уникальным: он не использует IRC как это делает IRCflu, в отличие от DDG он работает прямо в памяти, и он запускается на Unix-устройствах в противовес ботнету InterPlanetary Storm. Если он на кого и похож, особенно в плане наименования функций и нумерации версий, так это на Rakos, P2P ботнет на Golang, проанализированный ESET еще в 2016 году.

Отслеживание действий И смягчение последствий


Guardcore Labs предоставили скрипт по отслеживанию FritzFrog для запуска на SSH серверах. Он ищет следующие индикаторы ботнета:
  • Запуск процессов nginx, ifconfig или libexec, чей исполняемый файл более в системе не существует (как можно видеть ниже).
  • Прослушивание порта 1234.

В дополнение к этому, TCP трафик через порт 5555 может указывать на сетевой трафик к пулу Monero.

ubuntu@ip-111-11-11-11:~$ ./detect_fritzfrog.sh
FritzFrog Detection Script by Guardicore Labs
=============================================

[*] Fileless process nginx is running on the server.
[*] Listening on port 1234
[*] There is evidence of FritzFrog's malicious activity on this machine.

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

Слабые пароли оказываются ключевой уязвимостью для атак FritzFrog. Мы рекомендуем использовать сильные пароли и публичные ключи авторизации, что намного безопаснее. Кроме того, критически важно исключить публичный ключ FritzFrog из файла authorization_keys чтобы не дать злоумышленникам доступ к устройству. Роутеры и IoT устройства обычно раскрывают свой SSH и потому становятся уязвимы для атак FritzFrog; мы рекомендуем сменить таким устройствам SSH порт или, если функционал не используется, полностью отключить SSH.
Подробнее..

Перевод Как x86_x64 адресует память

22.06.2020 16:10:43 | Автор: admin
Сегодня я собираюсь поговорить про адресацию памяти: один, казалось бы, небольшой, и тем не менее удивительно непростой элемент семантики команд архитектуры х86_64. В особенности хочется поговорить про команду mov и то, как через только одну эту команду х86_64 пользователю становятся доступны различные методы адресации памяти.

Я не буду говорить про остальные затрагивающие память команды (то есть, благодаря CISC, почти все остальные), команды которые пишут массивные фрагменты памяти (это о тебе, fxsave), или иные касающиеся темы вопросы (модели кода, независящий от адреса код, и бинарная релокация). Я также не буду затрагивать исторические режимы адресации или режимы, которые активны при работе процессора x86_64 не в 64-битном режиме (т.е. любые отличные от long mode с 64-битным кодом).

Некоторые ограничения


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

Начнем с хорошего:

  • На достаточно высоком уровне в архитектуре х86_64 есть всего два режима адресации.
  • Все регистры в обоих режимах адресации должны быть строго одинакового размера. Другими словами, мы не можем странным образом смешивать 64, 32 и 16-битные регистры и получать актуальный адрес в кодировании х86_64 для подобного маневра попросту нет места.

В остальном все не столь радужно:

  • Один из этих двух режимов адресации все еще абсурдно сложен.
  • Регистры должны быть одинакового размера, но не обязаны быть той же разрядности что и режим процессора. Например, если мы добавим в нашем кодировании префикс байт (0x67), мы можем пользоваться 32-битными регистрами вместо 64-битных.

Адресация Scale-Index-Base-Displacement


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

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

  • Scale: 2-битный коэффициент константы, обычно равный 1, 2, 4 или 8.
  • Index: Любой регистр общего назначения (rax, rbx, &c).
  • Base: Любой регистр общего назначения.
  • Displacement: Интегральный сдвиг. Обычно даже в 64-битном режиме он ограничен 32 битами, но с использованием некоторых методов кодирования может быть и 64-битным. Мы об этом еще поговорим.

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

  • Displacement
  • Base
  • Base + Index
  • Base + Displacement
  • Base + Index + Displacement
  • Base + (Index * Scale)
  • (Index * Scale) + Displacement
  • Base + (Index * Scale) + Displacement

Давайте разберем каждую комбинацию по порядку.

Displacement


Это, пожалуй, самый простой механизм адресации в семье х86: displacement обрабатывается как абсолютный адрес памяти, и сам по себе, к несчастью, совершенно бесполезен в архитектуре х86_64. Помните, мы говорили, что displacement почти всегда ограничен 32 битами? Так как абсолютный адрес в х86_64 это 64 бита (на самом деле 48, но это неважно), он попросту не поместится в displacement, однако в виде исключения можно использовать 64-битный displacement с регистром a*.

Синтаксис Intel:

; store the qword at 0x00000000000000ff into raxmov rax, [0xff]; store the dword at 0x00000000000000ff into eaxmov eax, [0xff]; store the word at 0x00000000000000ff into axmov ax, [0xff]; store the byte at 0x00000000000000ff into almov al, [0xff]

gas (GNU ассемблер) и в 32, и в 64-битном режимах ссылается на них как на movabs.

Зачем мне (или моему компилятору) пользоваться этим режимом?


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

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

extern long var;void f(long x) { var = x; }

будет:

f:        mov     rax, rdi        movabs  QWORD PTR [var], rax        ret

(Посмотреть на Godbolt.)

Base


Адресация через регистр base добавляет еще один уровень неопределенности поверх абсолютной адресации: вместо использования закодированного в поле displacement команды абсолютного адреса, адрес загружается из указанного регистра общего пользования (Причем любого такого регистра! Ура!)

Эта неопределенность позволяет нам проводить абсолютную адресацию с произвольным регистром назначения по следующему шаблону:

; store the immediate (not displacement) into rbxmov rbx, 0xacabacabacabacab; store the qword at the address stored in rbx into rcxmov rcx, [rbx]

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

Зачем мне (или моему компилятору) пользоваться этим режимом?


Затем что порой после очередной операции мы уже посчитали адрес и хотим им воспользоваться.

Разобрав вариант в displacement, мы можем найти хороший пример такого приема:

mov rax, qword ptr [rax]

Base + Index


Эта комбинация аналогична регистру base за тем исключением, что мы добавляем значение регистра index. Пример:

; store the qword in rcx into the memory address computed; as the sum of the values in rax and rbxmov [rax + rbx], rcx

Зачем мне (или моему компилятору) пользоваться этим режимом?


Найти подходящий пример для такого режима оказалось для меня непростой задачей, поэтому мои коллеги, конечно же, сразу же подобрали один:

int foo(char * buf, int index) {  return buf[index];}

Результат:

push    rbpmov     rbp, rspmov     qword ptr [rbp - 8], rdimov     dword ptr [rbp - 12], esimov     rax, qword ptr [rbp - 8]  ; rax is bufmovsxd  rcx, dword ptr [rbp - 12] ; rcx is indexmovsx   eax, byte ptr [rax + rcx] ; store buf[index] into eaxpop     rbpret

(Посмотреть на Godbolt.)

Оглядываясь назад, мы можем заметить очевидное: в случаях, когда ни стартовый адрес массива, ни сдвиг в массив не зафиксированы в compile-time, Base + Index идеально подходит для моделирования доступов к массиву.

Base + Displacement


Больше неопределенности! Если вы еще не догадались, подсчет актуального адреса и регистром base, и полем displacement соответствует двум следующим операциям:

  1. Мы загружаем значение из регистра base
  2. Мы добавляем загруженное значение к значению поля displacement

Затем мы берем полученную сумму за фактический адрес. Пример:

; add 0xcafe to the value stored in rax; then, store the qword at the computed address into rbxmov rbx, [rax + 0xcafe]

Зачем мне (или моему компилятору) пользоваться этим режимом?


Некоторые режимы адресации, как мы уже видели на примере Base + Index, естественным образом отображают семантику С-подобных массивов. Base + Displacement можно рассматривать в таком же ключе, но со стороны структурной семантики: регистр base содержит адрес к началу структуры, а поле displacement содержит фиксированный сдвиг в эту структуру. Пример:

struct foo {    long a;    long b;};long bar(struct foo *foobar) {    return foobar->b;}

Результат:

push    rbpmov     rbp, rspmov     qword ptr [rbp - 8], rdimov     rax, qword ptr [rbp - 8] ; rax is foobarmov     rax, qword ptr [rax + 8] ; rax + 8 is foobar->b; store back into raxpop     rbpret

(Посмотреть на Godbolt.)

Если задуматься о конструкции и планировке стека в начале каждой функции как о самостоятельной структуре, пример выше становится очевиден: доступы вида [rbp - N] это по сути stack->objN.

Base + Index + Displacement


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

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

; add 0xcafe to the values stores in rax and rcx; then, store the qword at the computer address into rbxmov rbx, [rax + rcx + 0xcafe]

Зачем мне (или моему компилятору) пользоваться этим режимом?


Base + Index + Displacement естественным образом моделирует доступ к структуре внутри массива, точно так же как Base + Displacement естественным образом моделирует доступ к структуре, а Base + Index естественным образом моделирует доступ к массиву.

Не без труда, но с помощью -O1 мне удалось заставить clang скомпилировать пример в Godbolt:

struct foo {    long a;    long b;};long square(struct foo foos[], long i) {    struct foo x = foos[i];    return x.b;}

Краткий результат:

shl     rsi, 4mov     rax, qword ptr [rdi + rsi + 8] ; rdi is foos, rsi is i, 8 is the field offsetret

(Посмотреть на Godbolt.)

Base + (Index * Scale)


Наше первое умножение!

Поле scale похоже на поле displacement тем, что они оба являются закодированным в нашу команду коэффициентом константы, однако scale, в отличие от displacement, сильно ограничен: так как его диапазон составляет всего два бита, значения у scale может быть всего четыре: 1, 2, 4 или 8.

Как можно догадаться из названия, поле scale используется для скалирования, т.е. умножения, другого поля на себя. Если говорить точнее, оно всегда скалирует регистр index, и не может без него использоваться.

Зачем мне (или моему компилятору) пользоваться этим режимом?


В отличие от массива структур из предыдущих примеров, Base + (Index * Scale), кроме всего прочего, естественным образом моделирует доступ к массиву указателей. Пример:

struct foo {    long a;    long b;};long bar(struct foo *foos[], long i) {    struct foo *x = foos[i];    return x->b;}

Результат:

mov     rax, qword ptr [rdi + 8*rsi] ; rdi is foos, rsi is i, 8 is the scale (pointer-sized!)mov     rax, qword ptr [rax + 8]ret

(Посмотреть на Godbolt.)

(Index * Scale) + Displacement


Почти как на примере выше, эта комбинация без лишних сложностей меняет регистр base на поле displacement.

Зачем мне (или моему компилятору) пользоваться этим режимом?


(Index * Scale) + Displacement естественным образом моделирует особый случай доступа к массиву: когда массив можно статически (т.е. глобально) адресовать, а размер элементов можно вычислить через scale. Пример:

int tbl[10];int foo(int i) {    return tbl[i];}

Результат:

movsxd  rax, edimov     eax, dword ptr [4*rax + tbl] ; rax is i, 4 is the scale (sizeof(int) == 4)ret

(Посмотреть на Godbolt.)

Base + (Index * Scale) + Displacement


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

Зачем мне (или моему компилятору) пользоваться этим режимом?


Base + (Index * Scale) + Displacement естественным образом моделирует доступ к двумерному массиву. Пример:

long tbl[10][10];long foo(long i, long j) {    return tbl[i][j];}

Результат:

lea     rax, [rdi + 4*rdi]shl     rax, 4mov     rax, qword ptr [rax + 8*rsi + tbl]ret

(Посмотреть на Godbolt.)

RIP-относительная адресация


Выше мы описали режим адресации, который почти идентичен своему историческому эквиваленту в х86_32, и отличается в первую очередь использованием 64-битных регистров GPR, и порой 64-битными смещениями. Однако главным отличием является добавление абсолютно нового режима адресации под названием RIP-относительная (RIP-relative) адресация.

Почему этот режим называется RIP-относительным? Потому что он кодирует смещение относительно значения регистра RIP (в особенности RIP не текущей, а следующей команды). Обычно это выражается уже знакомым нам синтаксисом [Base + Displacement], вот только вместо GPR регистром base теперь является rip. Пример:

mov rax, [rip + 16]

Зачем мне (или моему компилятору) пользоваться этим режимом?


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

Пример компиляции с использованием -O1 и -fpic:

long tbl[10];int foo(int i) {    return tbl[i];}

Для него в архитектуре х86_64 нам потребуются всего два mov:

foo:        mov     rax, qword ptr [rip + tbl@GOTPCREL]        mov     rax, qword ptr [rax + 8*rdi]        ret

Однако в архитектуре х86_32 нам их потребуется три, плюс шаблоны:

foo:        call    .L0$pb.L0$pb:        pop     eax.Ltmp0:        add     eax, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb)        mov     ecx, dword ptr [esp + 4]        mov     eax, dword ptr [eax + tbl@GOT]        mov     eax, dword ptr [eax + 4*ecx]        ret

Напоследок: сегментация


Архитектура х86_64 убрала всю сегментацию, но только почти. Благодаря плоскому адресному пространству регистры сегментов более не требуются, но местами все еще проявляются:

  • Linux (точнее glibc) использует fs в пользовательском пространстве для доступа к настраиваемым ядром сегментам TLS. Спецификацию этих сегментов можно обнаружить в per-CPU конфигурации GDT (ссылка). Если предположить что ничего в glibc (или любой вашей libc) не использует gs, вы можете свободно им пользоваться.
  • В пространстве ядра Linux использует gs для хранения основного адреса региона per-CPU переменной. Мы можем наблюдать это в определении макроса PER_CPU_VAR (ссылка):
    #define PER_CPU_VAR(var)  %__percpu_seg:var
    
  • В х86_64 определение растет:
    %gs:var
    

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

Кстати, вот пример локально-поточной переменной:

int __thread x = 0;int foo(void) {    int *y = &x;    return *y;}

Результат:

push    rbpmov     rbp, rspmov     rax, qword ptr fs:[0]    ; grab the base address of the thread-local storage arealea     rax, [rax + x@TPOFF]     ; calculate the effective address of x within the TLSmov     qword ptr [rbp - 8], rax ; store the address of x into ymov     rax, qword ptr [rbp - 8]mov     eax, dword ptr [rax]pop     rbpret

(Прочесть на Godbolt.)
Подробнее..

Перевод Разбираемся в моделях кода архитектуры x64

01.07.2020 18:14:54 | Автор: admin
Какой моделью кода мне воспользоваться? часто возникающий, но нечасто разбираемый вопрос при написании кода для архитектуры х64. Тем не менее, это довольно интересная проблема, и для понимания генерируемого компиляторами машинного кода х64 полезно иметь представление о моделях кода. Кроме того, для тех, кто беспокоится о производительности вплоть до мельчайших команд, выбор модели кода влияет и на оптимизацию.

Информация по этой теме в сети, или где бы то ни было еще, встречается редко. Самым важным из доступных ресурсов является официальный х64 ABI, скачать его можно по ссылке (далее по тексту он будет упоминаться как ABI). Часть информации также можно найти на man-страницах gcc. Задача данной статьи предоставить доступные рекомендации по теме, обсудить связанные с ней вопросы, а так же хорошими примерами через используемый в работе код продемонстрировать некоторые концепты.

Важное замечание: эта статья не является обучающим материалом для начинающих. Перед ознакомлением рекомендуется уверенное владение C и ассемблером, а так же базовое знакомство с архитектурой х64.



Также смотрите нашу предыдущую публикацию на схожую тему: Как x86_x64 адресует память



Модели кода. Мотивационная часть


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

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

Таким образом, компромиссом становятся модели кода. [1] Модель кода это формальное соглашение между программистом и компилятором, в котором программист указывает свои намерения относительно размера ожидаемой программы (или программ), в которую попадет компилируемый в данный момент объектный модуль. [2] Модели кода нужны для того чтобы программист мог сказать компилятору: не волнуйся, этот объектный модуль пойдет только в небольшие программы, так что можно пользоваться быстрыми RIP-относительными режимами адресации. С другой стороны, он может сказать компилятору следующее: мы собираемся компоновать этот модуль в большие программы, так что пожалуйста используй неторопливые и безопасные абсолютные режимы адресации с полным 64-битным сдвигом.

О чем расскажет эта статья


Мы поговорим о двух описанных выше сценариях, малой модели кода и большой модели кода: первая модель говорит компилятору, что 32-битного относительного сдвига должно хватить для всех ссылок на код и данные в объектном модуле; вторая настаивает на использовании компилятором абсолютных 64-битных режимов адресации. Кроме того, существует еще и промежуточный вариант, так называемая средняя модель кода.

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

Исходный пример на С


Для демонстрации обсуждаемых в этой статье концептов я воспользуюсь представленной ниже программой на С и скомпилирую ее с различными моделями кода. Как можно видеть, функция main получает доступ к четырем разным глобальным массивам и одной глобальной функции. Массивы отличаются двумя параметрами: размером и видимостью. Размер важен для объяснения средней модели кода и не понадобится в работе с малой и большой моделями. Видимость важна для работы PIC-моделей кода и бывает либо статичной (видимость только в исходном файле), либо глобальной (видимость всем скомпонованным в программу объектам).

int global_arr[100] = {2, 3};static int static_arr[100] = {9, 7};int global_arr_big[50000] = {5, 6};static int static_arr_big[50000] = {10, 20};int global_func(int param){    return param * 10;}int main(int argc, const char* argv[]){    int t = global_func(argc);    t += global_arr[7];    t += static_arr[7];    t += global_arr_big[7];    t += static_arr_big[7];    return t;}

gcc использует модель кода как значение опции -mcmodel. Кроме того, флагом -fpic можно задать PIC компиляцию.

Пример компиляции в объектный модуль через большую модель кода с использованием PIC:

> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o

Малая модель кода


Перевод цитаты из man gcc на тему малой модели кода:

-mcmodel=small
Генерация кода для малой модели: программа и ее символы должны быть скомпонованы в нижних двух гигабайтах адресного пространства. Размер указателей 64 бит. Программы могут быть скомпонованы и статически, и динамически. Это основная модель кода.


Другими словами, компилятор может спокойно считать, что код и данные доступны через 32-битный RIP-относительный сдвиг из любой команды в коде. Давайте взглянем на дизассемблированный пример программы на С, которую мы скомпилировали через не-PIC малую модель кода:

> objdump -dS codemodel1_small.o[...]int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: e8 00 00 00 00          callq  33 <main+0x1e>  33: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  3c: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  45: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  48: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  4e: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  51: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  57: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  5a: 8b 45 fc                mov    -0x4(%rbp),%eax}  5d: c9                      leaveq  5e: c3                      retq

Как можно видеть, доступ ко всем массивам организован одинаково с использованием RIP-относительного сдвига. Однако в коде сдвиг равен 0, поскольку компилятор не знает, где будет размещен сегмент данных, поэтому для каждого такого доступа он создает релокацию:

> readelf -r codemodel1_small.oRelocation section '.rela.text' at offset 0x62bd8 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend00000000002f  001500000002 R_X86_64_PC32     0000000000000000 global_func - 4000000000038  001100000002 R_X86_64_PC32     0000000000000000 global_arr + 18000000000041  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b800000000004a  001200000002 R_X86_64_PC32     0000000000000340 global_arr_big + 18000000000053  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098

Давайте в качестве примера полностью декодируем доступ к global_arr. Интересующий нас дизассемблированный сегмент:

  t += global_arr[7];36:       8b 05 00 00 00 00       mov    0x0(%rip),%eax3c:       01 45 fc                add    %eax,-0x4(%rbp)

RIP-относительная адресация относительна очередной команде, таким образом сдвиг необходимо запатчить в команду mov таким образом, чтобы он соответствовал 0х3с. Нас интересует вторая релокация, R_X86_64_PC32, она указывает на операнд mov по адресу 0x38 и означает следующее: берем значение символа, добавляем слагаемое и вычитаем указываемый релокацией сдвиг. Если вы все корректно посчитали, вы увидите как результат разместит относительный сдвиг между очередной командой и global_arr, плюс 0х1с. Поскольку 0х1с означает седьмой int в массиве (в архитектуре х64 размер каждого int составляет 4 байта), то этот относительный сдвиг нам и нужен. Таким образом, используя RIP-относительную адресацию, команда корректно ссылается на global_arr[7].

Также интересно отметить следующее: пусть команды доступа к static_arr здесь и схожи, его переадресация использует другой символ, тем самым вместо конкретного символа указывая в секцию .data. Виной тому действия компоновщика, он размещает статический массив в известном месте секции, и таким образом массив нельзя использовать совместно с другими общими библиотеками. В итоге компоновщик урегулирует ситуацию с этой релокацией. С другой стороны, поскольку global_arr может быть использован (или перезаписан) другой общей библиотекой, уже динамический загрузчик должен будет разобраться со ссылкой к global_arr. [3]

Наконец, давайте взглянем на отсылку к global_func:

  int t = global_func(argc);24:       8b 45 ec                mov    -0x14(%rbp),%eax27:       89 c7                   mov    %eax,%edi29:       b8 00 00 00 00          mov    $0x0,%eax2e:       e8 00 00 00 00          callq  33 <main+0x1e>33:       89 45 fc                mov    %eax,-0x4(%rbp)

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

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

Большая модель кода


Перевод цитаты из man gcc на тему большой модели кода:

-mcmodel=large
Генерация кода для большой модели: Эта модель не делает предположений относительно адресов и размеров секций.

Пример дизассемблированного кода main, скомпилированного при помощи не-PIC большой модели:

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: 48 ba 00 00 00 00 00    movabs $0x0,%rdx  35: 00 00 00  38: ff d2                   callq  *%rdx  3a: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  3d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  44: 00 00 00  47: 8b 40 1c                mov    0x1c(%rax),%eax  4a: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  4d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  54: 00 00 00  57: 8b 40 1c                mov    0x1c(%rax),%eax  5a: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  5d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  64: 00 00 00  67: 8b 40 1c                mov    0x1c(%rax),%eax  6a: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  6d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  74: 00 00 00  77: 8b 40 1c                mov    0x1c(%rax),%eax  7a: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  7d: 8b 45 fc                mov    -0x4(%rbp),%eax}  80: c9                      leaveq  81: c3                      retq

И вновь полезно взглянуть на релокации:

Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend000000000030  001500000001 R_X86_64_64       0000000000000000 global_func + 000000000003f  001100000001 R_X86_64_64       0000000000000000 global_arr + 000000000004f  000300000001 R_X86_64_64       0000000000000000 .data + 1a000000000005f  001200000001 R_X86_64_64       0000000000000340 global_arr_big + 000000000006f  000300000001 R_X86_64_64       0000000000000000 .data + 31080

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

  t += global_arr[7];3d:       48 b8 00 00 00 00 00    movabs $0x0,%rax44:       00 00 0047:       8b 40 1c                mov    0x1c(%rax),%eax4a:       01 45 fc                add    %eax,-0x4(%rbp)

Двум командам необходимо получить желаемое значение из массива. Первая команда размещает абсолютный 64-битный адрес в rax, который, как мы скоро увидим, окажется адресом global_arr, тогда как вторая команда загружает слово из (rax) + 0х1с в eax.

Так что давайте сфокусируемся на команде по адресу 0x3d, movabs, абсолютной 64-битной версии mov в архитектуре х64. Она может забросить полную 64-битную константу прямо в регистр, и так как в нашем дизассемблированном коде значение этой константы равно нулю, за ответом нам придется обратиться к таблице релокаций. В ней мы найдем абсолютную релокацию R_X86_64_64 для операнда по адресу 0x3f, со следующим значением: размещение значения символа плюс слагаемого обратно в сдвиг. Другими словами, rax будет содержать абсолютный адрес global_arr.

А что насчет функции вызова?

  int t = global_func(argc);24:       8b 45 ec                mov    -0x14(%rbp),%eax27:       89 c7                   mov    %eax,%edi29:       b8 00 00 00 00          mov    $0x0,%eax2e:       48 ba 00 00 00 00 00    movabs $0x0,%rdx35:       00 00 0038:       ff d2                   callq  *%rdx3a:       89 45 fc                mov    %eax,-0x4(%rbp)

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

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

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

Средняя модель кода


Как и ранее, давайте взглянем на перевод цитаты из man gcc:

-mcmodel=medium
Генерация кода для средней модели: Программа скомпонована в нижних двух гигабайтах адресного пространства. Здесь же расположены и малые символы. Символы размера большего, чем задано через -mlarge-data-threshold, попадают в больше данные или секции bss и могут находиться выше двух гигабайт. Программы могут быть скомпонованы и статически, и динамически.

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

Также важно отметить, что при работе со средней моделью кода для больших данных по аналогии с секциями .data и .bss создаются специальные секции: .ldata и .lbss. Это не так важно в призме темы текущей статьи, однако я собираюсь немного от нее отклониться. Детальнее с данным вопросом можно ознакомиться в ABI.

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

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: e8 00 00 00 00          callq  33 <main+0x1e>  33: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  3c: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  45: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  48: 48 b8 00 00 00 00 00    movabs $0x0,%rax  4f: 00 00 00  52: 8b 40 1c                mov    0x1c(%rax),%eax  55: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  58: 48 b8 00 00 00 00 00    movabs $0x0,%rax  5f: 00 00 00  62: 8b 40 1c                mov    0x1c(%rax),%eax  65: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  68: 8b 45 fc                mov    -0x4(%rbp),%eax}  6b: c9                      leaveq  6c: c3                      retq

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

Средняя модель кода это умелый компромисс между большой и малой моделями. Навряд ли код программы окажется слишком велик [4], так что подвинуть его сверх лимита в два гигабайта могут разве что статически скомпонованные в него большие куски данных, возможно как часть некоего объемного табличного поиска. Так как средняя модель кода отсеивает такие объемные куски данных и особым образом их обрабатывает, то вызовы кодом функций и малых символов будут так же эффективны, как и в малой модели кода. Только обращения к большим символам, по аналогии с большой моделью, потребуют от кода воспользоваться полным 64-битным методом большой модели.

Малая PIC-модель кода


Теперь давайте посмотрим на PIC варианты моделей кода, и как и раньше мы начнем с малой модели. [5] Ниже можно видеть пример кода, скомпилированного через малую PIC-модель:

int main(int argc, const char* argv[]){  15:   55                      push   %rbp  16:   48 89 e5                mov    %rsp,%rbp  19:   48 83 ec 20             sub    $0x20,%rsp  1d:   89 7d ec                mov    %edi,-0x14(%rbp)  20:   48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24:   8b 45 ec                mov    -0x14(%rbp),%eax  27:   89 c7                   mov    %eax,%edi  29:   b8 00 00 00 00          mov    $0x0,%eax  2e:   e8 00 00 00 00          callq  33 <main+0x1e>  33:   89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  3d:   8b 40 1c                mov    0x1c(%rax),%eax  40:   01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  43:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  49:   01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  4c:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  53:   8b 40 1c                mov    0x1c(%rax),%eax  56:   01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  59:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  5f:   01 45 fc                add    %eax,-0x4(%rbp)    return t;  62:   8b 45 fc                mov    -0x4(%rbp),%eax}  65:   c9                      leaveq  66:   c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend00000000002f  001600000004 R_X86_64_PLT32    0000000000000000 global_func - 4000000000039  001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4000000000045  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b800000000004f  001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 400000000005b  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098

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

Как можно заметить, нет никакой разницы между сгенерированным для статичных массивов кодом и кодом в не-PIC случае. Это один из плюсов архитектуры x64: благодаря IP-относительному доступу к данным, мы бонусом получаем PIC, по крайней мере до тех пор пока не требуется внешний доступ к символам. Все команды и релокации остаются теми же, так что обрабатывать их лишний раз не нужно.

Интересно обратить внимание на глобальные массивы: стоит напомнить, что в PIC глобальные данные должны проходить через GOT, поскольку в какой-то момент их могут хранить, или пользоваться ими, общие библиотеки [6]. Ниже можно видеть код для доступа к global_arr:

  t += global_arr[7];36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax3d:   8b 40 1c                mov    0x1c(%rax),%eax40:   01 45 fc                add    %eax,-0x4(%rbp)

Интересующая нас релокация это R_X86_64_GOTPCREL: позиция входа символа в GOT плюс слагаемое, минус сдвиг за применение релокации. Другими словами, в команду патчится относительный сдвиг между RIP (следующей инструкции) и зарезервированного для global_arr в GOT слота. Таким образом, в rax в команду по адресу 0x36 размещается фактический адрес global_arr. Следом за этим шагом идет сброс ссылки на адрес global_arr плюс сдвиг на его седьмой элемент в eax.

Теперь давайте взглянем на вызов функции:

  int t = global_func(argc);24:   8b 45 ec                mov    -0x14(%rbp),%eax27:   89 c7                   mov    %eax,%edi29:   b8 00 00 00 00          mov    $0x0,%eax2e:   e8 00 00 00 00          callq  33 <main+0x1e>33:   89 45 fc                mov    %eax,-0x4(%rbp)

В ней есть релокация операнда callq по адресу 0x2e, R_X86_64_PLT32: адрес PLT входа для символа плюс слагаемое, минус сдвиг за применение релокации. Другими словами, callq должен корректно вызывать PLT трамплин для global_func.

Обратите внимание, какие неявные предположения совершает компилятор: что к GOT и PLT можно получить доступ через RIP-относительную адресацию. Это будет важно при сравнении этой модели с другими PIC-вариантами моделей кода.

Большая PIC-модель кода


Дизассемблирование:

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 53                      push   %rbx  1a: 48 83 ec 28             sub    $0x28,%rsp  1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx  25: 49 bb 00 00 00 00 00    movabs $0x0,%r11  2c: 00 00 00  2f: 4c 01 db                add    %r11,%rbx  32: 89 7d dc                mov    %edi,-0x24(%rbp)  35: 48 89 75 d0             mov    %rsi,-0x30(%rbp)    int t = global_func(argc);  39: 8b 45 dc                mov    -0x24(%rbp),%eax  3c: 89 c7                   mov    %eax,%edi  3e: b8 00 00 00 00          mov    $0x0,%eax  43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx  4a: 00 00 00  4d: 48 01 da                add    %rbx,%rdx  50: ff d2                   callq  *%rdx  52: 89 45 ec                mov    %eax,-0x14(%rbp)    t += global_arr[7];  55: 48 b8 00 00 00 00 00    movabs $0x0,%rax  5c: 00 00 00  5f: 48 8b 04 03             mov    (%rbx,%rax,1),%rax  63: 8b 40 1c                mov    0x1c(%rax),%eax  66: 01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr[7];  69: 48 b8 00 00 00 00 00    movabs $0x0,%rax  70: 00 00 00  73: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  77: 01 45 ec                add    %eax,-0x14(%rbp)    t += global_arr_big[7];  7a: 48 b8 00 00 00 00 00    movabs $0x0,%rax  81: 00 00 00  84: 48 8b 04 03             mov    (%rbx,%rax,1),%rax  88: 8b 40 1c                mov    0x1c(%rax),%eax  8b: 01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr_big[7];  8e: 48 b8 00 00 00 00 00    movabs $0x0,%rax  95: 00 00 00  98: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  9c: 01 45 ec                add    %eax,-0x14(%rbp)    return t;  9f: 8b 45 ec                mov    -0x14(%rbp),%eax}  a2: 48 83 c4 28             add    $0x28,%rsp  a6: 5b                      pop    %rbx  a7: c9                      leaveq  a8: c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9
000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0
000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0
00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0
00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0
000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0

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

1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx25: 49 bb 00 00 00 00 00    movabs $0x0,%r112c: 00 00 002f: 4c 01 db                add    %r11,%rbx

Ниже можно прочесть перевод связанной с этим цитаты из ABI:

В малой модели кода ко всем адресам (включая GOT) можно получить доступ через предоставленную архитектурой AMD64 IP-относительную адресацию. Именно поэтому нет нужды в явном указателе GOT и таким образом нет нужды в устанавливающем его прологе в функции. В большой и средней моделях кода необходимо определить регистр для хранения адреса GOT в независимых от расположения объектах, поскольку AMD64 ISA не поддерживает мгновенное перемещение размером больше чем 32 бит.

Давайте посмотрим на то, как описанный выше пролог вычисляет адрес GOT. Во-первых, команда по адресу 0x1e загружает свой собственный адрес в rbx. Затем совместно с релокацией R_X86_64_GOTPC64 совершается абсолютный 64-битный шаг в r11. Эта релокация означает следующее: берем адрес GOT, вычитаем перемещаемый сдвиг и добавляем слагаемое. Наконец, команда по адресу 0x2f складывает оба результата вместе. Итогом становится абсолютный адрес GOT в rbx. [7]

Зачем же мучиться с вычислением адреса GOT? Во-первых, как отмечено в цитате, в большой модели кода мы не можем предполагать, что 32-битного RIP-относительного сдвига будет достаточно для адресации GOT, из-за чего нам и требуется полный 64-битный адрес. Во-вторых, мы все еще хотим работать с PIC-вариацией, так что мы не можем попросту поместить абсолютный адрес в регистр. Скорее сам адрес должен быть вычислен относительно RIP. Для этого и нужен пролог: он совершает 64-битное RIP-относительное вычисление.

В любом случае, раз у нас в rbx теперь есть адрес GOT, давайте посмотрим на то как получить доступ к static_arr:

  t += static_arr[7];69:       48 b8 00 00 00 00 00    movabs $0x0,%rax70:       00 00 0073:       8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax77:       01 45 ec                add    %eax,-0x14(%rbp)

Релокация первой команды это R_X86_64_GOTOFF64: символ плюс слагаемое минус GOT. В нашем случае это относительный сдвиг между адресом static_arr и адресом GOT. Следующая инструкция добавляет результат в rbx (абсолютный адрес GOT) и сбрасывает ссылку со сдвигом по 0x1c. Для простоты визуализации такого вычисления ниже можно ознакомиться с псевдо-C примером:

// char* static_arr// char* GOTrax = static_arr + 0 - GOT;  // rax now contains an offseteax = *(rbx + rax + 0x1c);   // rbx == GOT, so eax now contains                             // *(GOT + static_arr - GOT + 0x1c) or                             // *(static_arr + 0x1c)

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

Но что насчет global_arr?

  t += global_arr[7];55:       48 b8 00 00 00 00 00    movabs $0x0,%rax5c:       00 00 005f:       48 8b 04 03             mov    (%rbx,%rax,1),%rax63:       8b 40 1c                mov    0x1c(%rax),%eax66:       01 45 ec                add    %eax,-0x14(%rbp)

Этот код несколько длиннее, а релокация отличается от обычной. По сути, GOT используется здесь более традиционным образом: релокация R_X86_64_GOT64 для movabs лишь говорит функции разместить сдвиг в GOT, там где в rax расположен адрес global_arr. Команда по адресу 0x5f берет адрес global_arr из GOT и помещает его в rax. Следующая команда сбрасывает ссылку на global_arr[7] и помещает значение в eax.

Теперь давайте взглянем на ссылку кода для global_func. Вспомним что в большой модели кода мы не могли делать предположений относительно размера секций кода, так что нам следует полагать что даже для доступа к PLT нам потребуется абсолютный 64-битный адрес:

  int t = global_func(argc);39: 8b 45 dc                mov    -0x24(%rbp),%eax3c: 89 c7                   mov    %eax,%edi3e: b8 00 00 00 00          mov    $0x0,%eax43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx4a: 00 00 004d: 48 01 da                add    %rbx,%rdx50: ff d2                   callq  *%rdx52: 89 45 ec                mov    %eax,-0x14(%rbp)

Интересующая нас релокация это R_X86_64_PLTOFF64: адрес PLT входа для global_func минус адрес GOT. Результат размещается в rdx, куда затем помещается rbx (абсолютный адрес GOT). В итоге мы получаем адрес PLT входа для global_func в rdx.

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

Средняя PIC-модель кода


Наконец, мы разберем сгенерированный для средней PIC-модели код:

int main(int argc, const char* argv[]){  15:   55                      push   %rbp  16:   48 89 e5                mov    %rsp,%rbp  19:   53                      push   %rbx  1a:   48 83 ec 28             sub    $0x28,%rsp  1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx  25:   89 7d dc                mov    %edi,-0x24(%rbp)  28:   48 89 75 d0             mov    %rsi,-0x30(%rbp)    int t = global_func(argc);  2c:   8b 45 dc                mov    -0x24(%rbp),%eax  2f:   89 c7                   mov    %eax,%edi  31:   b8 00 00 00 00          mov    $0x0,%eax  36:   e8 00 00 00 00          callq  3b <main+0x26>  3b:   89 45 ec                mov    %eax,-0x14(%rbp)    t += global_arr[7];  3e:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  45:   8b 40 1c                mov    0x1c(%rax),%eax  48:   01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr[7];  4b:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  51:   01 45 ec                add    %eax,-0x14(%rbp)    t += global_arr_big[7];  54:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  5b:   8b 40 1c                mov    0x1c(%rax),%eax  5e:   01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr_big[7];  61:   48 b8 00 00 00 00 00    movabs $0x0,%rax  68:   00 00 00  6b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  6f:   01 45 ec                add    %eax,-0x14(%rbp)    return t;  72:   8b 45 ec                mov    -0x14(%rbp),%eax}  75:   48 83 c4 28             add    $0x28,%rsp  79:   5b                      pop    %rbx  7a:   c9                      leaveq  7b:   c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend000000000021  00160000001a R_X86_64_GOTPC32  0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4000000000037  001700000004 R_X86_64_PLT32    0000000000000000 global_func - 4000000000041  001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 400000000004d  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8000000000057  001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4000000000063  000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0

Для начала давайте уберем вызов функции. Аналогично малой модели, в средней модели мы предполагаем, что ссылки на код не превышают пределов 32-битного RIP сдвига, следовательно, код для вызова global_func полностью аналогичен такому же коду в малой PIC-модели, равно как и для случаев массивов малых данных static_arr и global_arr. Поэтому мы сфокусируемся на массивах больших данных, но сначала поговорим о прологе: здесь он отличается от пролога большой модели данных.

1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx

Это весь пролог: чтобы при помощи релокации R_X86_64_GOTPC32 разместить GOT адрес в rbx, потребовалась всего одна команда (по сравнению с тремя в большой модели). В чем же разница? Дело в том, что так как в средней модели GOT не является частью секций больших данных, мы предполагаем его доступность в рамках 32-битного сдвига. В большой модели мы не могли совершать подобные предположения, и были вынуждены пользоваться полным 64-битным сдвигом.

Вызывает интерес тот факт, что код для доступа к global_arr_big похож на такой же код в малой PIC-модели. Это происходит по той же причине, почему пролог средней модели короче пролога большой модели: мы полагаем доступность GOT в рамках 32-битной RIP-относительной адресации. Действительно, к самому global_arr_big нельзя получить такой доступ, но этот случай все равно покрывает GOT, так как фактически global_arr_big в нем и находится, причем в виде полного 64-битного адреса.

Ситуация, тем не менее, отличается для static_arr_big:

  t += static_arr_big[7];61:   48 b8 00 00 00 00 00    movabs $0x0,%rax68:   00 00 006b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax6f:   01 45 ec                add    %eax,-0x14(%rbp)

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

Примечания:


[1] Не стоит путать модели кода с 64-битными моделями данных и моделями памяти Intel, все это разные темы.

[2] Важно помнить: собственно команды создает компилятор, и режимы адресации фиксируются именно на этом шаге. Компилятор не может знать в какие программы или общие библиотеки попадет объектный модуль, одни могут оказаться малыми, а другие большими. Компоновщику известен размер итоговой программы, но уже слишком поздно: компоновщик может лишь патчить сдвиг команд релокацией, а не менять сами команды. Таким образом, соглашение модели кода должно быть подписано программистом на этапе компиляции.

[3] Если что-то осталось непонятным, ознакомьтесь со следующей статьей.

[4] Тем не менее, объемы постепенно растут. Когда я в последний раз проверял Debug+Asserts билд Clang, он почти достигал одного гигабайта, за что во многом спасибо автогенерируемому коду.

[5] Если вы еще не знаете как работает PIC (как в целом, так и в частности для архитектуры x64), самое время ознакомиться со следующими статьями по теме: раз и два.

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

[7] 0x25 0x7 + GOT 0x27 + 0x9 = GOT



Подробнее..

Перевод Как рендерится кадр DOOM Ethernal

11.09.2020 14:12:59 | Автор: admin


Вступление


Doom Eternal не нуждается в отдельном представлении: это прямой преемник Doom 2016, разработанный благодаря седьмой итерации id Tech, внутреннего движка студии id Software. В свое время меня поразило и высокое качество визуальной составляющей Doom 2016, и простота и элегантность технических решений. В этом отношении Doom Eternal превосходит своего предшественника во многих областях, и некоторые из них достойны детального разбора. В этой аналитической статье я постараюсь обсудить их все.

Мой анализ вдохновлен трудом Adrian Courrges про Doom 2016 (перевод). Я считаю, что подобные работы позволяют взглянуть на подходы к решению некоторых проблем рендеринга AAA-проектов и тем самым становятся превосходными обучающими материалами. В этом анализе я планирую обсудить общие особенности и не погружаться слишком глубоко в тонкости каждого способа и прохода рендеринга. Кроме того, некоторые проходы в Doom Eternal почти не отличаются от своих аналогов в Doom 2016 и уже были разобраны в труде Adrian Courrges, поэтому я могу их пропустить.

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

Итак, приступим.

С выходом id Tech 7 переход движка с OpenGL на Vulkan API позволил разработчикам эффективнее работать с особенностями текущего поколения графических процессоров, например несвязанными ресурсами (bindless resources).

Один кадр в Doom Eternal




Выше мы можем видеть близкую к началу секцию игры: интерьер с несколькими противниками и объемным освещением. По аналогии с его предшественником, процессом визуализации в Doom Eternal заведует прямой рендеринг, однако если Doom 2016 вынужден проводить прямой рендеринг совместно с G-буферизацией отражающих поверхностей, в нашем случае буфер не используется и прямой рендеринг берет на себя все задачи.

Уход от мегатекстур


С выходом созданной на движке id Tech 5 игры Rage мир познакомился с концептом реализации текстур под названием мегатекстуры. Этот метод применяется в Doom 2016 и на каждый кадр он рендерит так называемую виртуальную текстуру с информацией о видимых текстурах. Виртуальная текстура анализируется в следующем кадре, чтобы определить какие текстуры следует подгрузить с диска. Однако у мегатекстур есть очевидная проблема: как только текстура попадает поле зрения, подгружать ее уже поздновато, поэтому на первых нескольких кадрах после появления текстура выглядит размыто. С выходом id Tech 7 разработчики отказались от такого метода.

Скиннинг через графический процессор


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

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

По ссылке можно ознакомиться с отличной статьей о скиннинге через вычислительный шейдер за авторством Jnos Turnszki.

Полезно также отметить, что в Doom Eternal используется интересный вид кеширования Alembic Cache, сравнимый с сильно сжатым обратно воспроизводимым видео. В таких кешах хранится запеченная анимация для выдачи и разжатия в ходе исполнения программы. Цитируя технический анализ Digital Foundry, Alembic Cache применяется в широком диапазоне анимаций, начиная с масштабных кинематографичных сцен и заканчивая крохотными щупальцами на полу. Особенно удобен такой подход для анимаций со сложностями реализации через скиновую анимацию, например для органики и симуляции тканей. Если вас заинтересовала эта технология, рекомендую ознакомиться с презентацией Axel Gneiting на Siggraph 2014.

Карты теней


Следующим этапом является рендеринг теней, и подход к генерации их карт на первый взгляд у id Tech 7 и его предшественника не отличается.

Как можно видеть ниже, тени рендерятся в большую текстуру глубиной 24 бита и размером 4096 на 8196 пикселей, местами различаясь по уровню качества. Текстура не меняется между кадрами, и согласно презентации Devil is in the Details на Siggraph 2016, статичная геометрия кешируется в карте теней, чтобы не перерисовать ее для каждого кадра. Идея сама по себе проста: нам не нужно обновлять тени до тех пор, пока перед источником света не начнется движение, и таким образом мы можем объявить кешированную карту теней: обычную карту со статичной геометрией, так как мы полагаем, что геометрия не меняется. Если в конусе обзора движется динамический объект, кешированная карта теней копируется в основную, и поверх этого перерисовывается динамическая геометрия. Такой подход позволяет не перерисовывать всю сцену в конусе обзора при каждом ее обновлении. Естественно, при смещении света всю сцену придется перерисовать с нуля.

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

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



Скорость и предварительный проход обработки глубины


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


Оружие игрока


Статические объекты


Динамические объекты

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



Z-иерархическая глубина


Следующий шаг это генерация иерархической mip-цепи буфера глубины: эта цепь похожа на mip карту, но вместо усреднения четырех соседних пикселей берет их максимальное значение. Такой подход часто используется в графике для множества задач, например для ускорения отражений и отбрасывания прегражденной геометрии. В нашем случае mip-цепь отбрасывает освещение и декальные текстуры, о которых мы поговорим позднее. В последнее время mip-генерацию проводят за один проход, с записью сразу в несколько mip-ов, но в Doom Eternal запись все еще ведется отдельно для каждого mip.



Декали сеток


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

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

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

Ниже мы можем видеть сетку декальных текстур. Для удобства визуализации идентификаторы покрашены в разные цвета.



Отбрасывание света и декалей


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

В Doom 2016 использовался процессорный вариант кластерного отбрасываения света: свет и декали собирались в конусообрзаные фроксели, которые затем считывались в ходе шейдинга через определения индекса кластера из позиции пикселя. Размер каждого кластера составлял 256 пикселей и для сохранения квадратной формы логарифмически делился на 24 сегмента. Такой прием вскоре переняли многие другие разработчики, и похожие методы встречаются, например, в Detroit: Become Human и Just Cause.

Учитывая рост числа источников динамического освещения (сотни) и декалей (тысячи), процессорной кластеризации отбрасывания освещения в Doom Eternal уже не хватало, так как воксели становились слишком грубыми. В итоге для id Tech 7 разработчики придуман иной подход, и через исполняемые на различных этапах вычислительные шейдеры создали программный растеризатор. Сначала декали и свет связываются в гексаэдр (шестигранник) и передаются в вычислительный растеризатор, откуда вершины проецируются в экранное пространство. Затем второй вычислительный шейдер обрезает треугольники по границам экрана и собирает их в тайлы размером 256 на 256 пикселей. Одновременно с этим по аналогии с кластерным отбрасыванием отдельные элементы источников света и декалей записываются во фроксели, после чего следующий вычислительный шейдер проводит похожую процедуру под тайлы 32 на 32 пикселя. В каждом тайле прошедшие тест на глубину элементы помечаются в битовое поле. Последний вычислительный шейдер переводит битовые поля в список источников света, которые в итоге используются при проходе освещения. Что характерно, индексы элементов все еще записываются в трехмерные фроксели размером 256 на 256 пикселей по аналогии с кластерным подходом. В местах со значительным прерыванием глубины, для определения числа источников света в каждом тайле сравнивается минимальное значение и нового списка источников света, и старого списка кластерных источников.

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

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

Преграждение окружающего света в экранном пространстве


Преграждение окружающего света вычисляется в половинном разрешении вполне стандартным путем: сначала 16 случайных лучей исходят из позиции каждого пикселя в полусфере, а затем при помощи буфера глубины определяются пересекающиеся с геометрией лучи. Чем больше лучей пересекает геометрии, тем больше будет преграждение. Эта техника называется преграждение направленного света в экранном пространстве (Screen Space Directional Occlusion), или SSDO, и подробное его описание за авторством Yuriy O`Donnell можно прочитать по ссылке. Вместо традиционного хранения значений преграждения в одноканальной текстуре, направленное преграждение хранится в трехкомпонентной текстуре, а итоговое преграждение определяется через скалярное произведение над нормалью пикселя.

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



Непрозрачный прямой проход


В этом проходе многие элементы наконец встают на свои места. В отличие от Doom 2016, здесь все рендерится напрямую через несколько массивных мегашейдеров. Во всей игре предположительно около 500 процессорных состояний и дюжина макетов дескрипторов. Сначала рендерится оружие игрока, потом динамические объекты, и затем статические. Обратите внимание, что порядок не особо-то и важен, ведь благодаря препроходу глубины мы уже получили буфер глубины, и он может заранее исключать не соответствующие глубине пиксели.


Оружие игрока


Динамические объекты


Первый набор статических объектов


Второй набор статических объектов

У большинства движков AAA-игр графы шейдеров и особенности статических шейдеров позвляют разработчикам возможность креативно подходить к работе со всевозможными материалами и поверхностями, и каждый материал, каждая поверхность ведет к созданию своего собственного уникального шейдера. В результате мы сталкиваемся с невероятным многообразием перестановок шейдеров для всех возможных комбинаций особенностей движка. Однако id Tech сильно отличается от других AAA-проектов: он комбинирует почти все материалы и особенности во всего лишь несколько массивных мегашейдеров. Такой подход позволяет графическим процессам жестче объединять геометрию, что в свою очередь положительно сказывается на производительности. Позже мы еще это обсудим.

Несвязанные ресурсы


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



Динамическое слияние вызовов отрисовки


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

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

Отражения


Чаще всего для создания отражений экранного пространства вычислительный шейдер использует алгоритм raymarching. Алгоритм испускает из пикселя луч в мировое пространство в сторону отражения, которое зависит от неровности отражающей поверхности. Точно так же дело обстояло в Doom 2016, там как часть прямого прохода записывался небольшой G-буфер. Однако в Doom Eternal уже никакого G-буфера нет, и даже отражения экранного пространства вычисляются не в вычислительном шейдере по отдельности, а сразу в прямом шейдере. Интересно узнать, насколько такое отклонение в пиксельном шейдере влияет на производительность, поскольку создается ощущение, что ценой повышенной нагрузки на регистр разработчики пытались снизить количество целей рендера и как следствие сократить нагрузку на пропускную способность памяти.

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



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

Частицы


Симуляция


В Doom Eternal часть процессорной симуляции частиц ложится на плечи вычислительных шейдерв, поскольку у некоторых систем частиц есть зависимости от информации экранного пространства, например буфера глубины для симуляции столкновений. Тогда как другие системы частиц могут прогоняться в кадре сразу и вычисляться асинхронно, таким симуляциям предварительно необходимы данные препрохода глубины. Что характерно, в отличие от традиционной шейдерной симуляции частиц, здесь симуляция ведется через выполнение последовательности команд из хранящегося в вычислительном шейдере буфера. Каждый поток шейдера прогоняет все команды, среди которых может быть по несколько kill, emit или модификаций параметра частицы. Все это похоже на записанную в шейдере виртуальную машину. Я многое не понимаю в тонкостях работы такой симуляции, но основан подход на презентации Brandon Whitney The Destiny Particle Architecture на Siggraph 2017. Метод в презентации очень похож на описанный мною выше и используется во множестве других игр. Например, я уверен, что похожим образом в Unreal Engine 4 работает система симуляции частиц Niagara.

Освещение


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


Увеличенный фрагмент атласа освещения.

Небо и рассеяние


Теперь мы поговорим про объемное освещение. Его генерация состоит из четырех проходов и начинается с создания 3D LUT текстуры для атмосферы неба через raymarching сквозь само небо в сторону источника света.



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



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



В третьем проходе данные рассеяния для каждой клетки множатся в каждую последующую клетку в сторону обзора и пишутся в новую 3D текстуру.

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


До


После

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



Прозрачность


По аналогии с Doom 2016, прозрачность рендерится прямым проходом после непрозрачной геометрии при наличии данных о рассеянии света. Текстура сцены при этом теряет в разрешении (downsamples), а для имитации прозрачности на основе гладкости поверхности подбирается подходящий mip-уровень. Данные о рассеянии света помогают создать изнутри поверхности визуально хорошее рассеяние.

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


Для прозрачности теряют в разрешении только имеющие к ней отношение пиксели.

Пользовательский интерфейс


Обычно последним проходом в кадре становится пользовательский интерфейс. Как это обычно и происходит, интерфейс рендерится во вторичную с полным разрешением LDR (восьмибитную) цель рендера, и цвет предварительно умножается на альфа-канал. В ходе тональной компрессии интерфейс накладывается на HDR текстуру. Обычно заставить интерфейс работать с остальным HDR контентом кадра не так-то просто, но в Doom Eternal при тональной компресии интерфейс волшебным образом скалируется и выглядит естественно на фоне прочего 3D контента.



Постобработка


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

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



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

Затем тональная компрессия объединяет все эффекты. Один-единственный вычислительный шейдер делает следующее:

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

И наконец, сверху накладывается интерфейс.

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



Заключение


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

Rip and tear, until it is done.

Справочные материалы


Подробнее..

Перевод В чём главные проблемы Intel

29.01.2021 14:10:10 | Автор: admin


Оглядываясь назад, моя статья по поводу назначения нового исполнительного директора Intel в 2013 году оказалась чрезмерно оптимистичной. Одно название чего стоит: Возможность для Intel. В реальности вышло не так за эти годы у Intel ничего не получилось, никакими возможностями она не воспользовалась.

Откуда мы знаем, что не получилось? Во-первых, спустя восемь лет Intel опять назначает нового директора (Пэт Гелсингер), но не вместо того, о котором я писал (Брайан Кржанич), а вместо его преемника (Боб Свон). Очевидно, в то самое окно возможностей компания на самом деле не попала. И теперь уже встаёт вопрос выживания компании. И даже вопрос национальной безопасности Соединённых Штатов Америки.

Проблема 1: мобильные устройства


Вторая причина, по которой заголовок 2013 года был чрезмерно оптимистичным, заключается в том, что к тому моменту Intel уже попала в серьёзную беду. Вопреки своим заявлениям, компания слишком сосредоточилась на скорости CPU и слишком пренебрежительно отнеслась к энергопотреблению, поэтому не смогла сделать процессор для iPhone, и, несмотря на годы попыток, не смогла попасть на Android.

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

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

Проблема 2: успех на серверах


Intel захватила этот рынок не так давно. Изначально на нём доминировали интегрированные компании, такие как Sun, с соответствующими ценами, но благодаря взрыву продаж персональных компьютеров Intel быстро улучшала производительность и снижала цены CPU, особенно по отношению к производительности. Конечно, ПК не дотягивали до надёжности интегрированных серверов, но на рубеже веков Google поняла, что масштаб и сложность услуг делают невозможным создание действительно надёжного стека. Решением стали отказоустойчивые серверы с горячей заменой вышедших из строя компонентов. Это позволило строить дата-центры на относительно дешёвых процессорах x86.



В течение двух следующих десятилетий подход Google приняли все крупные ЦОДы, так что x86 стала архитектурой по умолчанию для серверов. Основную выгоду из этого извлекла Intel, поскольку именно она делала лучшие процессоры x86, особенно для серверных приложений. Это связано как с собственным дизайном Intel, так и с её великолепными фабриками. AMD иногда угрожала действующему лидеру, но только в ноутбуках низкого уровня, а вовсе не в дата-центрах.

Таким образом, Intel избежала судьбы Microsoft в постдесктопную эпоху: Microsoft пролетела не только мимо мобильных устройств, но и мимо серверов, которые работают под управлением Linux, а не Windows. Конечно, компания как может поддерживает Windows и на компьютерах (через Office), и на серверах (через Azure). Однако всё выходит наоборот: то, что недавно подпитывало рост компании, становится концом Windows, поскольку Office переходит в облако с работой на всех устройствах, а Azure переходит на Linux. В обоих случаях Microsoft пришлось признать, что их власть теперь не в контроле над API, а в обслуживании уже существующих клиентов в новом масштабе.

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

Большинство компаний сами не выпускают чипы. Они создают дизайн и отдают на завод. AMD, Nvidia, Qualcomm, MediaTek, Apple ни у кого нет собственных заводов. Безусловно, это имеет смысл: производство полупроводников, возможно, самая капиталоёмкая отрасль в мире, так что AMD, Qualcomm и другие хотят заниматься более прибыльными проектами с более высокой маржой.

Однако проектирование микросхем становится всё более стандартным. Почти все чипы основаны на архитектуре ARM. Оплатив лицензию, компании вроде Apple могут создавать собственные модификации и заказывать их производство. Дизайн немножко уникален, но в мобильных устройствах это не самое принципиальное. Здесь никогда не будет доминировать единый дизайн от одного разработчика, как Intel доминировала на десктопном рынке.

С другой стороны, именно производственные мощности становятся более дефицитными и, следовательно, более ценными. На самом деле в мире только четыре крупных производственных компании: Samsung, GlobalFoundries, Taiwan Semiconductor Manufacturing Company (TSMC) и Intel. Только четыре компании могут создавать чипы, которые сегодня установлены в каждом мобильном устройстве, а завтра будут установлены вообще везде.

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

Кстати, моя рекомендация не означает отказ от x86, я добавил в сноске:

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

На самом деле, бизнес x86 оказался слишком прибыльным, чтобы пойти на такой радикальный шаг. Это именно та проблема, которая ведёт к разрушению. Да, Intel избежала судьбы Microsoft, но при этом не испытала сильнейшей финансовой боли, которая необходима как стимул для такой кардинальной трансформации бизнеса (например, только после краха рынка памяти в 1984 году Энди Гроув в Intel решил полностью сосредоточиться на производстве процессоров).

Проблема 3: производство




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

Это угрожает Intel по нескольким фронтам:

  • Intel окончательно потеряла рынок маков, в том числе из-за выдающейся производительности нового чипа M1. Но важно отметить причины такой производительности это не только дизайн Apple, но и 5-нм техпроцесс TSMC.
  • Десктопные процессоры AMD теперь быстрее, чем у Intel, и чрезвычайно конкурентоспособны на серверах. Опять же, преимущество AMD отчасти связано с улучшением дизайна, но не менее важным является производство по 7-нм процессу TSMC.
  • Крупные облачные провайдеры всё больше инвестируют в разработку собственных чипов. Например, Amazon уже выпустила вторую версию процессора Graviton ARM, на котором будет работать таймлайн твиттера. Одно из преимуществ Graviton его архитектура, а другое ну, вы уже поняли производство компанией TSMC по тому же 7-нм техпроцессу (который конкурирует с наконец-то запущенным 10-нм техпроцессом Intel).

Короче говоря, Intel теряет долю на рынке, ей угрожает AMD на x86-серверах и облачные компании типа Amazon с собственными процессорами. И я даже не упомянул других специализированных решений, таких как приложения на GPU для машинного обучения, которые разрабатывает Nvidia и производит Samsung.

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

Проблема 4: TSMC


К сожалению, это ещё не самое худшее. На следующий день после назначения нового директора Intel компания TSMC объявила впечатляющие финансовые результаты и, что более важно, прогнозы капитальных инвестиций на 2021 год, от Bloomberg:

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

Это огромная сумма инвестиций, которая только упрочит лидерство TSMC.

Предполагаемый рост финансирования привёл к тому, что производители оборудования для производства микросхем хлынули из Нью-Йорка в Токио. Капитальные расходы TSMC от 25 до 28 миллиардов долларов в 2021 году гораздо выше прошлогодних 17,2 миллиардов. Около 80% вложений направят на передовые технологии производства CPU, то есть TSMC ожидает резкого роста бизнеса по производству передовых микросхем. Аналитики предполагают, что после серии внутренних технологических сбоев Intel передаст производство на аутсорсинг таким компаниям, как TSMC.

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

Проблема 4: геополитика


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

Международный статус Тайваня, как говорится, сложный. Собственно, как и отношения между Китаем и США. Всё это накладывается одно на другое и создаёт совершенно новые осложнения, делая ситуацию ещё более запутанной.

Ну а география, напротив, простая и понятная:



Как видите, Тайвань находится недалеко от китайского побережья. Рядом Южная Корея, родина Samsung, которая тоже производит чипы самого высокого класса. Соединённые Штаты по другую сторону Тихого океана. Есть передовые фабрики Intel в Орегоне, Нью-Мексико и Аризоне, но Intel производит чипы только для собственных интегрированных вариантов использования.

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

Если вы занимаетесь военным стратегическим планированием в США, это большая проблема. Ваша задача не предсказывать войны, а планировать действия, которые могут произойти при неудачном стечении обстоятельств, то есть если вдруг случится война между США и Китаем. И в этом планировании серьёзной проблемой является размещение заводов TSMC и Samsung в пределах лёгкой досягаемости китайских ракет.

Буквально несколько дней назад компания TSMC официально объявила о строительстве 5-нм завода в Аризоне. Да, сегодня это передовые технологии, но завод откроется только в 2024 году. Тем не менее это почти наверняка будет самая передовая фабрика в США, которая выполняет сторонние заказы. Надеюсь, к моменту открытия Intel превзойдёт её возможности.

Однако заметим, что интересы Intel и США не совпадают. Первая заботится о платформе x86, а США нужны передовые фабрики общего назначения на её территории. Иными словами, у Intel всегда в приоритете дизайн, а у США производство.

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

Решение 1: раздел


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

Главное, что нужно понять о микроэлектронике что маржа в дизайне гораздо выше. Например, у Nvidia валовая маржа 60-65%, в то время как у TSMC, которая производит для неё микросхемы, ближе к 50%. Как я уже отмечал выше, маржа Intel традиционно ближе к Nvidia благодаря интеграции дизайна и производства, поэтому собственные чипы всегда будут приоритетом для её производственного подразделения. От этого пострадает обслуживание потенциальных клиентов и гибкость в выполнении сторонних заказов, а также эффективность привлечения лучших поставщиков (что ещё больше снизит маржу). Здесь ещё и вопрос доверия: готовы ли конкуренты делиться своими разработками, особенно если Intel уделяет приоритетное внимание собственному дизайну?

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

Решение 2: субсидии


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

Вот почему федеральная программа субсидирования должна действовать как гарантия покупки. Государство закупает определённое количество произведённых в США 5-нм процессоров по такой-то цене; определённое количество произведённых в США 3-нм процессоров по такой-то цене; определённое количество 2-нм процессоров и так далее. Это не только установит цели для производства Intel, но и подтолкнёт другие компании зайти на этот рынок. Возможно, вернутся в игру глобальные производственные компании или TSMC построит больше фабрик в США, а возможно, в нашем мире почти свободного капитала наконец появится стартап, готовый совершить революцию.

Безусловно, мы чрезмерно упрощаем проблему. В производстве электроники очень много факторов. Например, упаковка интегральных схем (сборка кристалла в корпус) давным-давно переехала за границу в погоне за снижением затрат и теперь полностью автоматизирована. Вернуть её проще. Однако крайне важно понять, что восстановление конкурентоспособности, а тем более лидерства США, займёт много лет. Определённую роль играет федеральное правительство, но и Intel должна принять реальность, что её интегрированная модель больше не работает.
Подробнее..

Категории

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

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