Дата публикации статьи: 14.06.2006 18:49

Использование фиберов для расщепления выполнения процедуры

Автор: А. Скробов

    В двух моих предыдущих статьях (1, 2) я рассказывал об использовании фиберов (“волокон”) в VB6 для имитации одновременного выполнения нескольких задач, и для упрощения реализации COM-перечислителей. В этой статье пойдёт речь о третьей интересной области применения фиберов – реализации расщепления (fork) выполнения программы.

Расщепление выполнения программы

    Расщепление (fork) в UNIX-совместимых ОС – это создание для родительского процесса его идентичной копии в качестве дочернего процесса; поскольку поддержки многопоточных приложений в UNIX длительное время не было, операция расщепления, позволяющая передать дочернему процессу большой объём данных, активно использовалась для организации в рамках одного приложения нескольких параллельно работающих процессов. (Например, для сетевых сервисов классическим решением является создание нового дочернего процесса для каждого клиентского подключения; эти процессы наследуют от родительского и код, и данные.) В Windows же, напротив, создание нового процесса возможно только при запуске нового приложения; при этом код и данные вновь созданного процесса инициализируются из исполняемого файла. (Низкоуровневый (native) API в Windows NT позволяет выполнить в точности такое же расщепление процесса, которое выполняется в UNIX функцией fork; но этот низкоуровневый API не поддерживается Microsoft, и использование его функций бывает оправдано только в очень узком классе низкоуровневых приложений.)
    Как расщепление, так и создание процесса из исполняемого файла имеют в различных ситуациях свои преимущества. Расщепление оказывается эффективным решением в случаях, когда задача естественно дробится на подзадачи – тогда программа превращается в дерево процессов, каждый из которых наследует от родительского готовое решение какой-то части задачи. Примером задачи, в которой оправдана такая организация программы, может быть реализация метода ветвей и границ. Однако в Windows создание большого количества потоков в одном процессе связано со сложностями из-за исчерпания их стеками адресного пространства процесса. (Одним из решений этой проблемы могло бы быть использование пула потоков). Кроме того, создание потока подразумевает запуск его процедуры с самого начала, передавая ей аргументом адрес структуры с параметрами – тогда как естественным в случае расщепления потока было бы наследование “дочерним” потоком локальных переменных “родительского”. (На самом деле Windows не хранит отношения иерархии для потоков, и их деление на дочерние и родительские чисто условное.) Можно было бы попытаться создать точную копию существующего потока при помощи функций API GetThreadContext и SetThreadContext, но сложности есть и тут: во-первых, эти функции нельзя применять к запущенному потоку – значит, поток, когда он хочет расщепиться, должен запрашивать некий поток-диспетчер, чтобы тот остановил первый поток, создал его копию, и вновь запустил первый поток. Во-вторых, состояние потока, являющегося объектом ядра Windows, невозможно в точности воспроизвести из кода, выполняющегося в пользовательском режиме, – и поэтому создать полную копию существующего потока в принципе невозможно.
Далее будет показано, как применение фиберов позволяет решить все названные проблемы.

