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 выглядит примерно так:

  1. Ядро загружает образ (≈ двоичный файл) и видит, что это динамический исполняемый файл
  2. Ядро загружает динамический загрузчик (ld.so) и передаёт ему управление
  3. Динамический загрузчик разрешает зависимости и загружает их
  4. Динамический загрузчик возвращает управление исходному двоичному файлу
  5. Оригинальный двоичный файл начинает выполнение в _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)


  1. boris768
    17.10.2022 17:25
    +1

    Кстати да, а может ли вообще wine обработать код с прямым syscall или есть ли какой-то способ (исключая наверное установку гипервизора)? Я бы не сказал, что такие программы встречаются в обычных условиях, но к примеру некоторые обфускаторы (как vmprotect, но он так не делает) могут заменять вызовы условного OpenProcess на NtOpenProcess через syscall (иначе говоря воспроизводя точный код из ntdll), это позволяет обойти хуки на winapi вызовы с целью анализа.


    1. screwer
      17.10.2022 17:55
      +2

      Если есть поддержка в ядре - не вижу никаких препятствий. WSL-1 работает именно так. В дяре windows реализовали поддержку системных вызовов Linux. И int/syscall выполняются как линуксовые.


      1. vda19999
        17.10.2022 20:03
        +1

        А она есть?


        1. mapron
          18.10.2022 01:48

          Да, и насколько я помню даже есть API для «легковесных» процессов (т.к. родные процессы Windows «тяжелее» своих аналогов из Unix). Про это даже статейка была несколько лет назад.

          Я правда сам не ковырял но чет подозреваю что это документировано мягко говоря не очень. Т.к. эти все штуки не подразумеваются для использования Win32 разработчиками)


    1. johnfound
      18.10.2022 04:48
      -4

      Да, можно. Работает вполне нормально. Ведь, процессор выполнит инструкции будь они syscall или int 80h.
      Я это использую в своих проектов.
      Конечно надо проверять работает программа в Windows или в Linux/Wine а то в Windows просто упадёт.


    1. netch80
      18.10.2022 11:23
      +4

      > Кстати да, а может ли вообще wine обработать код с прямым syscall

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

      Если бы программа хотела работать под одной конкретной версией Windows, она могла бы зафиксировать эти номера, но кому это нужно? Поэтому всё кроме ну очень хакерского делают все вызовы через ntdll.dll через имена, где API уже предсказуемо.

      UPD: А ещё не обязательно один вызов ntdll.dll отображается в один сисколл. Может быть несколько вызовов, может быть локальная обработка… всё в их руках. Тут и есть граница стабильного userland API.

      > могут заменять вызовы условного OpenProcess на NtOpenProcess через syscall

      Чтобы работать, условно говоря, на винде 1803, но не 1809… такой софт точно не нужен под Wine.


      1. boris768
        18.10.2022 11:36

        Вполне себе существуют парсеры ntdll, способные вытаскивать номера вызовов SSDT, пример на шарпе, вопрос лишь чисто о возможности обработки подобного.

        И да, в принципе это возможно (но wine не делает) с помощью обработки SEH исключений, при таком вызове нужно лишь перебросить исполнение на вайновский ntdll в нужную функцию и оттуда продолжить исполнение.


        1. netch80
          18.10.2022 11:54
          +1

          > Вполне себе существуют парсеры ntdll, способные вытаскивать номера вызовов SSDT, пример на шарпе, вопрос лишь чисто о возможности обработки подобного.

          Тут тогда второй вопрос возникает (надо было, наверно, написать сразу) — ntdll.dll не обязана всё заворачивать в сисколлы ядра. Она вполне может сделать какую-то обработку сама — и иногда это делает. Может транслировать в два сисколла. И так далее. Граница стабильного userland API это именно вход в ntdll.dll. И обрабатывать уже весь код — это тьюринг-полная задача.

          Я там обновил с примечанием, пока было ещё две минуты:)

          > И да, в принципе это возможно (но wine не делает) с помощью обработки SEH исключений,

          Я попрошу подробностей, но это всё равно не покрывает всех случаев.


          1. boris768
            18.10.2022 13:02
            +2

            Пример можно найти здесь, это эмулятор kernel mode для запуска драйверов в целях анализа (очень специфичных, как Вы можете заметить), при этом применяется SEH для обработки вызовов ntoskrnl, чтения/записи памяти. Аналогично, как я предполагаю, можно обработать ситуации и в wine, но я с Вами согласен - задача слишком широкая для таких узких случаев. Благодарю за дискуссию.


    1. lookies
      19.10.2022 10:08
      +1

      Есть возможность с версии ядра 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 инструкции, что не всегда просто/возможно реализовать, но работать будет скорей всего быстрее чем в оригинале, т.к. переключений контекстов не будет вообще.


  1. Mingun
    17.10.2022 17:32
    +2

    Вот бы еще кто-то написал бы такую же понятную статью, чем все-таки syscall отличается от обычного call. И там и там стек (надеюсь). И там и там регистры. И там и там можно вернуть результат. В чем разница? Пока везде из объяснений вижу (тут читать с придыханием) "ну это же syscall!"


    1. screwer
      17.10.2022 17:47
      +19

      Если очень-очень кратко и упрощённо - различные уровни исполнения, которые отслеживаются аппаратно. Изначально пользовательский уровень не имел доступа к памяти уровня супервизора (ядро). Потом и в обратную сторону добавили подобное ограничение. Страницы памяти супервизора помечены специальным битом. В режиме супервизора (ядра) нам доступно больше инструкций, главным образом системных, управляющих трансляцией страниц памяти,состоянием процессора(-ов), управление кешами т.д.. Итого - супервизор изолирован от пользовательского кода, и имеет над ним полный контроль. Учитывает выделенные ресурсы, и способен уничтожить проблемную пользовательскую задачу, корректно освободить ресурсы, оставляя всю систему в рабочем и стабильном состоянии.
      Также ядро отвечает за обработку прерываний. Как аппаратных, обеспечивая работу устройств. Так и программных. Этот ваш syscall - в прошлом просто программное прерывание (2е или 80, для win/lin x86) которое обрабатывается ядром как системный вызов. Потом сделали специальную инструкцию, но суть осталась прежней.


    1. boris768
      17.10.2022 17:49
      +3

      syscall - это инструкция процессора, вызывающая переход исполнения кода в режим ядра, то есть связующая часть между UserMode и KernelMode, у нее несколько иное поведение, нежели у обычной инструкции call


      1. netch80
        18.10.2022 11:17
        +2

        Syscall как системный вызов между уровнями привилегий может быть сделан любой инструкцией. В контексте разговора по Wine важно именно это.
        То, что на x86-64 (x64 в терминах Windows) сейчас для этого используется syscall, это уже частности.
        В x86-32 на Windows использовалось несколько разных прерываний и у каждого задавался номер функции.


    1. vda19999
      17.10.2022 20:03
      +3

      Вообще-то, syscall - это новая инструкция. До её появления системные вызовы выполнялись с помощью инструкции int с параметром 0x80 (это число для Линукса, для винды какое-то другое, должно быть). Это означало, что процессор инициирует прерывание с номером 0х80, а далее передаёт управление коду операционной системы, который обрабатывает это прерывание, вызывая нужный системный вызов.

      У этой инструкции int очень хитрое описание того, что она делает, но если вкратце - сохраняет часть информации, типо сегментных регистров и меняет уровень привелегий.

      syscall и sysret появились позже и делают нечто похожее, при условии что ОС настроила их работы.


      1. Mingun
        17.10.2022 22:08

        А есть где-то почитать историю того, что именно настраивается, что значит настроить аппаратную инструкцию (новые же транзисторы на чипе не вырастишь), и зачем это было нужно, и как к этому пришли?


        1. 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. Адрес должен быть записан в MSR IA32_LSTAR (номер 0xC0000082)."

          Взято отсюда: https://gitlab.com/slon/shad-os/-/tree/master/gate64


        1. 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 делает обратное. По сравнению с переключением через шлюз привилегий, как в старом стиле, это удешевление, наверно, раз в сто-двести. Работа уже на уровне кода ядра с сохранением остальных регистров, конечно, смягчает эти сто раз, но всё равно время переключения сокращается ну очень заметно.


    1. DjPhoeniX
      18.10.2022 09:10
      +4

      Вообще про потроха всего системного лучше всего читать на osdev, в частности https://wiki.osdev.org/System_Calls


  1. Ukaru
    17.10.2022 22:26
    +4

    Спасибо за перевод. Стало гораздо понятнее и интересно.


  1. koil
    17.10.2022 23:39
    +16

    "Поэтому приложению нужен способ как бы «прервать своё выполнение» и передать управление ядру (эта операция обычно называется переключением контекста). "

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


  1. 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 на чистой свежеустановленной системе.


    1. mikleh
      18.10.2022 12:39

      Я так понимаю, это связано с тем, что под wine, да и в нативных сборках под linux, картинка отрисовывается далеко не в полном качестве, так как части функций DirectX просто нет. Естественно, заглушки выполняются быстрее чем те алгоритмы, которые винда честно выполняет.


      1. 13werwolf13
        18.10.2022 12:54
        +2

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


    1. perfect_genius
      18.10.2022 13:12

      Т.е. в Стиме продолжает продаваться игра, приводящая к BSOD?


      1. 13werwolf13
        18.10.2022 14:02

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

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


        1. perfect_genius
          18.10.2022 18:32
          +1

          Оказывается, действительно снята с продажи, но в комментариях не нашёл про BSOD. Это происходит прямо на запуске?
          Видимо, проблема из-за SecuROM.


          1. 13werwolf13
            18.10.2022 18:46
            +1

            Да, после нажатия на кнопку "играть" в Steam появляется окошко с какими то стимовскими препендами как для каждой игры, посде чего оно закрывается, на долю секунды мигает полноэкранная вьюха игры и сразу BSOD. В причинах не разбирался так как не интересно, единственное что проверил две версии винды, и семёрку и десятку. Так как мои знания о работе ОС мелкософта и софта под них практически равны нулю копаться дальше не было желания.


      1. billyevans
        19.10.2022 03:06
        +1

        У меня очень предсказуемо gta V делала BSOD и сейчас red dead redemption 2 примерно после 30-60м игры тоже. Кажется это появилось после обновления на win11.


        1. perfect_genius
          19.10.2022 13:40
          +1

          Абсолютно дико такое встречать в современное время.


          1. billyevans
            20.10.2022 03:11

            Особенно дико это выглядит для меня. Последний раз я комп на винде использовал в 2003м году, тут решил купить чисто игровой, там вообще ничего кроме Стима и battle.net ну и игр из них не установлено. Я и дрова всякие обновлял и температуру пытался мерить, в общем странная фигня, но мой опыт использования PC 20 лет назад такой же как и сейчас с синим экраном ;-) Сейчас даже более консистентно.


  1. axe_chita
    18.10.2022 09:02

    Спасибо за ликбез, было познавательно.


  1. saipr
    18.10.2022 10:52

    С удовольствием прочитал. Wine стоит аж с 1998 года, в основном для word и excel.