Заводим Git for Windows под ReactOS

Всем доброго времени суток!

Меня зовут Станислав, и я люблю писать код. Это моя первая статья на Хабре, на написание которой меня сподвигло несколько факторов:


  • Недостаток статей технического плана в хабе ReactOS
  • Недавнее возвращение Geektimes на Хабр
  • Возможность собрать ReactOS в ReactOS
  • Довольно интересный случай исправления проблемы в ReactOS, в котором я принимал непосредственное участие

Позвольте представить вам виновников данного торжества (исправленного бага, который мешал запуску Git в ReactOS) — хорватский разработчик Hermès Bélusca-Maïto (далее просто Гермес, с ником hbelusca), и собственно я (с ником x86corez).

История начинается со следующих сообщений из IRC-канала разработчиков ReactOS:

Jun 03 18:52:56  Anybody want to work on some small problem? If so, can someone figure out why this problem https://jira.reactos.org/browse/CORE-12931 happens on ReactOS? :D
Jun 03 18:53:13  That would help having a good ROS self-hosting system with git support.
Jun 03 18:53:34  (the git assertion part only).


Разбор полётов

Поскольку на данный момент ReactOS нацелен на совместимость с Windows Server 2003, в качестве подопытного кролика был выбран Git версии 2.10.0 — последней, в которой заявлена поддержка Windows XP и 2003.

Тестирование осуществлялось в командной строке ReactOS, внешние симптомы проблемы были довольно неоднозначными. Например, при запуске git без указания дополнительных аргументов, он без каких-либо проблем выводил справочную информацию в консоль. Но стоило попробовать git clone или хотя бы git --version, в большинстве случаев консоль совсем ничего не выводила, либо изредка показывала битое сообщение с assertion:

git.exe clone -v "https://github.com/minoca/os.git" "C:\Documents and Settings\Administrator\Bureau\minocaos"

A s s e r t i o n   f a i l e d ! 

P r o g r a m :   C : \ P r o g r a m   F i l e s \ G i t \ m i n g w 3 2 \ b i n \ g i t . e x e 
F i l e :   e x e c _ c m d . c ,   L i n e   2 3 
E x p r e s s i o n :   a r g v 0 _ p a t h 

This application has requested the Runtime to terminate in an unusual way.
Please contact the application's support team for more information.

Исследованию проблемы очень поспособствовало, что клиент git — с открытым исходным кодом, и найти строку, на которой возникало исключение, не составило большого труда: https://github.com/git-for-windows/git/blob/4cde6287b84b8f4c5ccb4062617851a2f3d7fc78/exec_cmd.c#L23

char *system_path(const char *path)
{
    /* вырезано */

#ifdef RUNTIME_PREFIX
    assert(argv0_path); // проблема возникает здесь
    assert(is_absolute_path(argv0_path));

    /* вырезано */
#endif

    strbuf_addf(&d, "%s/%s", prefix, path);
    return strbuf_detach(&d, NULL);
}

А значение переменной argv0_path задавалось данным участком кода:

const char *git_extract_argv0_path(const char *argv0)
{
    const char *slash;

    if (!argv0 || !*argv0)
        return NULL;

    slash = find_last_dir_sep(argv0);

    if (slash) {
        argv0_path = xstrndup(argv0, slash - argv0);
        return slash + 1;
    }

    return argv0;
}

После выяснения этих деталей я отписался на IRC-канале:

Jun 03 19:04:36  hbelusca: https://github.com/git-for-windows/git/blob/4cde6287b84b8f4c5ccb4062617851a2f3d7fc78/exec_cmd.c#L23
Jun 03 19:04:41  assertion is here
Jun 03 19:04:57  yes I know, I've seen the code yesterday. The question is why it's FALSE on ROS but TRUE on Windows.
Jun 03 19:06:02  argv0_path = xstrndup(argv0, slash - argv0);
Jun 03 19:06:22  xstrndup returns NULL %-)
Jun 03 19:06:44  ok, so what's the values of argv0 and slash on windows vs. on ROS? :P
Jun 03 19:08:48  good question!