Устройство фиберов

    Чтобы обеспечить полное восстановление состояния процессора при возврате управления в фибер, ОС должна выделять для каждого фибера отдельный стек и сохранять в принадлежащем фиберу блоке памяти, называемом контекстом фибера, копию значений регистров процессора. Кроме этого, Windows выделяет 4 байта в контексте фибера для его “данных”; это может быть указатель на структуру произвольного размера. Содержимое этих 4 байт задаётся при создании фибера функцией CreateFiber или ConvertThreadToFiber и передаётся аргументом в процедуру фибера; кроме того, доступ к этим 4 байтам можно получать при помощи макроса GetFiberData.
    Начиная с Windows Server 2003, контекст фибера дополнен копией состояния сопроцессора и локальным хранилищем фибера (fiber local storage, FLS). В Windows XP и более старых версиях состояние сопроцессора не восстанавливалось при переключении фиберов, что препятствовало реализации алгоритмов, использующих вычисления с плавающей запятой, в виде многофиберных программ. Чтобы создать фибер, в контексте которого сохраняется копия состояния сопроцессора, нужно использовать новые функции API ConvertThreadToFiberEx и CreateFiberEx.
    FLS работает по аналогии с локальным хранилищем потока (thread local storage, TLS): сначала функцией FlsAlloc выделяется индекс FLS, затем этот индекс используется для доступа к 4-байтной ячейке FLS посредством функций API FlsGetValue и FlsSetValue. Когда использование ячейки FLS завершено, она освобождается вызовом функции FlsFree. Работа этих четырёх функций аналогична работе функций TLS API – TlsAlloc, TlsGetValue, TlsSetValue и TlsFree соответственно, однако есть одна дополнительная возможность, отсутствующая в TLS API: при выделении индекса FLS можно указать функцию обратного вызова (callback), которая будет вызвана при уничтожении соответствующей ячейки FLS (либо явным вызовом FlsFree, либо уничтожением содержавшего её фибера, либо уничтожением потока, содержавшего этот фибер).
    Чтобы понять механизм работы фиберов в Windows на самом низком уровне, мной был осуществлён “обратный инженеринг” относящихся к ним функций API. Оказывается, что код, реализующий поддержку фиберов, во многом перекрывается с кодом поддержки потоков. (См. Приложение.) При создании фибера его контекст (структура размером 740 байт, назначение большей части которой неизвестно) выделяется в куче (heap) текущего процесса. Затем для фибера выделяется новый стек (при вызове CreateFiber), либо наследуется стек текущего потока (при вызове ConvertThreadToFiber). При этом содержимое полей регистров общего назначения в контексте вновь созданного фибера никак не инициализируется; мои эксперименты с отладчиком в Windows XP SP2 показали, что они заполняются значением 0BAADF00Dh. При преобразовании текущего потока в фибер не инициализируются даже поля служебных регистров в его контексте; они будут заполнены при первом переключении из этого фибера в другой. Поэтому предварительное преобразование текущего потока в фибер на самом деле необходимо только для переключения фиберов; создавать и удалять фиберы можно даже из “обычного” потока.
Информация о стеке фибера хранится в его контексте в полях StackBase, StackLimit и DeallocationStack – а именно, StackBase хранит адрес дна стека (верхнего края его закреплённой (committed) части); StackLimit хранит адрес предела стека (верхнего края сигнальной страницы (guard page), либо, если закреплён весь зарезервированный под стек объём памяти, и сигнальной страницы нет, – нижнего края закреплённой части стека); наконец, DeallocationStack хранит адрес нижнего края выделенного для стека блока виртуальной памяти.
    Далее, мы видим, что функция SwitchToFiber сохраняет только те регистры общего назначения, которые должны сохраняться процедурой типа stdcall – а именно ebx, ebp, esi и edi; так как SwitchToFiber сама является процедурой типа stdcall, то компилятор всё равно не должен рассчитывать, что между её вызовом (т.е. переключением на другой фибер) и возвратом из неё (т.е. переключением с другого фибера) будут сохраняться остальные регистры общего назначения. Любопытно отметить, что внутренняя функция BaseFiberStart, с которой начинается выполнение вновь созданного фибера, использует регистр ebx для хранения 4-байтных “данных” фибера, поэтому при входе в процедуру фибера значение регистра ebx будет совпадать с переданным аргументом.
Кроме сохранения и восстановления значений регистров, функция SwitchToFiber сохраняет в контексте вытесняемого фибера текущие значения полей ExceptionList и StackLimit из блока информации (TIB) текущего потока; после этого функция SwitchToFiber восстанавливает из контекста нового фибера значения ExceptionList, StackBase, StackLimit и DeallocationStack. Поскольку значения полей StackBase и DeallocationStack задаются один раз при создании стека фибера и никогда не изменяются, то они не обновляются в контексте фибера при переключениях.
    В Windows Server 2003 поддержка фиберов существенно расширена. Контекст фибера увеличен до 752 байт: в одном из трёх добавленных полей хранится адрес таблицы FLS, в другом – адрес стека контекстов активации (activation context stack, ACS) фибера. (Контексты активации появились в Windows XP в рамках технологии Side-by-side (SxS); контекст активации указывает, которая из нескольких одновременно установленных версий одной библиотеки будет использоваться программой. Windows XP позволяет использовать собственный ACS для каждого потока, Windows Server 2003 – для каждого фибера.) Таким образом, в Windows Server 2003 данные фибера состоят не из двух независимых частей, как раньше, а из четырёх – стека, контекста, FLS и ACS.
