Даже при вызове exec сразу после разветвления дочерний процесс завершается с помощью SIGSEGV.C#

Место общения программистов C#
Anonymous
Даже при вызове exec сразу после разветвления дочерний процесс завершается с помощью SIGSEGV.

Сообщение Anonymous »

Я пытаюсь создать эмулятор терминала на C# (.NET 10.0), который может работать в Linux (в данном случае kubuntu 25.10). Таким образом, я должен использовать API openpty для получения пары FD главный/ведомый PTY, а затем, после выполнения разветвления, скопировать их в слоты STDIN_FILENO, STDOUT_FILENO и STDERR_FILENO перед выполнением нужной программы для дочернего процесса. После некоторого поиска я обнаружил функцию forkpty, которая выполняет за вас большую часть этой работы, так что все, что вам нужно сделать после ее возврата в дочернем процессе, — это exec.
Но когда я пытаюсь сделать это из C#, дочерний процесс генерирует SIGSEGV между возвратом и вызовом exec. У меня была мысль, что, возможно, вызовы P/Invoke изначально генерировались как заглушки, и для разрешения заглушки требовалось нечто большее, чем неглубокая вилка, но даже если я сделаю фиктивный прогревочный вызов execvp перед вызовом forkpty (с пустым аргументом файла, чтобы он быстро возвращался с ошибкой), я все равно наблюдаю немедленный вызов SIGSEGV. В выводе strace я вижу, что forkpty выполняет работу по замене файловых дескрипторов stdin, stdout и stderr, среди прочего. Я вполне уверен, что SIGSEGV не происходит внутри самого forkpty. (Это также подтверждается обратной трассировкой, кратко упомянутой ниже.)
SIGSEGV также не запускается вызовом execvp; если я полностью закомментирую вызов forkpty, так что родительский вызов просто принесет себя в жертву непосредственно exec, тогда все будет происходить так, как и ожидалось. Процесс заменяется и запускается bash (хотя pty все еще находится в каноническом режиме :-P).
Код:

Код: Выделить всё

    var term = new termios() { ... /* a bunch of flags */ };
var win = new winsize() { ... /* 80x25 character, 640x400 pixel */ };

string filePath = "/usr/bin/bash";

nint fileNamePtr = Marshal.StringToHGlobalAnsi(filePath);

nint argvPtr = Marshal.AllocHGlobal(2 * nint.Size);

Marshal.WriteIntPtr(argvPtr, 0, Marshal.StringToHGlobalAnsi(Path.GetFileName(filePath)));
Marshal.WriteIntPtr(argvPtr, nint.Size, 0);

nint emptyString = argvPtr + nint.Size; // we just wrote a 0 to this address

int result = execvp(emptyString, 0);
Console.WriteLine("Warm-up result: {0}", result);

sleep(0); // also warmup

int childPID = forkpty(
out int masterFD,
name: null,
ref term,
ref win);

if (childPID == 0)
{
// to verify whether child code is running -- SIGSEGV happens before this sleep could have returned
//sleep(15);

execvp(fileNamePtr, argvPtr);
}

Console.WriteLine("Child PID is: {0}", childPID);
При запуске:
  • Он выводит: Результат прогрева: -1, что указывает на то, что вызов execvp сработал (и не удалось из-за намеренной передачи пустой строки для файла)
  • Родительский процесс выводит: Дочерний PID: 12345
  • Но вывод strace показывает, что дочерний процесс уже получил SIGSEGV перед этим последним выводом.
  • Ошибка сегментации происходит до того, как код попадает в тело оператора if. Чтобы исключить исключение, возникающее в execvp, я попытался добавить сон, но SIGSEGV по-прежнему происходит сразу после возврата fork к дочернему элементу.
Я могу записать дамп ядра, но это не кажется очень полезным, поскольку изображение, которое выполняется при его захвате, представляет собой вывод JIT. Если я загружаю дамп ядра в GDB, обратная трассировка представляет собой просто последовательность адресов памяти, которые на самом деле нигде не существуют. Следует отметить, что ни forkpty, ни execvp не включены в обратную трассировку.
(Я пробовал настроить минидампы «COMPlus» на основе найденных мной ссылок, но, похоже, это ничего не дало. Я нашел одно сообщение, в котором указывалось, что запись 0xff в /proc/self/coredump_filter приведет к тому, что сгенерированные системой дампы будут включать все, но хотя это привело к увеличению более чем вдвое размер файла дампа ядра, GDB по-прежнему не видит никакого кода.)
Я пытался получить выходные данные ассемблера из JIT, но все, что я вижу, это вызов по одному адресу в том месте, где forkpty находится в коде, за которым немедленно следует настройка параметров для execvp и еще один вызов. Насколько я понимаю, эти вызовы относятся к батутам, которые разрешаются по требованию, но после выполнения одного вызова батут заменяется прямым вызовом фактической целевой функции. (Это понимание может быть неправильным или неполным, но я не знаю, как его проверить.) Именно на этой стратегии своевременного разрешения я пытался выполнить фиктивные прогревающие вызовы к конечным точкам P/Invoke перед разветвлением, но безрезультатно.
Что может произойти? Как я могу отладить это дальше?
Я могу очистить свое тестовое приложение и добавить его в репозиторий, если это будет полезно.

Вернуться в «C#»