לדלג לתוכן

יצירת תהליך וזהותו

נניח שהכלי שלנו מפעיל תוכנית עזר. הפעולה נראית כמו קריאה לפונקציה, אבל למעשה אנחנו מבקשים מערכת חדשה של משאבים: מרחב כתובות, thread ראשוני, token, סביבת טקסט ו־handles. אם אחד החלקים אינו מוגדר, התוכנית יכולה לעבוד במחשב אחד ולהתנהג אחרת באחר.

הפעולה CreateProcessW יוצרת תהליך ו־thread ראשוני. הצלחתה אינה אומרת שהילד הגיע ל־main, שקרא תצורה או שהוא מוכן לקבל בקשות. זוהי נקודת התחלה של מחזור חיים, ולא אישור מוכנות.

מפעילים עותק של עצמנו

התוכנית הבאה מזהה אם היא בתפקיד הורה או ילד. ההורה מפעיל את אותו image עם דגל קבוע וסביבת משתנים קטנה ומפורשת. שומרים אותה כ־spawn.cpp.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstdio>
#include <cwchar>
#include <iterator>
#include <string>
#include <vector>

int wmain(int argc, wchar_t** argv) {
    if (argc == 2 && std::wcscmp(argv[1], L"--child") == 0) {
        wchar_t role[64]{};
        const DWORD length = GetEnvironmentVariableW(
            L"AMITTECH_ROLE", role, static_cast<DWORD>(std::size(role)));
        if (length == 0 || length >= std::size(role)) return 8;
        std::wprintf(L"child pid=%lu role=%ls\n",
                     GetCurrentProcessId(), role);
        return 7;
    }

    std::vector<wchar_t> path(32768);
    DWORD length = GetModuleFileNameW(
        nullptr, path.data(), static_cast<DWORD>(path.size()));
    if (length == 0 || length >= path.size()) return 1;

    std::vector<wchar_t> system_root(32768);
    length = GetEnvironmentVariableW(L"SystemRoot", system_root.data(),
                                    static_cast<DWORD>(system_root.size()));
    if (length == 0 || length >= system_root.size()) return 2;

    std::wstring command = L"\"" + std::wstring(path.data()) + L"\" --child";
    std::wstring environment = L"AMITTECH_ROLE=child";
    environment.push_back(L'\0');
    environment += L"SystemRoot=";
    environment += system_root.data();
    environment.push_back(L'\0');
    environment.push_back(L'\0');

    STARTUPINFOW startup{};
    startup.cb = sizeof(startup);
    PROCESS_INFORMATION process{};
    if (!CreateProcessW(path.data(), command.data(), nullptr, nullptr,
                        FALSE, CREATE_UNICODE_ENVIRONMENT,
                        environment.data(), nullptr, &startup, &process)) {
        std::printf("create error=%lu\n", GetLastError());
        return 3;
    }

    CloseHandle(process.hThread);
    const DWORD wait = WaitForSingleObject(process.hProcess, INFINITE);
    DWORD exit_code = 0;
    const BOOL queried = wait == WAIT_OBJECT_0 &&
                         GetExitCodeProcess(process.hProcess, &exit_code);
    const DWORD error = queried ? 0 : GetLastError();
    CloseHandle(process.hProcess);
    if (!queried) {
        std::printf("query error=%lu\n", error);
        return 4;
    }
    std::printf("child exit=%lu\n", exit_code);
}
cl /nologo /std:c++20 /EHsc /W4 /Zi spawn.cpp /Fespawn.exe
.\spawn.exe

הכללת iterator מספקת את std::size במפורש, ולכן הניסוי אינו נשען על הכללות עקיפות של ספרייה מסוימת.

שם היישום ושורת הפקודה אינם אותו פרמטר

העברנו נתיב מלא ב־lpApplicationName. כך איננו מבקשים מהמערכת לנחש איזו תוכנית מסתתרת בתחילת שורת פקודה עם רווחים. שורת הפקודה עדיין כוללת את שם התוכנית כפי שה־CRT של הילד מצפה לפרש אותו.

הפרמטר lpCommandLine ניתן לשינוי על ידי CreateProcessW. לכן העברנו buffer בר־כתיבה של wstring, ולא literal קבוע שהומר ב־cast. החל מ־C++17, הגרסה הלא־const של data מספקת גישה מתאימה לאחסון.

הציטוט בדוגמה מספיק לנתיב תוכנית Windows ולדגל הקבוע. הוא אינו פונקציה כללית לציטוט רשימת ארגומנטים שרירותית. פרמטר עם backslashes לפני מרכאות דורש טיפול לפי parser היעד; תוכנית שאינה משתמשת ב־CRT יכולה לפרש שורת פקודה אחרת.

הסביבה היא snapshot של החלטה

בלוק environment הוא רצף של מחרוזות שמסתיימות ב־null, עם null נוסף בסוף. העברנו דגל Unicode כדי שהמערכת תפרש אותו כ־UTF-16. ההורה מספק רק משתנה ניסוי ו־SystemRoot, ולא מעתיק בסתר את כל סביבתו.

לילד עשויות להיות דרישות נוספות כמו PATH או TEMP. במקרה כזה מוסיפים אותן לחוזה, או בוחרים במפורש לרשת את הסביבה באמצעות null. המטרה אינה תמיד לצמצם למינימום, אלא לדעת איזו בחירה נעשתה.

שינוי מאוחר במשתני ההורה אינו מעדכן את בלוק הסביבה שכבר קיבל הילד. התהליך החדש מחזיק מצב משלו. לכן environment אינו ערוץ עדכון חי בין processes.

ירושת handles היא החלטת יכולת

בחרנו bInheritHandles שקר, ולכן איננו מבקשים ירושה כללית של handles שסומנו להורשה. בפרויקט supervisor נשתמש ברשימה מפורשת כשנרצה להעביר pipe או event לילד.

הורשה לא מתוכננת יכולה להשאיר קובץ, pipe או job חי גם אחרי שההורה סגר את העותק שלו. היא יכולה גם להעביר גישה למשאב שהילד לא היה אמור לקבל. זהו חלק מהחוזה של יצירת התהליך, ולא רק עניין של ניקוי.

זהות תהליך רחבה יותר מ־PID

ה־PID הוא מזהה נוח לאירועים, אך ניתן לשימוש חוזר לאחר סיום חיים. ה־process handle שחזר לנו נותן הפניה לאובייקט המסוים שנוצר ומאפשר המתנה ושאילתת exit status בהתאם לזכויותיו.

ברמת משתמש אפשר לחקור PEB ופרמטרי תהליך באמצעות symbols וכלים כמו WinDbg. ברמת קרנל אפשר לחקור EPROCESS של יעד מעבדה. אלה מבנים פנימיים שמשתנים; אפליקציה נתמכת משתמשת ב־APIs המתועדים לקבלת מידע.

יצירה רגילה משתמשת בהקשר האבטחה של התהליך הקורא לפי החוזה של CreateProcessW. התחזות של thread באמצעות impersonation אינה מבטיחה שהילד ייווצר עם אותו token מושאל. בפרק האבטחה נלמד את הפעולות שמיועדות במפורש ליצירה תחת זהות אחרת.

מוכנות דורשת הודעה

אם ההורה צריך לדעת שהילד סיים אתחול, הוא זקוק לפרוטוקול: event, pipe או הודעת readiness. המתנה קצרה עם Sleep אינה הוכחה, והצלחה של CreateProcess אינה אומרת שהילד כבר קיים מבחינת הפרוטוקול העסקי.

לקריאה נוספת: החוזה של CreateProcessW.