Третье из новых полей в контексте фибера появилось, начиная с Windows Server 2003 SP1; оно хранит гарантированный объём стека, задаваемый функцией API SetThreadStackGuarantee – это тот объём стека, которым может пользоваться процедура обработки исключения переполнения стека. Мы видим, что в Windows Server 2003 продолжается вынесение в контекст фибера данных, относившихся ранее к контексту потока.
    Кроме того, что в контекст фибера добавлены три перечисленных поля, в Windows Server 2003 по-новому используется структура FiberContext – теперь в ней может сохраняться копия состояния сопроцессора (а конкретно, в полях FloatSave.ControlWord, FloatSave.StatusWord и Dr6; в последнем из них сохраняется относящийся к SSE регистр mxcsr). Другое интересное изменение – то, что больше не используется поле FiberContext.Eip; вместо этого на стеке оставляется адрес возврата из функции SwitchToFiber, так что после восстановления регистра esp инструкция ret приводит к возврату управления в нужный фибер. Наконец, теперь при создании контекста фибера всё его содержимое инициализируется нулями. Из всего этого можно сделать вывод, что при разработке Windows Server 2003 поддержка фиберов подверглась серьёзной переработке – были устранены предыдущие недочёты, добавлен существенный пласт новой функциональности. Как результат этого, код, работавший с фиберами на низком уровне в Windows XP и более старых версиях, при переносе на Windows Server 2003 потребует проверки и доработки.
Ввиду сложности и малодокументированности новых структур, появившихся в контексте фибера в Windows Server 2003, а также малой распространённости ОС, в которых эти структуры поддерживаются, – в дальнейшем низкоуровневая работа с фиберами Windows будет рассматриваться только применительно к их “старой” реализации, относящейся к версиям от Windows NT 3.51 до Windows XP включительно.

Расщепление фиберов

    Теперь, когда мы знаем все тонкости внутреннего устройства фиберов, мы видим, что их использование для расщепления задачи на подзадачи оказывается решением всех перечисленных в первой части статьи проблем. Поскольку данные фибера целиком расположены в памяти пользовательского режима, то возможно создать его точную копию; компактность этих данных позволяет разместить в памяти большое число фиберов; наконец, благодаря кооперативному принципу разделения времени между фиберами отпадает необходимость в остановке и повторном запуске задачи на время создания её копии.
    Моей целью было создать пример программы, использующей фиберы для расщепления процедуры с полным сохранением контекста её выполнения (текущая команда внутри процедуры, значения локальных переменных и регистров). В качестве задачи для иллюстрации этого подхода взята задача разложения числа на множители всеми возможными (с точностью до перестановок множителей) способами. Вот соответствующая ей “исходная” программа для UNIX-совместимых ОС, написанная на Перле c использованием функции fork:

#!/usr/bin/perl
($N=shift) or die ("usage: factorize.pl N\n");
factorize($N);


