Но когда я пытаюсь сделать это из C#, дочерний процесс генерирует SIGSEGV между возвратом и вызовом exec. У меня была мысль, что, возможно, вызовы P/Invoke изначально генерировались как заглушки, и для разрешения заглушки требовалось нечто большее, чем неглубокая вилка, но даже если я сделаю фиктивный прогревочный вызов execvp перед вызовом forkpty (с пустым аргументом файла, чтобы он быстро возвращался с ошибкой), я все равно наблюдаю немедленный вызов SIGSEGV. В выводе strace я вижу, что forkpty выполняет работу по замене файловых дескрипторов stdin, stdout и stderr, среди прочего. Я вполне уверен, что SIGSEGV не происходит внутри самого forkpty. (Это также подтверждается обратной трассировкой, кратко упомянутой ниже.)
SIGSEGV также не запускается вызовом execvp; если я полностью закомментирую вызов forkpty, так что родительский вызов просто принесет себя в жертву непосредственно exec, тогда все будет происходить так, как и ожидалось. Процесс заменяется и запускается bash (хотя pty все еще находится в каноническом режиме
Код:
Код: Выделить всё
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 к дочернему элементу.
(Я пробовал настроить минидампы «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
Мобильная версия