Wine — это свободное программное обеспечение для запуска Windows-приложений на нескольких POSIX-совместимых операционных системах, включая Linux, macOS и BSD.
Если вы любите Linux, то наверняка когда-то запускали Wine. Возможно, для какой-то «важной» программы Windows, у которой нет аналога под Линуксом, или поиграться. Забавный факт: даже Steam Deck от Valve запускает игры через оболочку на основе Wine (она называется Proton).
За последний год я намучился с отладчиком, который позволяет одновременно дебажить и Wine, и Windows-приложение в нём. Разобраться во кишочках Wine оказалось очень интересно! Я-то раньше много им пользовался, но никогда не понимал механику целиком. Можно взять файл Windows — и просто запустить его в Linux без каких-либо изменений. Если вы хотите знать, как это сделано, добро пожаловать под кат.
Дисклеймер. В статье реальность сильно упрощается, а многие детали игнорируются. Текст даёт общее представление, как работает Wine.
© «Время приключений» (1 сезон, 18 серия) — прим. пер.
Wine — не эмулятор!
Прежде чем разбираться в работе Wine, нужно сказать, чем он НЕ является. Вообще, W.I.N.E. — это рекурсивный акроним, который расшифровывается как "Wine Is Not an Emulator". Почему? Потому что есть куча отличных эмуляторов и для старых архитектур, и для современных консолей, а Wine принципиально реализован по-другому. Давайте вкратце рассмотрим, как вообще работают эмуляторы.
Представьте простую игровую приставку, которая понимает две инструкции:
-
push <value>
— пушит заданное значение в стек
-
setpxl
— достаёт три значения из стека и рисует пиксель с цветомarg1
в точке(arg2, arg3)
(вполне достаточно для визуализации классных демок, верно?)
> dump-instructions game.rom
...
# рисуем красную точку по координатам (10,10)
push 10
push 10
push 0xFF0000
setpxl
# рисуем зелёную точку по координатам (15,15)
push 15
push 15
push 0x00FF00
setpxl
Бинарный файл игры (или картридж ROM) представляет собой последовательность таких инструкций, которые аппаратное обеспечение может загрузить в память и выполнить. Нативное железо выполняет их в натуральном режиме, но как запустить старый картридж на современном ноуте? Для этого делаем эмулятор — программу, которая загружает ROM из картриджа в оперативную память и выполняет его инструкции. Это интерпретатор или виртуальная машина, если хотите. Реализация эмулятора для нашей приставки с двумя инструкциями будет довольно простой:
enum Opcode {
Push(i32),
SetPixel,
};
let program: Vec<Opcode> = read_program("game.rom");
let mut window = create_new_window(160, 144); // Виртуальный дисплей 160x144 пикселей
let mut stack = Vec::new(); // Стек для передачи аргументов
for opcode in program {
match opcode {
Opcode::Push(value) => {
stack.push(value);
}
Opcode::SetPixel => {
let color = stack.pop();
let x = stack.pop();
let y = stack.pop();
window.set_pixel(x, y, color);
}
}
}
Настоящие эмуляторы намного сложнее, но основная идея та же: поддерживать некоторый контекст (память, регистры и т.д.), обрабатывать ввод (клавиатура/мышь) и вывод (например, рисование в каком-то окне), разбирать входные данные (ROM) и выполнять инструкции одну за другой.
Разработчики Wine могли пойти по этому пути. Но есть две причины, почему они так не поступили. Во-первых, эмуляторы и виртуальные машины тормозные по своей сути — там огромный оверхед на программное выполнение каждой инструкции. Это нормально для старого железа, но не для современных программ (тем более видеоигр, которые требовательны к производительности). Во-вторых, в этом нет необходимости! Linux/macOS вполне способны запускать двоичные файлы Windows нативно, их нужно только немного подтолкнуть…
Давайте скомпилируем простую программу для Linux и Windows и сравним результат:
int foo(int x) {
return x * x;
}
int main(int argc) {
int code = foo(argc);
return code;
}
Слева — Linux, справа — Windows
Результаты заметно отличаются, но набор инструкций фактически один и тот же:
push
, pop
, mov
, add
, sub
, imul
, ret
. Если бы у нас был «эмулятор», который понимает эти инструкции, то смог бы выполнить обе программы. И такой «эмулятор» существует — это наш CPU.
Как Linux запускает бинарники
Прежде чем запускать чужеродный двоичный файл, давайте разберёмся, как запускается под Linux родной бинарник.
❯ cat app.cc
#include <stdio.h>
int main() {
printf("Hello!\n");
return 0;
}
❯ clang app.cc -o app
❯ ./app
Hello! # работает!
Довольно просто, но давайте копнём глубже. Если сделать
.app
?❯ ldd app
linux-vdso.so.1 (0x00007ffddc586000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f743fcdc000)
/lib64/ld-linux-x86-64.so.2 (0x00007f743fed3000)
❯ readelf -l app
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1050
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
...
Самое главное, что
.app
— это динамически исполняемый файл. Он зависит от некоторых динамических библиотек и требует их присутствия в рантайме. Иначе не запустится. Другой интересный момент — запрос интерпретатора (requesting program interpreter
в последней строке листинга). Какой ещё интерпретатор? Я думал, что C++ — компилируемый язык, в отличие от Python…В данном контексте интерпретатор — это «динамический загрузчик». Специальный инструмент, которая запускает исходную программу: разрешает и загружает её зависимости, а затем передаёт ей управление.
❯ ./app
Hello! # Работает!
❯ /lib64/ld-linux-x86-64.so.2 ./app
Hello! # Тоже работает!
# Домашнее задание: запустите это и попробуйте понять смысл выдачи.
❯ LD_DEBUG=all /lib64/ld-linux-x86-64.so.2 ./app
При запуске исполняемого файла ядро Linux определяет, что файл динамический и требует загрузчика. Затем оно запускает загрузчик, который выполняет всю работу. Это можно проверить, если запустить программу под отладчиком:
❯ lldb ./app
(lldb) target create "./app"
Current executable set to '/home/werat/src/cpp/app' (x86_64).
(lldb) process launch --stop-at-entry
Process 351228 stopped
* thread #1, name = 'app', stop reason = signal SIGSTOP
frame #0: 0x00007ffff7fcd050 ld-2.33.so`_start
ld-2.33.so`_start:
0x7ffff7fcd050 <+0>: movq %rsp, %rdi
0x7ffff7fcd053 <+3>: callq 0x7ffff7fcdd70 ; _dl_start at rtld.c:503:1
ld-2.33.so`_dl_start_user:
0x7ffff7fcd058 <+0>: movq %rax, %r12
0x7ffff7fcd05b <+3>: movl 0x2ec57(%rip), %eax ; _dl_skip_args
Process 351228 launched: '/home/werat/src/cpp/app' (x86_64)
Мы видим, что первая выполненная инструкция находится в библиотеке
ld-2.33.so
, а не в бинарнике .app
.Подводя итог, запуска динамически связанного исполняемого файла в Linux выглядит примерно так:
- Ядро загружает образ (≈ двоичный файл) и видит, что это динамический исполняемый файл
- Ядро загружает динамический загрузчик (
ld.so
) и передаёт ему управление
- Динамический загрузчик разрешает зависимости и загружает их
- Динамический загрузчик возвращает управление исходному двоичному файлу
- Оригинальный двоичный файл начинает выполнение в
_start()
и в конечном итоге доходит доmain()
Понятно, почему исполняемый файл Windows не запускается в Linux — у него другой формат. Ядро просто не знает, что с ним делать:
❯ ./HalfLife4.exe
-bash: HalfLife4.exe: cannot execute binary file: Exec format error
Однако если пропустить шаги с первого по четвёртый и каким-то образом перескочить на пятый, то теоретически должно сработать, верно? Ведь с точки зрения операционной системы что значит «запустить» бинарный файл?
В каждом исполняемом файле есть раздел
.text
со списком сериализованных инструкций CPU:❯ objdump -drS app
app: file format elf64-x86-64
...
Disassembly of section .text:
0000000000001050 <_start>:
1050: 31 ed xor %ebp,%ebp
1052: 49 89 d1 mov %rdx,%r9
1055: 5e pop %rsi
1056: 48 89 e2 mov %rsp,%rdx
1059: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
105d: 50 push %rax
105e: 54 push %rsp
105f: 4c 8d 05 6a 01 00 00 lea 0x16a(%rip),%r8 # 11d0 <__libc_csu_fini>
1066: 48 8d 0d 03 01 00 00 lea 0x103(%rip),%rcx # 1170 <__libc_csu_init>
106d: 48 8d 3d cc 00 00 00 lea 0xcc(%rip),%rdi # 1140 <main>
1074: ff 15 4e 2f 00 00 call *0x2f4e(%rip) # 3fc8 <__libc_start_main@GLIBC_2.2.5>
107a: f4 hlt
107b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
...
Чтобы «запустить» бинарный файл, ОС загружает его в память (в частности, раздел
.text
), устанавливает указатель текущей инструкции на адрес, где находится код, и всё — исполняемый файл типа «запущен». Как сделать это для исполняемых файлов Windows?Легко! Просто возьмём код из исполняемого файла Windows, загрузим в память, направим
%rip
в нужное место — и CPU с радостью выполнит этот код! Если архитектура процессора одинаковая, то процессору вообще без разницы, откуда выполнять ассемблерные инструкции.Hello, Wine!
По сути, Wine — это «динамический загрузчик» для исполняемых файлов Windows. Это родной двоичный файл Linux, поэтому может нормально запускаться, и он знает, как работать с EXE и DLL. То есть своего рода эквивалент
ld-linux-x86-64.so.2
:# запуск бинарника ELF
❯ /lib64/ld-linux-x86-64.so.2 ./app
# запуск бинарника PE
❯ wine64 HalfLife4.exe
Здесь
wine64
загружает исполняемый файл Windows в память, анализирует его, выясняет зависимости, определяет, где находится исполняемый код (т. е. раздел .text
), и переходит в этот код.Примечание. В действительности он переходит к чему-то вродеntdll.dll!RtlUserThreadStart()
, это точка входа в «пространство пользователя» в мире Windows. Потом вmainCRTStartup()
(эквивалент_start
), и в самmain()
.
На данный момент наша Linux-система выполняет код, изначально скомпилированный для Windows, и всё вроде бы работает. За исключением системных вызовов.
Системные вызовы
Системные вызовы (syscall) — вот где основные сложности. Это вызовы к функциям, которая реализованы не в бинарнике или динамических библиотеках, а в родной ОС. Набор системных вызовов представляет системный API операционной системы. В нашем случае это Windows API.
Примеры системных вызовов в Linux:
read
, write
, open
, brk
, getpid
Примеры в Windows:
NtReadFile
, NtCreateProcess
, NtCreateMutant
????Системные вызовы не являются обычными вызовами функций в коде. Открытие файла, например, должно выполняться самим ядром, поскольку именно оно следит за файловыми дескрипторами. Поэтому приложению нужен способ как бы «прервать своё выполнение» и передать управление ядру (эта операция обычно называется переключением контекста).
Набор системных функций и способы их вызова в каждой ОС разные. Например, в Linux для вызова функции
read()
наш бинарник записывает в регистр %rdi
дескриптор файла, в регистр %rsi
— указатель буфера, а в %rdx
— количество байт для чтения. Однако в ядре Windows нет функции read()
! Ни один из аргументов не имеет там смысла. Бинарник Windows использует свой способ выполнения системных вызовов, который не сработает в Linux. Не будем здесь углубляться детали системных вызовов, например, вот отличная статья о реализации в Linux.Скомпилируем ещё одну небольшую программу и сравним сгенерированный код в Linux и Windows:
#include <stdio.h>
int main() {
printf("Hello!\n");
return 0;
}
Слева — Linux, справа — Windows
На этот раз мы вызываем функцию из стандартной библиотеки, которая в конечном итоге выполняет системный вызов. На скриншоте выше версия Linux вызывает
puts
, а версия Windows — printf
. Эти функции из стандартных библиотек (libc.so
в Linux, ucrtbase.dll
в Windows) для упрощения взаимодействия с ядром. Под Linux сейчас частенько собирают статически связанные бинарники, не зависимые от динамических библиотек. В этом случае реализация puts
встроена в двоичный файл, так что libc.so
не задействуется в рантайме.Под Windows до недавнего времени «системные вызовы bcgjkmpjdfkb только вредоносные программы»[нет источника] (вероятно, это шутка автора — прим. пер.). Обычные приложения всегда зависят от
kernel32.dll/kernelbase.dll/ntdll.dll
, где скрывается низкоуровневая магия тайного общения с ядром. Приложение просто вызывает функцию, а библиотеки заботятся об остальном:источник
В этом месте вы наверное поняли, что будет дальше. ????
Трансляция системных вызовов в рантайме
А что, если «перехватывать» системные вызовы во время выполнения программы? Например, когда приложение вызывает
NtWriteFile()
, мы берём управление на себя, вызываем write()
, а потом возвращаем результат в ожидаемом формате — и возвращаем управление. Должно сработать. Быстрое решение в лоб для примера выше:// HelloWorld.exe
lea rcx, OFFSET FLAT:`string'
call printf
↓↓
// «Фальшивый» ucrtbase.dll
mov edi, rcx // Преобразование аргументов в Linux ABI
call puts@PLT // Вызов реальной реализации Linux
↓↓
// Real libc.so
mov rdi, <stdout> // запись в STDOUT
mov rsi, edi // указатель на "Hello"
mov rdx, 5 // сколько символов писать
syscall
По идее, можно сделать собственную версию
ucrtbase.dll
со специальной реализацией printf
. Вместо обращения к ядру Windows она будет следовать формату интерфейсов Linux ABI и вызывать функцию write
из библиотеки libc.so
. Однако на практике мы не можем изменять код этой библиотеки по ряду причин — это муторно и сложно, нарушает DRM, приложение может статически ссылаться на ucrtbase.dll
и т. д.Поэтому вместо редактирования бинарника мы внедримся в промежуток между исполняемым файлом и ядром, а именно в
ntdll.dll
. Это «ворота» в ядро, и Wine действительно предоставляет собственную реализацию. В последних версиях Wine решение состоит из двух частей: ntdll.dll
(библиотека PE) и ntdll.so
(библиотека ELF). Первая часть — это тоненькая прокладка, которая просто перенаправляет вызовы в ELF-аналог. А уже он содержит специальную функцию __wine_syscall_dispatcher
, которая выполняет магию преобразования текущего стека из Windows в Linux и обратно.Поэтому в Wine системный вызов выглядит следующим образом:
Диспетчер системных вызовов — это мост между мирами Windows и Linux. Он заботится о соглашениях и стандартах для системных вызовов: выделяет пространство стека, перемещает регистры и т. д. Когда выполнение переходит к библиотеке Linux (
ntdll.so
), мы можем свободно использовать любые нормальные интерфейсы Linux (например, libc
или syscall
), реально читать/записывать файлы, занимать/отпускать мьютексы и так далее.И это всё?
Звучит почти слишком просто. Но так и есть. Во-первых, под Windows много разных API. Они плохо документированы и имеют известные (и неизвестные, ха-ха) ошибки, которые следует воспроизвести в точности. (Вспомните, как при разработке Windows 95 туда скопировали утечку памяти из SimCity, чтобы популярная игра не крашилась в новой ОС. Возможно, такие специфические вещи приходится воспроизводить под Linux для корректной работы конкретных программ — прим. пер.). Большая часть исходного кода Wine — это реализация различных Windows DLL.
Во-вторых, системные вызовы можно выполнять по-разному. Технически ничто не мешает Windows-приложению выполнить прямой системный вызов через
syscall
, и в идеале это тоже должно работать (как мы уже говорили, Windows-игры делают всякие безумные вещи). В ядре Linux специальный механизм для обработки таких ситуаций, который, конечно, добавляет сложности.В-третьих, весь этот бардак 32 vs 64 бит. Есть много старых 32-битных игр, которые никогда не перепишут на 64 бита. В Wine есть поддержка обеих платформ. И это тоже плюс к общей сложности.
В-четвертых, мы даже не упомянули
wine-server
— отдельный процесс Wine, который поддерживает «состояние» ядра (открытые дескрипторы файлов, мьютексы и т. д.).И последнее… о, так вы хотите запустить игру? А не просто hello world? Ну так это совсем другое дело! Тогда нужно разобраться с DirectX, со звуком (привет, PulseAudio, старый друг), устройствами ввода (геймпады, джойстики) и т. д. Куча работы!
Wine разрабатывался в течение многих лет и прошёл долгий путь. Сегодня вы без проблем запускаете под Linux самые последние игры, такие как Cyberpunk 2077 или Elden Ring. Чёрт возьми, иногда производительность Wine даже выше, чем у Windows! В какое замечательное время мы живём…
P. S. На всякий случай повторим дисклеймер: статья даёт только базовое представление о работе Wine. Многие детали упрощены или опущены. Так что не судите очень строго, пожалуйста.
Комментарии (33)
Mingun
17.10.2022 17:32+2Вот бы еще кто-то написал бы такую же понятную статью, чем все-таки syscall отличается от обычного call. И там и там стек (надеюсь). И там и там регистры. И там и там можно вернуть результат. В чем разница? Пока везде из объяснений вижу (тут читать с придыханием) "ну это же syscall!"
screwer
17.10.2022 17:47+19Если очень-очень кратко и упрощённо - различные уровни исполнения, которые отслеживаются аппаратно. Изначально пользовательский уровень не имел доступа к памяти уровня супервизора (ядро). Потом и в обратную сторону добавили подобное ограничение. Страницы памяти супервизора помечены специальным битом. В режиме супервизора (ядра) нам доступно больше инструкций, главным образом системных, управляющих трансляцией страниц памяти,состоянием процессора(-ов), управление кешами т.д.. Итого - супервизор изолирован от пользовательского кода, и имеет над ним полный контроль. Учитывает выделенные ресурсы, и способен уничтожить проблемную пользовательскую задачу, корректно освободить ресурсы, оставляя всю систему в рабочем и стабильном состоянии.
Также ядро отвечает за обработку прерываний. Как аппаратных, обеспечивая работу устройств. Так и программных. Этот ваш syscall - в прошлом просто программное прерывание (2е или 80, для win/lin x86) которое обрабатывается ядром как системный вызов. Потом сделали специальную инструкцию, но суть осталась прежней.
boris768
17.10.2022 17:49+3syscall - это инструкция процессора, вызывающая переход исполнения кода в режим ядра, то есть связующая часть между UserMode и KernelMode, у нее несколько иное поведение, нежели у обычной инструкции call
netch80
18.10.2022 11:17+2Syscall как системный вызов между уровнями привилегий может быть сделан любой инструкцией. В контексте разговора по Wine важно именно это.
То, что на x86-64 (x64 в терминах Windows) сейчас для этого используется syscall, это уже частности.
В x86-32 на Windows использовалось несколько разных прерываний и у каждого задавался номер функции.
vda19999
17.10.2022 20:03+3Вообще-то, syscall - это новая инструкция. До её появления системные вызовы выполнялись с помощью инструкции int с параметром 0x80 (это число для Линукса, для винды какое-то другое, должно быть). Это означало, что процессор инициирует прерывание с номером 0х80, а далее передаёт управление коду операционной системы, который обрабатывает это прерывание, вызывая нужный системный вызов.
У этой инструкции int очень хитрое описание того, что она делает, но если вкратце - сохраняет часть информации, типо сегментных регистров и меняет уровень привелегий.
syscall и sysret появились позже и делают нечто похожее, при условии что ОС настроила их работы.
Mingun
17.10.2022 22:08А есть где-то почитать историю того, что именно настраивается, что значит настроить аппаратную инструкцию (новые же транзисторы на чипе не вырастишь), и зачем это было нужно, и как к этому пришли?
vda19999
17.10.2022 22:42+8Видимо, использование int 0x80 для системных вызовов было неоптимально - инструкция делает что-то весьма хитрое и делает работу, которая, возможно, и не нужна. В итоге AMD и Intel решили в 64-ёх битной архитектуре сделать специальные инструкции для этого.
Однако, операционная система может не поддерживать вызов системных вызовов с помощью инструкции syscall. Поэтому процессор позволит выполнить её только если операционная система настроит её, а именно:
" Процессор x86 даст выполнить инструкцию
syscall
только если установлен битIA32_EFER.SCE
в регистреIA32_EFER
.IA32_EFER
- это model specific register (MSR) с номером0xC0000080
. БитIA32_EFER.SCE
- нулевой бит этого регистра. Чтение и запись таких регистров делается специальными инструкциямиrdmsr
/wrmsr
" и "Далее, помимо того чтобы разрешить вызыватьsyscall
, нужно указать точку входа в ядре ОС. Точка входа - это адрес инструкции, на которую будет совершён переход при выполненииsyscall
. Адрес должен быть записан в MSRIA32_LSTAR
(номер0xC0000082
)."Взято отсюда: https://gitlab.com/slon/shad-os/-/tree/master/gate64
netch80
18.10.2022 11:14+11> и зачем это было нужно, и как к этому пришли?
Очень долго программные прерывания делали ровно тем же механизмом, который использовался для аппаратных прерываний. Аппаратное прерывание должно отрабатываться так, что если оно происходит, то исполняющаяся при этом программа (пользовательская — почти всегда, ядро — обычно) не замечает ничего, кроме задержки во времени. Для него оправданы сложности с полным сохранением контекста. На x86 это делается через шлюз привилегий, при этом процессор сохраняет старые значения всех регистров разом и загружает новые, это долго и нудно.
Но когда решили, что это стало слишком тормозить пользовательскую активность, кто-то вспомнил, что если программа вызывает действие явно и в предназначенный ею момент, то не обязательно сохранять всё и идти дорогим путём. Вот тогда и появились, примерно одновременно, машинные команды sysenter в варианте Intel и syscall — в AMD (в 64 битах победила, соответственно, эта).
У этих двух команд есть два важных отличия. Первое — они позволяют только переходить из минимально привилегированного пользовательского режима (3-е кольцо в терминах x86) в супервизора (0-е кольцо). (Для сравнения: ARM, RISC-V — там номера у них соответственно 0 и 1, перевёрнуто направление и нет так и не заработавших 1 и 2 в x86.) Второе — они, в отличие от int N, явно перезаписывают некоторые регистры вместо использования стека, как в старых прерываниях. За счёт этого, как выше сказано, резко удешевляется переключение, но программа должна быть готова, что в каком-то регистре изменится значение — а она готова, раз выполняет sysenter или syscall.
> что значит настроить аппаратную инструкцию (новые же транзисторы на чипе не вырастишь)
Любой обработчик прерывания надо настроить, задав ему адрес перехода (как минимум; другие параметры не к этой теме). Для старого стиля прерываний это таблица прерываний в памяти. Для обсуждаемого тут это служебные регистры процессора (Intel их зовёт model-specific registers, MSR, что некорректно, но у них уже устоялось). Настройка — запись в такой регистр адреса перехода в режиме ядра. После этого команда SYSCALL сохраняет адрес возврата в RCX, флаги в R11, переключает режим в супервизора (ring 0) во флагах, и перезаписывает RIP из служебного регистра (IA32_LSTAR). (Там ещё пляски с CS и SS касательно длины адреса, пропустим.) SYSRET делает обратное. По сравнению с переключением через шлюз привилегий, как в старом стиле, это удешевление, наверно, раз в сто-двести. Работа уже на уровне кода ядра с сохранением остальных регистров, конечно, смягчает эти сто раз, но всё равно время переключения сокращается ну очень заметно.
DjPhoeniX
18.10.2022 09:10+4Вообще про потроха всего системного лучше всего читать на osdev, в частности https://wiki.osdev.org/System_Calls
koil
17.10.2022 23:39+16"Поэтому приложению нужен способ как бы «прервать своё выполнение» и передать управление ядру (эта операция обычно называется переключением контекста). "
Нет, эта операция не называется "переключением контекста". Эта операция так и называется, переход из пользовательского режима в режим ядра и в общем случае вообще никак не связана с переключением контекста.
13werwolf13
18.10.2022 08:46+9Огромное спасибо за статью. Базовое понимание как работает wine я давно приобрёл, но вот никогда не умел его объяснить ньюфагам. Теперь буду просто делиться ссылкой!!
Чёрт возьми, иногда производительность Wine даже выше, чем у Windows!
некоторое время назад забавлялся сравнивая производительность пары игр, приведу примеры:
1) WoW, под wine производительность была заметно лучше чем под windows 10
2) ARK, под wine производительность хуже чем под win (но ark в целом то ещё тормозилово)
3) valheim, нативный под linux уделывает wine под linux и нативный под винду
4) snowrunner, wine версия уделывает нативную под винду
5) DarkVoid, wine версия работает шикарно, нативная под виндой вызывает bsod и на win10 и на win7все игры (кроме wow поставлены из steam). тесты проводились на одном и том же PC на чистой свежеустановленной системе.
mikleh
18.10.2022 12:39Я так понимаю, это связано с тем, что под wine, да и в нативных сборках под linux, картинка отрисовывается далеко не в полном качестве, так как части функций DirectX просто нет. Естественно, заглушки выполняются быстрее чем те алгоритмы, которые винда честно выполняет.
13werwolf13
18.10.2022 12:54+2Визуально отличий нету. Что там на самом деле я не могу сказать, не закапывался так глубоко.
perfect_genius
18.10.2022 13:12Т.е. в Стиме продолжает продаваться игра, приводящая к BSOD?
13werwolf13
18.10.2022 14:02не знаю продаётся ли она сейчас. мне она доступна так как была куплена очень давно. я её прошёл пиратской так как на момент покупки она вызывала BSOD системы. тогда я грешил на захламлённую винду, но спустя много лет опыт показал что на чистой системе и совсем другом железе ситуация не меняется.
UPD: проверил, если разлогиниться и пройтись поиском игры нет, видимо она доступна только купившим её ранее.
perfect_genius
18.10.2022 18:32+1Оказывается, действительно снята с продажи, но в комментариях не нашёл про BSOD. Это происходит прямо на запуске?
Видимо, проблема из-за SecuROM.13werwolf13
18.10.2022 18:46+1Да, после нажатия на кнопку "играть" в Steam появляется окошко с какими то стимовскими препендами как для каждой игры, посде чего оно закрывается, на долю секунды мигает полноэкранная вьюха игры и сразу BSOD. В причинах не разбирался так как не интересно, единственное что проверил две версии винды, и семёрку и десятку. Так как мои знания о работе ОС мелкософта и софта под них практически равны нулю копаться дальше не было желания.
billyevans
19.10.2022 03:06+1У меня очень предсказуемо gta V делала BSOD и сейчас red dead redemption 2 примерно после 30-60м игры тоже. Кажется это появилось после обновления на win11.
perfect_genius
19.10.2022 13:40+1Абсолютно дико такое встречать в современное время.
billyevans
20.10.2022 03:11Особенно дико это выглядит для меня. Последний раз я комп на винде использовал в 2003м году, тут решил купить чисто игровой, там вообще ничего кроме Стима и battle.net ну и игр из них не установлено. Я и дрова всякие обновлял и температуру пытался мерить, в общем странная фигня, но мой опыт использования PC 20 лет назад такой же как и сейчас с синим экраном ;-) Сейчас даже более консистентно.
saipr
18.10.2022 10:52С удовольствием прочитал. Wine стоит аж с 1998 года, в основном для word и excel.
boris768
Кстати да, а может ли вообще wine обработать код с прямым syscall или есть ли какой-то способ (исключая наверное установку гипервизора)? Я бы не сказал, что такие программы встречаются в обычных условиях, но к примеру некоторые обфускаторы (как vmprotect, но он так не делает) могут заменять вызовы условного OpenProcess на NtOpenProcess через syscall (иначе говоря воспроизводя точный код из ntdll), это позволяет обойти хуки на winapi вызовы с целью анализа.
screwer
Если есть поддержка в ядре - не вижу никаких препятствий. WSL-1 работает именно так. В дяре windows реализовали поддержку системных вызовов Linux. И int/syscall выполняются как линуксовые.
vda19999
А она есть?
mapron
Да, и насколько я помню даже есть API для «легковесных» процессов (т.к. родные процессы Windows «тяжелее» своих аналогов из Unix). Про это даже статейка была несколько лет назад.
Я правда сам не ковырял но чет подозреваю что это документировано мягко говоря не очень. Т.к. эти все штуки не подразумеваются для использования Win32 разработчиками)
johnfound
Да, можно. Работает вполне нормально. Ведь, процессор выполнит инструкции будь они syscall или int 80h.
Я это использую в своих проектов.
Конечно надо проверять работает программа в Windows или в Linux/Wine а то в Windows просто упадёт.
netch80
> Кстати да, а может ли вообще wine обработать код с прямым syscall
И не может, и не имеет смысла. Я вижу основную проблему в том, что в настоящей Windows номера системных вызовов не фиксированы, а раздаются в каждой версии по-своему, и версия ntdll.dll должна быть точно согласована с версией ядра, иначе ничего работать не будет. Таблица для справки. Видно, что номера иногда между версиями прыгают непредсказуемо.
Если бы программа хотела работать под одной конкретной версией Windows, она могла бы зафиксировать эти номера, но кому это нужно? Поэтому всё кроме ну очень хакерского делают все вызовы через ntdll.dll через имена, где API уже предсказуемо.
UPD: А ещё не обязательно один вызов ntdll.dll отображается в один сисколл. Может быть несколько вызовов, может быть локальная обработка… всё в их руках. Тут и есть граница стабильного userland API.
> могут заменять вызовы условного OpenProcess на NtOpenProcess через syscall
Чтобы работать, условно говоря, на винде 1803, но не 1809… такой софт точно не нужен под Wine.
boris768
Вполне себе существуют парсеры ntdll, способные вытаскивать номера вызовов SSDT, пример на шарпе, вопрос лишь чисто о возможности обработки подобного.
И да, в принципе это возможно (но wine не делает) с помощью обработки SEH исключений, при таком вызове нужно лишь перебросить исполнение на вайновский ntdll в нужную функцию и оттуда продолжить исполнение.
netch80
> Вполне себе существуют парсеры ntdll, способные вытаскивать номера вызовов SSDT, пример на шарпе, вопрос лишь чисто о возможности обработки подобного.
Тут тогда второй вопрос возникает (надо было, наверно, написать сразу) — ntdll.dll не обязана всё заворачивать в сисколлы ядра. Она вполне может сделать какую-то обработку сама — и иногда это делает. Может транслировать в два сисколла. И так далее. Граница стабильного userland API это именно вход в ntdll.dll. И обрабатывать уже весь код — это тьюринг-полная задача.
Я там обновил с примечанием, пока было ещё две минуты:)
> И да, в принципе это возможно (но wine не делает) с помощью обработки SEH исключений,
Я попрошу подробностей, но это всё равно не покрывает всех случаев.
boris768
Пример можно найти здесь, это эмулятор kernel mode для запуска драйверов в целях анализа (очень специфичных, как Вы можете заметить), при этом применяется SEH для обработки вызовов ntoskrnl, чтения/записи памяти. Аналогично, как я предполагаю, можно обработать ситуации и в wine, но я с Вами согласен - задача слишком широкая для таких узких случаев. Благодарю за дискуссию.
lookies
Есть возможность с версии ядра 5.11, добавлено специально для wine
https://www.kernel.org/doc/html/v5.15/admin-guide/syscall-user-dispatch.html
Если кратко, на поток включается prctl(PR_SET_SYSCALL_USER_DISPATCH, PR_SYS_DISPATCH_ON...), ядро начинает слать signal, когда происходит системный вызов в этом потоке, вместо вызова реализации системного вызова. В обработчике сигнала можно считать/обновить состояние регистров, как и при практически любом другом сигнале, и таком образом имитировать поведение другого ядра. Из минусов - избыточное количество переключений контекстов: если нативно выходит такая схема user -> kernel -> user, то с syscall user dispatch получается user -> kernel -> user -> kernel -> user. Из плюсов - машинный код не требует изменений, любой, даже самый хитрый, код будет работать.
Альтернативой будет (это можно сделать и на старых версиях ядра), патч блоков машинного кода, в которых есть syscall/int инструкции, что не всегда просто/возможно реализовать, но работать будет скорей всего быстрее чем в оригинале, т.к. переключений контекстов не будет вообще.