sub factorize {$N=shift;
my @factors, $i;
  for ($i=2; $i<$N; $i++) {
    unless (($N%$i)||fork) {
      push @factors, $i;
      $N/=$i;
      exit if ($N<$i--);
    }
  }
  print join('*',@factors, "$N\n");
}

    К статье приложен исходный код программы, аналогичной этой, но написанной на VB6 и работающей под Windows. Она подробно прокомментирована, поэтому нет нужды дополнительно рассматривать детали её работы здесь; коснусь лишь основных моментов. Прежде всего, для каждого “дочернего” фибера необходимо создавать копию стека его “родителя”, чтобы сохранялись значения локальных переменных расщепляемой процедуры. Недостаточно просто скопировать содержимое стека фибера-родителя в стек фибера-копии: если процедура хранила ссылки на свои локальные переменные, то эти ссылки будут продолжать указывать внутрь стека фибера-родителя. (Примером таких ссылок, указывающих внутрь стека, является цепочка кадров стека.) Поэтому перед переключением на фибер необходимо восстанавливать содержимое стека (который в этом случае оказывается общим для всех фиберов) из сохранённой для этого конкретного фибера копии; фактически, мы добавляем в состав контекста фибера содержимое его стека. Поскольку восстановление содержимого стека может потребовать копирования больших объёмов памяти, то нам выгодно переключать фиберы как можно реже – в данном примере вся процедура фибера выполняется целиком без переключения на другие фиберы.
    Кроме иерархии основных фиберов с общим стеком, образующей дерево подзадач исходной задачи, в данной реализации используются также два служебных фибера, каждый со своим отдельным стеком. Один из этих фиберов используется для репликации основных фиберов (создания по фиберу-родителю его точной копии), другой – для подвёрстки стека при переключении основных фиберов.
В данном примере мы предполагаем, что стек основных фиберов никогда не выйдет за пределы изначально закреплённой области: если стек фибера-родителя превосходит по размеру эту область, то копирование его в стек “дочернего” фибера приведёт к исключению защиты памяти. Можно было бы реализовать обработку такой ситуации, и расширять закреплённую часть стека фибера-копии, когда это необходимо; но мной было принято решение не усложнять данный пример в ущерб его иллюстративности. Само собой разумеется, что использование приведённого кода в промышленных целях потребовало бы его значительной доработки и дополнительного тестирования; мной же лишь намечен путь решения указанных проблем.
Касательно размера стека: поскольку я при создании фиберов не передаю в функцию API CreateFiber аргумент dwStackSize, то используется размер по умолчанию (1 МБ зарезервирован, 4 КБ закреплено). В данной задаче 2 ГБ адресного пространства процесса оказывается достаточно, чтобы вместить необходимое число стеков фиберов.     Если же, однако, требуется использовать для фиберов стеки меньшего размера, то необходимо либо уменьшать значение поля размера памяти, зарезервированной под стек, в заголовке исполняемого файла, либо удалять создаваемый для фибера системой стек и использовать вместо него самостоятельно созданный блок памяти нужного размера, – потому что Windows не позволяет зарезервировать под стек меньший объём памяти, чем указан в заголовке исполняемого файла, независимо от значения аргумента dwStackSize, переданного в функцию CreateFiber.
    Хочется отметить, что в отличие от моих первых примеров использования фиберов в VB6, эта программа работает и в скомпилированном виде, и под управлением среды разработки VB6; возможно, что это лишь счастливая случайность.
Несмотря на всю гибкость и универсальность предложенного подхода, он применим не во всех случаях, когда естественно возникает дробление задач на подзадачи (перебор перестановок, подмножеств и т.д.) Например, возникает естественное желание попробовать при помощи этого метода решать задачу о восьми ферзях: проходить двойным циклом по доске, и во всех полях, где возможно при текущей расстановке ферзей поставить нового ферзя – расщеплять выполнение. В одной из подзадач мы ставим нового ферзя на это поле, в другой подзадаче – пропускаем его. Тогда, если мы в какой-то из подзадач поставили восьмого ферзя, значит мы получили искомую расстановку; а если мы дошли до последнего поля доски, так и не расставив все восемь ферзей, значит эта расстановка не подходит. Этот алгоритм можно условно обозначить таким кодом корневого фибера, заменяющим в приведённой программе процедуру Factorize:

Private Type Board
    Squares(1 To 8, 1 To 8) As Boolean
    Queens As Long                  ' число установленных ферзей
End Type

Private Sub IFiber_FiberProc()
Dim Board As Board, i As Long, j As Long
For j = 1 To 8: For i = 1 To 8
    If CanPut(Board, i, j) Then
        If Not Fork Then
            Board.Squares(i, j) = True
            Board.Queens = Board.Queens + 1
            If 8 = Board.Queens Then PrintBoard Board: Switch
        End If
    End If
