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

Ответить
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 перед разветвлением, но безрезультатно.
Что может произойти? Как я могу отладить это дальше?
Я могу очистить свое тестовое приложение и добавить его в репозиторий, если это будет полезно.
ETA: вот дизассемблирование, полученное с помощью Sharplab.io с целью x64:

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

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

L0285: lea r8, [rbp-0x30]   ; parameter 2 loaded from a local
L0289: lea rcx, [rbp-0x60]  ; parameter 0 loaded from a local
L028d: lea r9, [rbp-0x40]   ; parameter 3 loaded from a local
L0291: xor edx, edx         ; parameter 1 == NULL
L0293: call 0x00007ffe91060030 ; call to forkpty P/Invoke trampoline

L0298: mov [rbp-0xe8], eax
L029e: mov eax, [rbp-0xe8]  ; unclear to me why this line exists

L02a4: mov [rbp-0x54], eax  ; stashing return value in childPID

if (childPID == 0)
{

L02a7: cmp dword ptr [rbp-0x54], 0  ; set flags based on value of childPID
L02ab: sete al                      ; AL = 1 iff childPID == 0
L02ae: movzx eax, al                ; expand AL out to the full register
L02b1: mov [rbp-0x64], eax          ; stash in a temporary location
L02b4: cmp dword ptr [rbp-0x64], 0  ; check value of: (childPID == 0)
L02b8: je short L02ce               ; it was zero? skip the if block

execvp(fileNamePtr, argvPtr);

L02ba: mov rcx, [rbp-0x48]     ; parameter 0 loaded from local
L02be: mov rdx, [rbp-0x50]     ; parameter 1 loaded from local
L02c2: call 0x00007ffe91060048 ; call to execvp P/Invoke trampoline
Возможно, я что-то упускаю, но не понимаю, как это может вызвать SIGSEGV. Должно быть, это что-то вне моего контроля, либо в эпилоге первого вызова P/Invoke, либо в настройке второго??
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

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