Имя переменной как бы намекает, что значение было получено из argv[0] — обычно в нулевом индексе этого массива содержится строка с именем команды, которой была вызвана текущая программа. Но в дальнейшем всё оказалось не столь очевидно…

Jun 03 20:15:21  hbelusca: surprise... git uses its own xstrndup implementation
Jun 03 20:15:35  so I can't simply hook it xD
Jun 03 20:15:56  well, with such a name "xstrndup" it's not surprising it's its own implementation
Jun 03 20:16:04  probably I would need an user-mode debugger... like OllyDbg
Jun 03 20:16:09  that's everything but standardized function.
Jun 03 20:16:24  x86corez: ollydbg should work on ROS.
Jun 03 20:16:30  what are you breaking today?
Jun 03 20:16:44  mjansen: https://jira.reactos.org/browse/CORE-12931
Jun 03 20:16:51  (of course if you also are able to compile that git with symbols and all the stuff, it would be very nice)


Переход к активным действиям

После этого я принял решение собрать git из исходников, чтобы было проще «на лету» выводить значения интересующих переменных прямо в консоль. Мной был выбран данный пошаговый мануал, и для сборки совместимой версии я выбрал эту ветку: https://github.com/git-for-windows/git/tree/shears/v2.10.0-rc2

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

image

Путём проб и ошибок, а также пользуясь подсказками из зала (IRC-канала) все проблемы компиляции удалось решить. Если кто-то захочет повторить мой путь, делюсь готовым диффом для успешной сборки: https://pastebin.com/ZiA9MaKt

Дабы исключить влияние множества функций при инициализации, я решил вывести несколько отладочных сообщений прямо в начале функции main(), которая в случае git расположена в файле common-main.c:

int main(int argc, const char **argv)
{
    /*
     * Always open file descriptors 0/1/2 to avoid clobbering files
     * in die().  It also avoids messing up when the pipes are dup'ed
     * onto stdin/stdout/stderr in the child processes we spawn.
     */
    //DebugBreak();
    printf("sanitize_stdfds(); 1\n");
    sanitize_stdfds();

    printf("git_setup_gettext(); 1\n");
    git_setup_gettext();

    /*
     * Always open file descriptors 0/1/2 to avoid clobbering files
     * in die().  It also avoids messing up when the pipes are dup'ed
     * onto stdin/stdout/stderr in the child processes we spawn.
     */
    printf("sanitize_stdfds(); 2\n");
    sanitize_stdfds();

    printf("git_setup_gettext(); 2\n");
    git_setup_gettext();

    printf("before argv[0] = %s\n", argv[0]);
    argv[0] = git_extract_argv0_path(argv[0]);
    printf("after argv[0] = %s\n", argv[0]);

    restore_sigpipe_to_default();
    printf("restore_sigpipe_to_default(); done\n");

    return cmd_main(argc, argv);
}

Вывод получился следующий:

C:\>git --version
sanitize_stdfds(); 1
git_setup_gettext(); 1
sanitize_stdfds(); 2
git_setup_gettext(); 2
before argv[0] = git
after argv[0] = git
restore_sigpipe_to_default(); done

A s s e r t i o n   f a i l e d ! 
(часть вырезана, совпадает с ранее приведённой)

Казалось бы, всё нормально, argv[0] таким и должен быть. Пришла идея погонять git внутри отладчика, например в OllyDbg, но что-то пошло не так…

Jun 04 01:54:46  now please try gdb/ollydbg in ROS
Jun 04 01:58:11  you have gdb in RosBE
Jun 04 01:58:20  just in case :p
Jun 04 01:59:45  ollydbg says "nope" with MEMORY_MANAGEMENT bsod
Jun 04 02:00:07  !bc 0x0000001A
Jun 04 02:00:08  KeBugCheck( MEMORY_MANAGEMENT );
Jun 04 02:00:13  :/
Jun 04 02:00:49  welp
Jun 04 02:00:56  you only have one option now :D

И вот тут sanchaez подсказал отличную идею, которая пролила свет на многое!