Next i, j
Switch
End Sub

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

Private Sub IFiber_FiberProc()
Dim Board As Board, i As Long, j As Long
For j = 1 To 8: For i = 1 To 8
    If CanPut(Board, i, j) Then
        If Not Fork Then
            Board.Squares(i, j) = True
            Board.Queens = Board.Queens + 1
            If 8 = Board.Queens Then PrintBoard Board: Switch
        End If
    End If
Next i: If If (j <> 8) And (GetCurrentFiber <> pFiber) Then Reschedule
Next j: Switch
End Sub

в модуле modFork:

Private CurrentFiber As IFiber      ' текущий основной фибер
Private StackSaver As Long          ' фибер, сохраняющий стек

' сохранить копию содержимого стека фибера
Private Sub SaveStack(ByVal SourceFiber As Long)    
Dim SourceContext As FIBER          ' считать данные из контекста фибера
    CopyMemory SourceContext, ByVal SourceFiber, LenB(SourceContext)

    ' скопировать стек
    Dim StackSize As Long: StackSize = StackBase - SourceContext.FiberContext.Esp
    CopyMemory ByVal (SourceContext.StackBase - StackSize), _
        ByVal SourceContext.FiberContext.Esp, StackSize
End Sub

' это наш планировщик фиберов
Public Sub Switch()                 ' выполняет следующий фибер в очереди
If FiberList.Count Then             ' требуется выполнение новых фиберов?
    Dim NextFiber As IFiber
    Set NextFiber = FiberList(1)    ' берём следующий фибер из очереди
    FiberList.Remove 1              ' удаляем его из очереди
    Set CurrentFiber = NextFiber    ' запоминаем его как текущий фибер
    NextFiber.Switch                ' переходим к его выполнению
Else                                ' все фиберы выполнены
    SwitchToFiber pFiberMain        ' переключаемся на главный фибер
End If
End Sub

Public Sub Reschedule()     ' добавить фибер в конец очереди для повторного выполнения
    FiberList.Add CurrentFiber
    FiberArgument = GetCurrentFiber
    SwitchToFiber StackSaver        ' сохранить стек текущего фибера
    Switch
End Sub

' процедура фибера, сохраняющего стек
Private Sub StackSaver_FiberProc(ByVal Data As Long)
Do
    SaveStack FiberArgument         ' сохранить копию содержимого стека
    SwitchToFiber FiberArgument     ' переключиться обратно на этот фибер
Loop
End Sub

    Здесь StackSaver_FiberProc – процедура третьего служебного фибера, который сохраняет текущее содержимое основного стека в стеке, прикреплённом к дочернему фиберу, а процедура Reschedule добавляет текущий фибер в конец списка фиберов, назначенных на выполнение. (Теперь, поскольку выполнение фибера прерывается, необходимо сохранять и восстанавливать содержимое его стека.) Приостановка текущего фибера выполняется в конце обхода каждой горизонтали, если только это не конец восьмой горизонтали (тогда обход доски завершён, и приостановка бессмысленна) и текущий фибер – не корневой (иначе нам негде сохранять копию его стека).
Реализация метода ветвей и границ также потребует доработки нашего метода: список фиберов, назначенных на выполнение, должен в этом случае быть не простой очередью, а хранить для каждого фибера “вес” (границу) представляемой им подзадачи (ветви), и позволять выбирать из списка ветвь с наименьшей границей. Чтобы организовать такой список, нужно хранить в нём фиберы упорядоченными по “весам” – т.е. добавлять новые фиберы не в конец списка, а в нужное место в середине. В остальном реализация метода ветвей и границ с использованием нашего подхода совпадает с реализацией обычных переборных задач.

Приложение. Псевдокод функций Windows API, работающих с фиберами
// нижеследующие объявления взяты из файла winnt.h из состава Windows DDK

