Но когда я пытаюсь сделать это из 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
ETA 2: Я узнал, как заставить GDB следовать за дочерним процессом в разветвлении, и смог шаг за шагом отладить дочерний процесс. Похоже, что после вызова P/Invoke требуется значительная обработка, прежде чем он фактически вернется к вызывающей стороне. Я заметил одну вещь: вызов tls_get_addr. Интересно, нарушается ли TLS при разветвлении, поскольку меняется идентичность потока, но если TLS — это просто общий диапазон адресов, который по-разному отображается в разных потоках, этого не должно происходить.
Только что во время сеанса отладки я останавливал инструкцию за инструкцией и остановился на этой инструкции:
Код: Выделить всё
(gdb) x/20i 0x00007fff78f341f0
=> 0x7fff78f341f0: push %rbp
0x7fff78f341f1: sub $0x150,%rsp
0x7fff78f341f8: lea 0x150(%rsp),%rbp
0x7fff78f34200: vxorps %xmm8,%xmm8,%xmm8
0x7fff78f34205: vmovdqu32 %zmm8,-0x140(%rbp)
0x7fff78f3420c: vmovdqu32 %zmm8,-0x100(%rbp)
0x7fff78f34213: vmovdqu32 %zmm8,-0xc0(%rbp)
0x7fff78f3421a: vmovdqu32 %zmm8,-0x80(%rbp)
0x7fff78f34221: vmovdqa %xmm8,-0x40(%rbp)
0x7fff78f34226: xor %eax,%eax
0x7fff78f34228: mov %rax,-0x30(%rbp)
0x7fff78f3422c: mov $0xc857ba0e,%eax
0x7fff78f34231: mov %rax,-0x150(%rbp)
0x7fff78f34238: mov %rdi,-0x8(%rbp)
0x7fff78f3423c: mov %rsi,-0x10(%rbp)
0x7fff78f34240: mov %edx,-0x14(%rbp)
0x7fff78f34243: mov %rcx,-0x20(%rbp)
0x7fff78f34247: xor %eax,%eax
0x7fff78f34249: mov %rax,-0x30(%rbp)
0x7fff78f3424d: nop
(gdb)
Код: Выделить всё
(gdb) ni
Thread 3.1 "THE THING" received signal SIGSEGV, Segmentation fault.
0x00007fff7829fb9c in ?? ()
=> 0x00007fff7829fb9c: ff 10 call *(%rax)
(gdb) bt
#0 0x00007fff7829fb9c in ?? ()
#1 0x00007feffbffe630 in ?? ()
#2 0x00007feffbffe889 in ?? ()
#3 0x00007feffbffe600 in ?? ()
#4 0x00007feffbffe640 in ?? ()
#5 0x00007feffbffe8b8 in ?? ()
#6 0x0000000000000001 in ?? ()
#7 0x00007feffbffe7f8 in ?? ()
#8 0x0000000000000000 in ?? ()
(gdb)
В любом случае, насколько я могу судить, это весь код, который выполняется между возвратом фактической функции forkpty и возвратом вызова батута P/Invoke в моем коде C#. Если нет какого-то волшебного трюка, о котором я не знаю, я думаю, это может указывать на то, что с fork буквально невозможно работать из кода C#.
ETA 3: Отключив сигналы перед разветвлением, я думаю, что добился прогресса. Я по-прежнему получаю SIGSEGV, но теперь он находится в коде загруженной библиотеки, а не в выходном коде JIT.
Код: Выделить всё
Thread 3.1 "THE THING" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7feffbfff6c0 (LWP 3390396)]
0x00007ffff7406790 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
=> 0x00007ffff7406790: 44 0f b6 04 17 movzbl (%rdi,%rdx,1),%r8d
(gdb) bt
#0 0x00007ffff7406790 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#1 0x00007ffff73c54cc in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#2 0x00007ffff73decb5 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#3 0x00007ffff73c5352 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#4 0x00007ffff47de83a in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#5 0x00007ffff47d8fe7 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#6 0x00007ffff47d70ca in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#7 0x00007ffff47e470b in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#8 0x00007ffff4836a6c in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#9 0x00007ffff46de852 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#10 0x00007ffff46e0eab in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#11 0x00007ffff46dfd31 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#12 0x00007ffff46e1c7b in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#13 0x00007ffff46e6288 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libclrjit.so
#14 0x00007ffff73cc04a in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#15 0x00007ffff7409f6e in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#16 0x00007ffff740981f in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#17 0x00007ffff7408f56 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#18 0x00007ffff7408a06 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#19 0x00007ffff7371664 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#20 0x00007ffff740c4cd in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#21 0x00007ffff740bd52 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#22 0x00007ffff7619d04 in ?? () from /usr/lib/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.so
#23 0x00007fff78f77bbf in ?? ()
#24 0x0000000000000000 in ?? ()
(gdb)