image

Исключения больше не возникало, и git успешно печатал свой номер версии.

Jun 04 02:23:40  it prints!
Jun 04 02:23:44  but only in gdb
Jun 04 02:23:53  oh
Jun 04 02:24:00  C:\git/git.exe
Jun 04 02:24:13  I wonder whether it's the same in windows, or not.

Дело сдвинулось с мёртвой точки, и я решил попробовать по-разному запускать git в командной строке, и не прогадал!

image

Проблема явно была в том, что git ожидал полный путь в командной строке. Тогда я решил проверить, какие данные будут выведены в Windows. Результаты меня немного удивили.

image

В переменной argv[0] почему-то был полный путь к приложению.

Jun 05 23:01:44  x86corez: can you try to run git also by not using cmd.exe?
Jun 05 23:02:05  (to exclude the possibility it's cmd that doesn't call Createprocess with a complete path)
Jun 05 23:02:09  while I think it should...
Jun 05 23:02:30  not using cmd... moment
Jun 05 23:02:55  x86corez: alternatively, on windows, try starting git using our own cmd.exe :)

Гермес предложил проверить, может быть проблема где-то в командной строке ReactOS…

image

Но это оказалось не так. Вариант с оболочкой cmd отпадает.

Jun 05 23:04:38  ROS cmd is not guilty
Jun 05 23:07:57  If there was a possibility to consult the received path, before looking at the contents of argvs... ?
Jun 05 23:08:30  dump contents of actual command line?
Jun 05 23:08:39  yeah
Jun 05 23:09:39  The thing you retrieve using GetCommandLineW
Jun 05 23:10:03  (which is, after simplifications, basically : NtCurrentPeb()->ProcessParameters->CommandLine )
Jun 05 23:10:59  Also I was thinking it could be a side-effect of having (or not having) git path into the env-vars....
Jun 05 23:12:17  hbelusca, command line is "git  --version"
Jun 05 23:12:34  Always?
Jun 05 23:12:39  Yes, even on Windows
Jun 05 23:15:13  ok but then it would be nice if these different results are at least the same on Windows and on ROS, so that we can 100% exclude problems outside of msvcrt.

Теперь оставалось лишь протестировать библиотеку msvcrt.dll из ReactOS под Windows. Я попробовал положить файл в той же директории, где лежал git.exe, но это не помогло. Марк посоветовал способ с файлом .local:

Jun 05 22:59:01  x86corez: add .local file next to msvcrt.dll ;)
Jun 05 22:59:47  exename.exe.local
Jun 05 23:00:17  just an empty file?
Jun 05 23:00:21  yea
Jun 05 23:00:49  mjansen: do we support these .local files?
Jun 05 23:00:52  we dont
Jun 05 23:00:54  windows does
Jun 05 23:15:48  moment... I'll try with .local
Jun 05 23:18:43  mjansen: I've created git.exe.local but it still doesn't load msvcrt.dll in this directory

Но и этот вариант почему-то тоже не сработал. Возможно сказался тот факт, что все эксперименты я проводил на серверной версии Windows Server 2008 R2.

Последнюю идею предложил Гермес:

Jun 05 23:19:28  last solution: patch "msvcrt" name within git and perhaps other mingwe dlls ^^
Jun 05 23:20:12  good idea about patching!

При помощи WinHex я заменил все вхождения подстроки msvcrt в файле git.exe на msvcrd, и переименовал msvcrt.dll от ReactOS соответствующим образом, и вот что получилось:

image

Jun 05 23:23:29  Yes! guilty is msvcrt :)
Jun 05 23:25:37  ah, so as soon as git uses our msvcrt we get the problem on windows.
Jun 05 23:25:38  hbelusca, mjansen, https://image.prntscr.com/image/FoOWnrQ4SOGMD-66DLW16Q.png
Jun 05 23:25:58  aha and it asserts <3
Jun 05 23:26:03  (it shows the assertion now)

Теперь уже в Windows мы получили то же самое сообщение об ошибке! А это означает, что источник проблемы в реализации одной из функций msvcrt из ReactOS.