_inline PVOID GetCurrentFiber( void ) { __asm mov eax, fs:[0x10] }
_inline PVOID GetFiberData( void )    { __asm mov eax, fs:[0x10] __asm mov eax,[eax] }

#define SIZE_OF_80387_REGISTERS 80
typedef struct _FLOATING_SAVE_AREA {
    DWORD   ControlWord, StatusWord, TagWord;
    DWORD   ErrorOffset, ErrorSelector;
    DWORD   DataOffset, DataSelector;
    BYTE    RegisterArea[SIZE_OF_80387_REGISTERS];
    DWORD   Cr0NpxState;
} FLOATING_SAVE_AREA, *PFLOATING_SAVE_AREA;

typedef struct {
    DWORD ContextFlags;
    DWORD   Dr0, Dr1, Dr2, Dr3, Dr6, Dr7;         // отладочные регистры
    FLOATING_SAVE_AREA FloatSave;                 // состояние сопроцессора
    DWORD   SegGs, SegFs, SegEs, SegDs;           // сегментные регистры
    DWORD   Edi, Esi, Ebx, Edx, Ecx, Eax;         // регистры общего назначения
    DWORD   Ebp, Eip, SegCs, EFlags, Esp, SegSs;  // служебные регистры
} CONTEXT, *PCONTEXT;

typedef struct _NT_TIB {
    PEXCEPTION_REGISTRATION_RECORD ExceptionList;
    PVOID StackBase;
    PVOID StackLimit;
    PVOID SubSystemTib;
    PVOID FiberData;
    PVOID ArbitraryUserPointer;
    struct _NT_TIB *Self;
} NT_TIB, *PNT_TIB;

#define NtCurrentProcess() ((HANDLE) -1)


// нижеследующий псевдокод соответствует Windows 2000 SP1

#define PcTeb              18h
_inline PVOID NtCurrentTeb( void )    { __asm mov eax, fs:[PcTeb] }

typedef struct {
    NT_TIB NtTib;
    PVOID EnvironmentPointer;
    CLIENT_ID ClientId;
// ПРОПУЩЕНО: ещё пара десятков полей
    PVOID DeallocationStack;  // смещение 0E0Ch
// ПРОПУЩЕНО: ещё с десяток полей
} TEB, *PTEB;

typedef struct _INITIAL_TEB {
    PVOID OldStackBase;
    PVOID OldStackLimit;
    PVOID StackBase;
    PVOID StackLimit;
    PVOID StackAllocationBase;
// ПРОПУЩЕНО: назначение дальнейших полей неизвестно
} INITIAL_TEB, *PINITIAL_TEB;

typedef struct {
    PVOID FiberData
    PEXCEPTION_REGISTRATION_RECORD ExceptionList
    PVOID StackBase
    PVOID StackLimit
    PVOID DeallocationStack
    CONTEXT FiberContext
// ПРОПУЩЕНО: назначение дальнейших полей неизвестно
} FIBER, *PFIBER;


typedef enum {
    BaseContextTypeProcess,
    BaseContextTypeThread,
    BaseContextTypeFiber
} BASE_CONTEXT_TYPE, *PBASE_CONTEXT_TYPE;

VOID BaseInitializeContext(PCONTEXT Context, PVOID Parameter, PVOID InitialPc,
                           PVOID InitialSp, BASE_CONTEXT_TYPE ContextType) {
/*  Эта функция инициализирует структуру CONTEXT.

Аргументы:
    Context   – Указывает буфер, в который поместит результат эта процедура.
    Parameter – Указывает параметр потока/фибера.
    InitialPc – Указывает начальное значение программного счётчика (EIP).
    InitialSp – Указывает начальное значение указателя стека (ESP).
    NewThread – Указывает назначение создаваемого контекста (с фиберами используется
           BaseContextTypeFiber).
*/
    Context->Eax = (ULONG)InitialPc;
    Context->Ebx = (ULONG)Parameter;

// ПРОПУЩЕНО: Инициализация полей сегментных регистров и флагов в Context.

    Context->Esp = (ULONG) InitialSp;

    // Фибер всегда запускается, начиная с процедуры BaseFiberStart.
    if ( ContextType == BaseContextTypeThread ) {
        Context->Eip = (ULONG) BaseThreadStartThunk;
    } else if ( ContextType == BaseContextTypeFiber ) {
        Context->Eip = (ULONG) BaseFiberStart;
    } else {
        Context->Eip = (ULONG) BaseProcessStartThunk;
    }

    Context->ContextFlags = CONTEXT_FULL;
    Context->Esp -= sizeof(Parameter); // Зарезервируем место для адреса возврата
}


