Я преследовал причину прерывистого сбоя в одной из наших служб.NET из-за внутренней ошибки в.NET Runtime (код выхода 0x80131506). Услуга, о которой идет речь, не выполняет какие-либо виды операций, которые обычно виноваты в таких ошибках (небезопасный код, PInvoke и т.д.). Я попытался отключить параллельный GC, как описано в KB2679415, а также переключиться на сервер GC, но прерывистые сбои сохраняются. Проблема проявляется в.NET 4.7.2 и более ранних версиях при компиляции в режиме отладки.
Служба широко использует старую версию NHibernate (2.0.1), и когда я рассматривал аварийные дампы в отладчике, всегда есть код NHibernate в стоп-косте при возникновении ошибки, хотя сам NHibernate - это все управляемый код, поэтому не должно быть способно вызвать такой крах.
Мне удалось воспроизвести сбой при отладчике, и с включенным журналом стресса GC и проверкой кучи, и, хотя он, похоже, указывает на проблему в JIT/GC, я не уверен, что правильно интерпретирую вывод.
Глядя на поток, на котором происходит сбой, в этом случае он встречается в clr!JIT_Stelem_Ref
:
clr!JIT_Stelem_Ref+0x18: cmp r9,qword ptr [r8] ds:aaaaaaaa'aaaaaaaa=????????????????
В этом случае строка 0xaa
, по-видимому, является результатом включения HeapVerify, что приводит к заполнению собранных областей памяти GC, по-видимому, для облегчения идентификации, и предполагает, что каким-то образом мы все еще имеем ссылку на старое местоположение собранного/перемещенного объекта,
Отслеживание назад в стеке, есть много записей 0xaaaaaaaaaaaaaaaa
, однако они перестают появляться в методе, который находился в верхней части стека вызовов, когда произошел самый последний GC, который в этом случае был NHibernate.Loader.Loader.GetRow()
в соответствии с журналом напряжений GC для последнего GC на этой теме:
(Примечание: я изменил порядок !dumplog
строк из вывода SOS ' !dumplog
для упрощения чтения):
2404 12445.672380360 : 'GC'GCROOTS' Starting scan of Thread 000000001EF4DED0 ID = 20 {
2404 12445.672380963 : 'GCROOTS' Scanning ExplicitFrame 000000001E6ED3B8 AssocMethod = 0000000000000000 frameVTable = 000007FEF365B640 (clr!RedirectedThreadFrame::'vftable')
2404 12445.672386397 : 'GCROOTS' Scanning Frameless method 000007FE93F43460 (NHibernate.Loader.Loader.GetRow(System.Data.IDataReader, NHibernate.Persister.Entity.ILoadable[], NHibernate.Engine.EntityKey[], System.Object, NHibernate.Engine.EntityKey, NHibernate.LockMode[], System.Collections.IList, NHibernate.Engine.ISessionImplementor)) ControlPC = 000007FE945E3095
2404 12445.672388208 : 'GC'GCROOTS' GC Root 000000001E6ED4C0 RELOCATED 000000003B1A7708 -> 000000003AC89F08 MT = 000007FE93DDF5C8 (...)
2404 12445.672388510 : 'GC'GCROOTS' GC Root 000000001E6ED4D8 RELOCATED 000000003B1A73A0 -> 000000003AC89D00 MT = 000007FEF1FD6EA8 (System.Object[])
2404 12445.672388510 : 'GC'GCROOTS' GC Root 000000001E6ED4E8 RELOCATED 000000003B1A7358 -> 000000003AC89CB8 MT = 000007FE9491D7C8 (NHibernate.Engine.EntityKey)
2404 12445.672388510 : 'GC'GCROOTS' GC Root 000000001E6ED4F8 RELOCATED 000000003B1A73A0 -> 000000003AC89D00 MT = 000007FEF1FD6EA8 (System.Object[])
Область стека для этого метода выглядит следующим образом:
00000000'1e6ed470 000000003b1a7358 ✕
00000000'1e6ed478 000000000291e3d0
00000000'1e6ed480 0000000000000000
00000000'1e6ed488 0000000000000000
00000000'1e6ed490 000000000662a900
00000000'1e6ed498 0000000006523c80
00000000'1e6ed4a0 0000000000000000
00000000'1e6ed4a8 0000000000000000
00000000'1e6ed4b0 0000000000000000
00000000'1e6ed4b8 0000000000000000
00000000'1e6ed4c0 000000003ac89f08 ✔
00000000'1e6ed4c8 0000000000000000
00000000'1e6ed4d0 0000000006524248
00000000'1e6ed4d8 000000003ac89d00 ✔
00000000'1e6ed4e0 0000000000000000
00000000'1e6ed4e8 000000003ac89cb8 ✔
00000000'1e6ed4f0 0000000000000000
00000000'1e6ed4f8 000000003ac89d00 ✔
00000000'1e6ed500 0000000100000000
00000000'1e6ed508 0000000c0000000b
00000000'1e6ed510 0000000006621660
00000000'1e6ed518 000000001e6ed690
00000000'1e6ed520 000000001e6ed6a0
Я указал 4 записи, упомянутые в журнале стресса GC, как перенесенные, которые были правильно обновлены с их новыми адресами, однако первая запись стека (000000003b1a7358
- NHibernate.Engine.EntityKey
), хотя она является одним из перемещенных объектов, не обновляется с новым адресом. Конечно, это было бы совершенно нормально, если бы это больше не использовалось, но на самом деле оно должно передаваться как параметр для вызова NHibernate.Loader.Loader.InstanceNotYetLoaded()
.
InstanceNotYetLoaded()
принимает 9 параметров (плюс this
), и я отметил, где каждая из них загружается в стек/регистр в следующем списке сборок. Я также включил соответствующий вывод из SOS ' !gcinfo
поскольку он относится к каждому из параметров в стеке:
Param Address Instruction GC Info
000007fe'945e3071 mov r9,qword ptr [rbp-38h]
P4> 000007fe'945e3075 mov qword ptr [rsp+20h],r9
000007fe'945e307a mov r9d,dword ptr [rbp-18h] +sp+20
000007fe'945e307e mov rcx,qword ptr [rbp+40h]
000007fe'945e3082 cmp r9,qword ptr [rcx+8]
000007fe'945e3086 jb 000007fe'945e308d
000007fe'945e3088 call clr!JIT_RngChkFail
000007fe'945e308d lea rcx,[rcx+r9*8+10h] -sp+20
000007fe'945e3092 mov r9,qword ptr [rcx]
-- GC Occurred Here --
P5> 000007fe'945e3095 mov qword ptr [rsp+28h],r9
000007fe'945e309a mov r9,qword ptr [rbp+38h] +sp+28
P6> 000007fe'945e309e mov qword ptr [rsp+30h],r9
000007fe'945e30a3 mov r9,qword ptr [rbp+30h] +sp+30
P7> 000007fe'945e30a7 mov qword ptr [rsp+38h],r9
000007fe'945e30ac mov r9,qword ptr [rbp+48h] +sp+38
P8> 000007fe'945e30b0 mov qword ptr [rsp+40h],r9
000007fe'945e30b5 mov r9,qword ptr [rbp+50h] +sp+40
P9> 000007fe'945e30b9 mov qword ptr [rsp+48h],r9
000007fe'945e30be mov r9d,dword ptr [rbp-18h] +sp+48
000007fe'945e30c2 mov rcx,qword ptr [rbp+20h]
000007fe'945e30c6 cmp r9,qword ptr [rcx+8]
000007fe'945e30ca jb 000007fe'945e30d1
000007fe'945e30cc call clr!JIT_RngChkFail
000007fe'945e30d1 lea rcx,[rcx+r9*8+10h] -sp+48 -sp+40 -sp+38 -sp+30 -sp+28
P3> 000007fe'945e30d6 mov r9,qword ptr [rcx]
this> 000007fe'945e30d9 mov rcx,qword ptr [rbp+10h]
P1> 000007fe'945e30dd mov rdx,qword ptr [rbp+18h]
P2> 000007fe'945e30e1 mov r8d,dword ptr [rbp-18h]
000007fe'945e30e5 call InstanceNotYetLoaded(...)
GC непосредственно перед 000007fe945e3095
произошел в 000007fe945e3095
, который после загрузки параметра 4 в стек (на 000007fe945e3075
), но также после того, как эта запись стека стала мертвой (в 000007fe945e308d
) в соответствии с информацией GC, которая объясняет, почему Фаза перераспределения GC не обновляла эту ссылку.
Похоже, что GC Info для параметров 5-9 также неправильно маркирует их как слишком ранние, и, возможно, в обоих случаях они помечены как мертвые сразу после того, что выглядит как проверка диапазона массива.
Это для меня выглядит как ошибка JIT, когда время жизни этих параметров стека неправильно отслеживается. Правильно ли этот анализ, если да, где лучше всего сообщить, если. Если это не ошибка JIT, что мне не хватает, что может объяснить эти неожиданные сбои на чисто управляемом коде?
Редактировать:
Я считаю, что следующий фрагмент воспроизведет проблему, по крайней мере, до создания плохой информации GC в режиме отладки.
public void Repro(int p1, object p2, object p3, object p4, object[] p5)
{
// Incorrect GC Info generated for this call
ReproHelper(p1, p2, p3, p4, p5[p1]);
}
public void ReproHelper(int p1, object p2, object p3, object p4, object p5)
{
Console.WriteLine(p1);
Console.WriteLine(p2);
Console.WriteLine(p3);
Console.WriteLine(p4);
Console.WriteLine(p5);
}
В сущности, должен быть метод вызова метода, который:
- Требуется не менее двух параметров, которые должны быть переданы в стек (т.е. не менее 5 параметров для метода экземпляра).
- 2- й параметр, переданный в стек (параметр 5), должен быть результатом доступа к массиву.
Когда эти критерии соблюдены, 4- й параметр загружается в стек для вызова, а запись стека правильно помечена как содержащая ссылку. Однако при определении значения параметра 5 выполняется проверка диапазона диапазона массива, и после этого запись стека для параметра 4 помечена как мертвая.
Если GC-GC происходит после проверки диапазона, но до фактического вызова, и GC дает объект, который передавался как перемещаемый параметр 4, когда метод возобновляется, вызов передает старый (недопустимый) адрес в параметр 4, а не новый.