Можно также отметить, что под Windows текст исключения отображается корректно.

Jun 05 23:26:13  but it prints text and correctly.
Jun 05 23:26:20  oh
Jun 05 23:26:33  and on ROS it doesn't print in most cases xD
Jun 05 23:26:38  so also it excludes another hypothesis, namely that it could have been a bug in our msvcrt/crt
Jun 05 23:26:56  So possibly a strange bug in our console

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

Jun 06 00:07:43  https://msdn.microsoft.com/en-us/library/tza1y5f7.aspx
Jun 06 00:07:57  When a program is run from the command interpreter (Cmd.exe), _pgmptr is automatically initialized to the full path of the executable file.
Jun 06 00:08:01  this ^^)
Jun 06 00:08:50  That's what GetModuleFileName does.
Jun 06 00:09:04  yeah
Jun 06 00:10:30  Of course in ROS msvcrt we don't do this, but instead we initialize pgmptr to what argv[0] could be.
Jun 06 00:11:08  That's one thing.
Jun 06 00:11:34  The other thing is that nowhere it appears (in MS CRT from VS, or in wine) that argv is initialized using pgmptr.
Jun 06 00:13:33  hbelusca, I've checked argv[0] in some ROS command line tools, running them in Windows
Jun 06 00:13:56  they all interpret argv[0] as command line, not full path
Jun 06 00:14:04  so... I think it's git specific behaviour
Jun 06 00:14:16  or specific mingw compiler settings
Jun 06 00:28:12  x86corez: I'm making a patch for our msvcrt, would be nice if you could test it :)
Jun 06 00:28:21  I'll test it

Гермес прислал ссылку на патч, я вручную применил его и пересобрал систему, после чего всё магическим образом заработало как надо!

image

Jun 06 00:34:26  hbelusca, IT WORKS!
Jun 06 00:35:10  L O L
Jun 06 00:35:18  So it seems that something uses pgmptr to rebuild an argv.
Jun 06 00:35:52  I've even able to clone :)
Jun 06 00:36:19  \o/
Jun 06 00:36:21  2.10.0-rc2? not the release?
Jun 06 00:36:24  ok I'm gonna commit that stuff.
Jun 06 00:36:43  x86corez: gonna have ROS self-hosting <33
Jun 06 00:36:48  yeah!
Jun 06 00:37:01  gigaherz: I've built that from sources
Jun 06 00:37:37  oh, for testing this bug? o_O
Jun 06 00:37:50  yes, you missed the fun :p
Jun 06 00:39:46  git 2.10.0-windows.1 (release) works too!
Jun 06 00:39:54  commit!!!


Послесловие

Таким образом, ещё один баг, косвенно препятствующий само-сборке ReactOS внутри ReactOS, был исправлен благодаря коллективным усилиям. Забавным совпадением является тот факт, что незадолго до этого был исправлен другой баг в том же msvcrt (а именно в функции qsort), который не позволял собрать драйверы USB в ReactOS.

Я участвую в разработке многих проектов, написанных на разных языках программирования, как закрытых, так и с открытым исходным кодом. С проектом ReactOS я сотрудничаю с 2014 года, но активно помогать и писать код начал только в 2017. Работать в этой области особенно интересно, потому что это целая операционная система! Чувствуется огромный масштаб результата, в который были вложены усилия, а также приятное ощущение, что одним багом стало меньше! :)

Кто-то наверняка задастся вопросом, почему я помогаю именно ReactOS, а не Linux например. Так исторически сложилось, что в большинстве случаев я пишу программы для Windows, а мой любимый язык программирования — Delphi. Возможно именно поэтому архитектура Windows NT вместе с Win32 API мне очень интересна, а проект свободной операционной системы ReactOS воплощает в реальность давнюю мечту — на практике позволяет узнать, как всё это устроено изнутри.

Надеюсь, вам было интересно прочитать мою первую статью здесь. С нетерпением жду ваших комментариев!


Ссылки


© Habrahabr.ru