VOID BaseThreadStart(LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter) {
/*  Существование потока в Win32 начинается с выполнения этой функции. Её назначение
    – вызвать процедуру потока, и, если она возвратит управление, уничтожить поток.

Аргументы:
    lpStartAddress – Указывает адрес процедуры потока; эта процедура должна
        принимать один 32-битный аргумент, и не должна возвращать управление.
    lpParameter – Указывает параметр, передаваемый в процедуру потока.
*/
    try {
// ПРОПУЩЕНО: если эта функция вызывается в контексте вновь созданного потока, то
//            здесь создаются соответствующие ей структуры режима ядра.
        ExitThread((lpStartAddress)(lpParameter));
    } except(UnhandledExceptionFilter(GetExceptionInformation())) {
        if ( !BaseRunningInServerProcess ) {
            ExitProcess(GetExceptionCode());
        } else {  // когда исключение в потоке-сервисе, уничтожается только этот поток
            ExitThread(GetExceptionCode());
        }
    }
}


VOID BaseFiberStart(VOID) {
/*  Существование фибера в Win32 начинается с выполнения этой функции. Её назначение
    – вызвать BaseThreadStart, взяв аргументы из структуры контекста фибера.
*/
    PFIBER Fiber = GetCurrentFiber();
    BaseThreadStart((LPTHREAD_START_ROUTINE)Fiber->FiberContext.Eax,
                    (LPVOID)Fiber->FiberContext.Ebx);
}


// NTDLL.RtlAllocateHeap документирован под псевдонимом (alias) KERNEL32.HeapAlloc

LPVOID CreateFiber(DWORD dwStackSize, LPFIBER_START_ROUTINE lpStartAddress,
                   LPVOID lpParameter) {
    NTSTATUS Status;
    PFIBER Fiber;
    INITIAL_TEB InitialTeb;

    // Выделяется память под структуру FIBER (контекст фибера).
    Fiber = RtlAllocateHeap(RtlProcessHeap(), MAKE_TAG(TMP_TAG), sizeof(*Fiber));
    if ( !Fiber ) {
        SetLastError(ERROR_NOT_ENOUGH_MEMORY);
        return Fiber;
    }

    // Выделяется стек для вновь созданного фибера.
    Status = BaseCreateStack(NtCurrentProcess(), dwStackSize, 0L, &InitialTeb);
    if ( !NT_SUCCESS(Status) ) {
        BaseSetLastNTError(Status);
        RtlFreeHeap(RtlProcessHeap(), 0, Fiber);
        return NULL;
    }

    // Контекст фибера заполняется начальными значениями.
    Fiber->FiberData = lpParameter;
    Fiber->StackBase = InitialTeb.StackBase;
    Fiber->StackLimit = InitialTeb.StackLimit;
    Fiber->DeallocationStack = InitialTeb.StackAllocationBase;
    Fiber->ExceptionList = (PEXCEPTION_REGISTRATION_RECORD)-1;

    BaseInitializeContext(&Fiber->FiberContext,lpParameter,(PVOID)lpStartAddress,
                          InitialTeb.StackBase,BaseContextTypeFiber);

    return Fiber;
}


LPVOID ConvertThreadToFiber(LPVOID lpParameter) {
    PFIBER Fiber;
    PTEB Teb;

    // Выделяется память под структуру FIBER (контекст фибера).
    Fiber = RtlAllocateHeap(RtlProcessHeap(), MAKE_TAG(TMP_TAG), sizeof(*Fiber));
    if ( !Fiber ) {
        SetLastError(ERROR_NOT_ENOUGH_MEMORY);
        return Fiber;
    }

    // Контекст фибера заполняется начальными значениями.
    Teb = NtCurrentTeb();
    Fiber->FiberData = lpParameter;
    Fiber->StackBase = Teb->NtTib.StackBase;
    Fiber->StackLimit = Teb->NtTib.StackLimit;
    Fiber->DeallocationStack = Teb->DeallocationStack;
    Fiber->ExceptionList = Teb->NtTib.ExceptionList;
    Teb->NtTib.FiberData = Fiber;

    return Fiber;
}


VOID DeleteFiber(LPVOID lpFiber) {
    SIZE_T dwStackSize;
    PFIBER Fiber = lpFiber;

    // Если уничтожается текущий фибер, уничтожить вместе с ним поток
    if ( NtCurrentTeb()->NtTib.FiberData == Fiber ) {
        ExitThread(1);
    }

    // Освободить стек фибера
    dwStackSize = 0;
    NtFreeVirtualMemory(NtCurrentProcess(),&Fiber->DeallocationStack,
                        &dwStackSize,MEM_RELEASE);

    // Освободить контекст фибера
    RtlFreeHeap(RtlProcessHeap(),0,Fiber);
}


; VOID SwitchToFiber(PFIBER NewFiber)
;
;    Эта функция сохраняет состояние текущего фибера, и переключает выполнение
;    на другой фибер.
;
; Аргументы:
;    NewFiber – Указывает адрес нового фибера.
;
; Возвращаемое значение: нет

SwitchToFiber PROC

        mov     edx,fs:[PcTeb]                   ; edx указывает на TEB
        mov     eax,[edx].TEB.NtTib.FiberData    ; eax указывает на текущий фибер

; Сохранить регистры, которые должны сохраняться при вызове stdcall-процедуры
        mov     [eax].FIBER.FiberContext.Ebx,ebx
        mov     [eax].FIBER.FiberContext.Edi,edi
        mov     [eax].FIBER.FiberContext.Esi,esi
        mov     [eax].FIBER.FiberContext.Ebp,ebp

; При обратном переходе из стека исключатся адрес возврата и аргумент
        mov     ecx,esp
        add     ecx,8
        mov     [eax].FIBER.FiberContext.Esp,ecx

; Сохранить адрес возврата для обратного перехода
        mov     ebx,[esp]
        mov     [eax].FIBER.FiberContext.Eip,ebx

; Сохранить параметры стека и обработки исключений
        mov     ecx,[edx].TEB.NtTib.ExceptionList
        mov     ebx,[edx].TEB.NtTib.StackLimit
        mov     [eax].FIBER.ExceptionList,ecx
        mov     [eax].FIBER.StackLimit,ebx

; Теперь восстановить состояние другого фибера
        mov     eax,[esp]+4                      ; eax указывает на новый фибер

; Восстанавливаем TEB
        mov     ecx,[eax].FIBER.ExceptionList
        mov     ebx,[eax].FIBER.StackBase
        mov     esi,[eax].FIBER.StackLimit
        mov     edi,[eax].FIBER.DeallocationStack
        mov     [edx].TEB.NtTib.ExceptionList,ecx
        mov     [edx].TEB.NtTib.InitialStack,ebx
        mov     [edx].TEB.NtTib.StackLimit,esi
        mov     [edx].TEB.DeallocationStack,edi

; Восстанавливаем указатель на текущий фибер
        mov     [edx].TEB.NtTib.FiberData,eax

; Восстанавливаем регистры нового фибера
        mov     edi,[eax].FIBER.FiberContext.Edi
        mov     esi,[eax].FIBER.FiberContext.Esi
        mov     ebp,[eax].FIBER.FiberContext.Ebp
        mov     ebx,[eax].FIBER.FiberContext.Ebx
        mov     ecx,[eax].FIBER.FiberContext.Eip
        mov     esp,[eax].FIBER.FiberContext.Esp

        jmp     ecx

SwitchToFiber ENDP