Дело, конечно, не такое интересное, как у Руссиновича, но, надеюсь, будет полезно некотроым разработчикам. Основная цель изложения — показать средства, с помощью которых мы можем анализировать поведение программы на самом низком уровне.
Итак, имеется приложение, написанное на C#, которое использует .net framework 1.1 (да-да). Приложение после некоторых внесенных изменений начало выбрасывать такое малоинформативное исключение:
InitEdit — функция формы в приложении, которая инициализирует данные. Строка исходного кода приложения, на которой оно выбрасывается:
Код функции ResetIndexes, который показывает нам Рефлектор, не сказать чтобы уж очень сложный:
Вроде как проблем нет. Функция пробегает по индексам и вызывает у каждого Reset. Где может быть исключение? SetShadowIndexes просто вызывает "this.shadowIndexes = this.LiveIndexes;" Значит, на момент исполнения строки №9 shadowIndexes ненулевое. Значит, резонно, как мне кажется, предположить, что где-то внутри этого массива находится null элемент, и, соответственно падает на строке 12. Думаю, проблема в этом, сказал я себе и принялся за работу.
Запускаем windbg, соединяемся с процессом, загружаем в дебагер Son of Strike (sos.dll). Думаю, все знают, что это такое, и нужды объяснять нет.
Ставим точку остановки на нашей функции:
(Мне нужна только вторая функция, поэтому отключаем первую точку командой windbg: bd 0).
Нельзя поставить точку остановки прямо на ResetIndexes, так как приложение довольно интенсивно использует датасеты и нам придется пропустить много ложных вызовов.
Поэтому точки надо расставлять последовательно, по мере вызова нужных функций.
После того, как дебаггер останавливает на InitEdit, ставим точку на DataSet.Merge:
(опять же SoS выставит остановку на все перегруженные функции Merge, поэтому избавляемся от не ненужных)
После DataSet.Merge нужно зайти в System.Data.Merger.MergeTable, откуда и вызывается DataTable.ResetIndexes.
Проверяем, что аргумент действительно тот, который мы ожидаем:
Cмотрим на имя таблицы, которая передана как параметр.
Имя таблицы записано в поле со смещением 0x38 от начала объекта.
TABLE_USERS — не та таблица, что нам нужна, поэтому идем дальше, пока не встретим
Только после этого добавляем точку остановки в ResetIndexes, запускаем программу дальше, и опять останавливаемся.
this передается в ecx, берем ее и смотрим что лежит в поле indexes:
Пока что все правильно. На момент входа в ResetIndexes поле indexes равно нулю, затем устанавливается с помощью SetShadowIndexes.
Мда… моя основная теория распадается на глазах. Нет там нулевых элементов. Вот незадача. Что ж, продолжим выполнение кода, и посмотрим на исключения изнутри windbg. Отпускаем программу. Когда происходит исключение, отладчик останавливается:
Выполняем команду !analyze -v, результат которой невероятно напичкан разного рода данными о произошедшем исключении. Привожу сжатом виде:
Cтек вызовов уже мы видели, правда в несколько сокращенном варианте. А вот по коду сейчас будем разбираться. Давайте взглянем на код, который вызвал исключение:
Читается значение из памяти по адресу, который содержится в ECX, а там (в ECX), как мы видим, ноль…
Давайте теперь посмотрим на функцию ResetIndexes, превращенную заботливым jit-компилятором в исполняемый код для процессора x86. Я повторю код функции на C#, и мы сейчас сопоставим его с кодом ассемблера.
Теперь дизассемблируем из windbg. Функция небольшая, а вот ассемблерного кода — две страницы :(
Строка на которой генерируется исключение, отмечена знаком ">>>", и становится ясно, что виноват не глючный какой-то элемент внутри списка shadowIndexes, а сам массив — где-то в процессе накручивания цикла значение shadowIndexes становися null.
Определить виновника теперь довольно просто.
Посмотрим на поля DataTable (0x1990a1d4 — это тот самый this, значение из ECX):
По адресу 1990a200 находятся indexes (поле со смещением 2с), рядом с ним — shadowIndexes. Cтавим точку остановки на запись в эту область памяти.
И запускаем программу, которая через несколько незабываемых мгновений валится обратно в отладчик. Значит, дествительно, по интересующему нас адресу было записано некое значение.
Видим, что текущая нструкция — «cmp eax,1557821Ch». На первый взгляд это странно. Показана она потому, что такая точка останова срабатывает после события записи.
Команда !clrstack выводит стек вызовов, полный рефлекшена (последний метод в стеке System.Reflection.RuntimeMethodInfo.InternalInvoke, что не очень-то для нас полезно).
Ипользуем dumpstack.
Ну вот, собственно, и виновник нашелся. Цветом в стеке выделенны ключевые вызовы: ResetIndexes вызывает событие OnListChanged, которое плавно перетекает в биндинг, который вызывает функцию приложения UpdatePrice. Эта функция по стечению обстоятельств меняет данные в датасете, что и приводит к преждевременному зануления shadowIndexes.
Несколько движений технологичным нано-молотком сишарпа, виновник наказан, программа работает. Ура!
Итак, имеется приложение, написанное на C#, которое использует .net framework 1.1 (да-да). Приложение после некоторых внесенных изменений начало выбрасывать такое малоинформативное исключение:
System.NullReferenceException: Object reference not set to an instance of an object.
at System.Data.DataTable.ResetIndexes()
at System.Data.Merger.MergeTable(DataTable src, DataTable dst)
at System.Data.Merger.MergeTableData(DataTable src)
at System.Data.Merger.MergeTable(DataTable src)
at System.Data.DataSet.Merge(DataTable table, Boolean preserveChanges, MissingSchemaAction missingSchemaAction)
at Com.Product.App.RequestForm.InitEdit(InitRequestArgs args)
InitEdit — функция формы в приложении, которая инициализирует данные. Строка исходного кода приложения, на которой оно выбрасывается:
dsRequestList.Merge(dsLast.Tables["TABLE_REQUESTS"], false, MissingSchemaAction.Ignore);
Код функции ResetIndexes, который показывает нам Рефлектор, не сказать чтобы уж очень сложный:
- internal void ResetIndexes()
- {
- this.RecomputeCompareInfo();
- if (this.indexes != null)
- {
- this.SetShadowIndexes();
- try
- {
- int count = this.shadowIndexes.Count;
- for (int i = 0; i < count; i++)
- {
- ((Index)this.shadowIndexes[i]).Reset();
- }
- }
- finally
- {
- this.shadowIndexes = null;
- }
- }
- }
* This source code was highlighted with Source Code Highlighter.
Вроде как проблем нет. Функция пробегает по индексам и вызывает у каждого Reset. Где может быть исключение? SetShadowIndexes просто вызывает "this.shadowIndexes = this.LiveIndexes;" Значит, на момент исполнения строки №9 shadowIndexes ненулевое. Значит, резонно, как мне кажется, предположить, что где-то внутри этого массива находится null элемент, и, соответственно падает на строке 12. Думаю, проблема в этом, сказал я себе и принялся за работу.
Запускаем windbg, соединяемся с процессом, загружаем в дебагер Son of Strike (sos.dll). Думаю, все знают, что это такое, и нужды объяснять нет.
Ставим точку остановки на нашей функции:
0:023> !bp app.exe Com.Product.App.RequestForm.InitEdit
Setting breakpoint at : 0xcef7988
bp 0xcef7988 " .echo [DEFAULT] [hasThis] Boolean Com.Product.App.RequestForm.InitEdit(Class Com.Product.Tickets.InitArgs) "
Setting breakpoint at : 0xcef7a38
bp 0xcef7a38 " .echo [DEFAULT] [hasThis] Boolean Com.Product.App.RequestForm.InitEdit(Class Com.Product.App.Requests.InitRequestArgs) "
(Мне нужна только вторая функция, поэтому отключаем первую точку командой windbg: bd 0).
Нельзя поставить точку остановки прямо на ResetIndexes, так как приложение довольно интенсивно использует датасеты и нам придется пропустить много ложных вызовов.
Поэтому точки надо расставлять последовательно, по мере вызова нужных функций.
После того, как дебаггер останавливает на InitEdit, ставим точку на DataSet.Merge:
0:000> !bp system.data.dll System.Data.DataSet.Merge
Setting breakpoint at : 0x8185418
bp 0x8185418 " .echo [DEFAULT] [hasThis] Void System.Data.DataSet.Merge(Class System.Data.DataSet) "
(опять же SoS выставит остановку на все перегруженные функции Merge, поэтому избавляемся от не ненужных)
После DataSet.Merge нужно зайти в System.Data.Merger.MergeTable, откуда и вызывается DataTable.ResetIndexes.
Проверяем, что аргумент действительно тот, который мы ожидаем:
0:000> !clrstack -a 1
Thread 0
ESP EIP
ESP/REG Object Name
eax 0x191cca10 Com.Product.App.Requests.dsRequests/TABLE_USERSDataTable
ecx 0x1562815c System.Data.Merger
edx 0x1952b684 System.Data.DataTable
esi 0x1562815c System.Data.Merger
edi 0x1952b684 System.Data.DataTable
0x0012e9b4 0x08186d00 [DEFAULT] [hasThis] Void System.Data.Merger.MergeTable(Class System.Data.DataTable,Class System.Data.DataTable)
EDI 0x1952b684 ESI 0x1562815c EBX 0x00000000 EDX 0x1952b684 ECX 0x1562815c
EAX 0x191cca10 EBP 0x0012e9dc ESP 0x0012e9b4 EIP 0x08186d00
Cмотрим на имя таблицы, которая передана как параметр.
0:000> !do (edx)
Name: System.Data.DataTable
MethodTable 0x04a633f8
EEClass 0x04a37244
Size 232(0xe8) bytes
GC Generation: 2
mdToken: 0x0200003d (c:\winxp\assembly\gac\system.data\1.0.5000.0__b77a5c561934e089\system.data.dll)
FieldDesc*: 0x04a62544
MT Field Offset Type Attr Value Name
0x7b308b0c 0x4000583 0x4 CLASS instance 0x00000000 site
...
0x04a633f8 0x40003fa 0x34 CLASS instance 0x15628170 extendedProperties
0x04a633f8 0x40003fb 0x38 CLASS instance 0x1952bc2c tableName
0x04a633f8 0x40003fc 0x3c CLASS instance 0x00000000 tableNamespace
...
Имя таблицы записано в поле со смещением 0x38 от начала объекта.
0:000> !do poi(edx+38)
String: TABLE_USERS
TABLE_USERS — не та таблица, что нам нужна, поэтому идем дальше, пока не встретим
0:000> !do poi(edx+38)
String: TABLE_REQUESTS
Только после этого добавляем точку остановки в ResetIndexes, запускаем программу дальше, и опять останавливаемся.
0:000> g
[DEFAULT] [hasThis] Void System.Data.DataTable.ResetIndexes()
eax=191c76ec ebx=00000001 ecx=191c70b4 edx=0006471c esi=19508a4c edi=19221a28
eip=07eff838 esp=0012e988 ebp=191c70b4 iopl=0 nv up ei ng nz na po cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000283
07eff838 55 push ebp
0:000> !clrstack -a 1
Thread 0
ESP EIP
ESP/REG Object Name
eax 0x191c76ec System.Collections.ArrayList
ecx 0x191c70b4 Com.Product.App.Requests.dsRequests/TABLE_REQUESTSDataTable
esi 0x19508a4c System.Data.DataRow
edi 0x19221a28 System.Data.DataTable
ebp 0x191c70b4 Com.Product.App.Requests.dsRequests/TABLE_REQUESTSDataTable
0x0012e988 0x07eff838 [DEFAULT] [hasThis] Void System.Data.DataTable.ResetIndexes()
EDI 0x19221a28 ESI 0x19508a4c EBX 0x00000001 EDX 0x0006471c ECX 0x191c70b4
EAX 0x191c76ec EBP 0x191c70b4 ESP 0x0012e988 EIP 0x07eff838
this передается в ecx, берем ее и смотрим что лежит в поле indexes:
0:000> !do ecx
Name: Com.Product.App.Requests.dsRequests/TABLE_REQUESTSDataTable
MethodTable 0x10bb39f4
EEClass 0x10ac9a40
Size 580(0x244) bytes
GC Generation: 2
mdToken: 0x02000296
FieldDesc*: 0x10bb2eb4
MT Field Offset Type Attr Value Name
0x7b308b0c 0x4000583 0x4 CLASS instance 0x00000000 site
...
0x04a633f8 0x40003f8 0x2c CLASS instance 0x191c76ec indexes
0x04a633f8 0x40003f9 0x30 CLASS instance 0x00000000 shadowIndexes
0x04a633f8 0x40003fa 0x34 CLASS instance 0x15629d98 extendedProperties
...
Пока что все правильно. На момент входа в ResetIndexes поле indexes равно нулю, затем устанавливается с помощью SetShadowIndexes.
0:000> !dc 0x191c76ec
Going to dump the Collection passed.
Name: System.Collections.ArrayList
GC Generation: 2
Address MT Class Name
0x1557971c 0x08121b80 System.Data.Index
0x1559e420 0x08121b80 System.Data.Index
0x1559e508 0x08121b80 System.Data.Index
0x1559e5f0 0x08121b80 System.Data.Index
0x1559e6d8 0x08121b80 System.Data.Index
0x1559e7c0 0x08121b80 System.Data.Index
0x1559e8a8 0x08121b80 System.Data.Index
0x1559e990 0x08121b80 System.Data.Index
0x1559ea78 0x08121b80 System.Data.Index
0x1559eb60 0x08121b80 System.Data.Index
0x1559ec48 0x08121b80 System.Data.Index
0x1559ed30 0x08121b80 System.Data.Index
0x1559ee18 0x08121b80 System.Data.Index
0x1559ef00 0x08121b80 System.Data.Index
0x1559efe8 0x08121b80 System.Data.Index
0x1559f0d0 0x08121b80 System.Data.Index
0x1559f1b8 0x08121b80 System.Data.Index
0x156f1a9c 0x08121b80 System.Data.Index
0x156f4cd4 0x08121b80 System.Data.Index
0x157962b0 0x08121b80 System.Data.Index
0x157ba910 0x08121b80 System.Data.Index
Мда… моя основная теория распадается на глазах. Нет там нулевых элементов. Вот незадача. Что ж, продолжим выполнение кода, и посмотрим на исключения изнутри windbg. Отпускаем программу. Когда происходит исключение, отладчик останавливается:
0:000> g
(1638.1738): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=191c70b4 ebx=00000015 ecx=00000000 edx=00000014 esi=195b0894 edi=00000014
eip=07eff899 esp=0012e960 ebp=0012e984 iopl=0 nv up ei ng nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010297
07eff899 8b01 mov eax,dword ptr [ecx] ds:0023:00000000=????????
Выполняем команду !analyze -v, результат которой невероятно напичкан разного рода данными о произошедшем исключении. Привожу сжатом виде:
0:000> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
FAULTING_IP:
+7eff899
07eff899 8b01 mov eax,dword ptr [ecx]
EXCEPTION_RECORD: ffffffff -- (.exr 0xffffffffffffffff)
.exr 0xffffffffffffffff
ExceptionAddress: 07eff899
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000000
Parameter[1]: 00000000
Attempt to read from address 00000000
FAULTING_THREAD: 00001738
DEFAULT_BUCKET_ID: NULL_POINTER_READ
PROCESS_NAME: App.exe
ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at "0x%08lx" referenced memory at "0x%08lx". The memory could not be "%s".
READ_ADDRESS: 00000000
FAILED_INSTRUCTION_ADDRESS:
+7eff899
07eff899 8b01 mov eax,dword ptr [ecx]
NTGLOBALFLAG: 0
APPLICATION_VERIFIER_FLAGS: 0
IP_ON_HEAP: 08186f34
MANAGED_STACK: !dumpstack -EE
!dumpstack -EE
Thread 0
Current frame: (MethodDesc 0x4a63200 +0x61 System.Data.DataTable.ResetIndexes)
ChildEBP RetAddr Caller,Callee
0012e984 08186f34 (MethodDesc 0x31fcff0 +0x234 System.Data.Merger.MergeTable)
0012e9b0 08185790 (MethodDesc 0x31fd020 +0x48 System.Data.Merger.MergeTableData)
0012e9dc 0818e31f (MethodDesc 0x31fcfe0 +0x2f System.Data.Merger.MergeTable)
0012e9ec 0818e2b1 (MethodDesc 0x7ec0ce8 +0x61 System.Data.DataSet.Merge)
0012ea00 0cef8056 (MethodDesc 0x7c8f8d0 +0x61e Com.Product.App.RequestForm.InitEdit)
0012ec3c 26a117e7 (MethodDesc 0x10eaee98 +0x57 Com.Product.Core.Entities.EntityContextCache..ctor)
0012ec50 0cef79df (MethodDesc 0x7c8f8c0 +0x57 Com.Product.App.RequestForm.InitEdit)
0012ec74 0818d593 (MethodDesc 0x7ec0448 +0x2b Com.CommonComponents.Tools.PerformanceTimer.sAddCheckPoint)
0012ec84 0cef7933 (MethodDesc 0x7c8da68 +0x8b Com.Product.Tickets.Ticket.OnInit)
0012ecb4 0cef7706 (MethodDesc 0x7d509e8 +0x6e Com.Product.App.RequestForm.OnInit)
0012ecf4 0cef766b (MethodDesc 0x7c8da58 +0x1b Com.Product.Tickets.Ticket.Init)
[неважная часть стека вызовов]
---------
Cтек вызовов уже мы видели, правда в несколько сокращенном варианте. А вот по коду сейчас будем разбираться. Давайте взглянем на код, который вызвал исключение:
07eff899 8b01 mov eax,dword ptr [ecx] ds:0023:00000000=????????
Читается значение из памяти по адресу, который содержится в ECX, а там (в ECX), как мы видим, ноль…
Давайте теперь посмотрим на функцию ResetIndexes, превращенную заботливым jit-компилятором в исполняемый код для процессора x86. Я повторю код функции на C#, и мы сейчас сопоставим его с кодом ассемблера.
- internal void ResetIndexes()
- {
- this.RecomputeCompareInfo();
- if (this.indexes != null)
- {
- this.SetShadowIndexes();
- try
- {
- int count = this.shadowIndexes.Count;
- for (int i = 0; i < count; i++)
- {
- ((Index)this.shadowIndexes[i]).Reset();
- }
- }
- finally
- {
- this.shadowIndexes = null;
- }
- }
- }
* This source code was highlighted with Source Code Highlighter.
Теперь дизассемблируем из windbg. Функция небольшая, а вот ассемблерного кода — две страницы :(
0:000> !u
No value passed in, defaulting to EIP
Will print '>>> ' at address: 0x07eff899
Normal JIT generated code
[DEFAULT] [hasThis] Void System.Data.DataTable.ResetIndexes()
Begin 0x07eff838, size 0xc4
07eff838 push ebp
07eff839 mov ebp,esp
07eff83b sub esp,18h
07eff83e push edi
07eff83f push esi
07eff840 push ebx ; настройка стека итд.
07eff841 mov dword ptr [ebp-8],0 ;
07eff848 mov dword ptr [ebp-14h],ecx ; По адресу "ebp-14h" сохраняется указатель на
; объект DataTable, внутри которого мы
; сейчас находимся, this.
07eff84b mov ecx,dword ptr [ebp-14h]
07eff84e call 07eff420 (System.Data.DataTable.RecomputeCompareInfo) ; (3) это понятно и так
07eff853 mov eax,dword ptr [ebp-14h]
07eff856 cmp dword ptr [eax+2Ch],0 ; (4) if (this.indexes != null)
07eff85a jne 07eff863
07eff85c pop ebx
07eff85d pop esi
07eff85e pop edi
07eff85f mov esp,ebp
07eff861 pop ebp
07eff862 ret ; выход, если все же indexes == null
07eff863 mov ecx,dword ptr [ebp-14h]
07eff866 call dword ptr ds:[4A634CCh] ; (6) this.LiveIndexes
07eff86c mov ebx,dword ptr [ebp-14h]
07eff86f lea edx,[ebx+30h] ; (6) this.indexes
07eff872 call 01003048 ; (6) indexes = LiveIndexes, интересно,
; оказывается компилятор-то заинлайнил
; функцию SetShadowIndexes,
; которая, прочем, только
; из этого присваивания и состоит.
07eff877 mov eax,dword ptr [ebp-14h] ; (9)
07eff87a mov dword ptr [ebp-18h],eax ; (9)
07eff87d mov ecx,dword ptr [eax+30h] ; (9) ECX сейчас содержит указатель shadowIndexes.
07eff880 mov eax,dword ptr [ecx] ; (9) Здесь танцы с бубном:
; (9) первое поле в объекте (то, которое берется как [ecx]) -
; (9) это указатель на MethodTable его класса,
07eff882 call dword ptr [eax+0D0h] ; (9) А это, собственно вызов функции со
; (9) смещением 0xD0 от начала таблицы методов -
; (9) ни что иное, как ArrayList.Count,
07eff888 mov ebx,eax ; (9) значение которого сейчас в eax.
; (9) Инструкции, начиная с адреса 07eff877
; (9) по текущую - это строка кода
; (9) int count = this.shadowIndexes.Count;
07eff88a xor edi,edi ;(10) int i = 0;
07eff88c cmp ebx,0 ;(10) Немедленный выход,
;(10) если shadowIndexes.Count == 0.
07eff88f jle 07eff8cc
07eff891 mov eax,dword ptr [ebp-18h] ;(12)
07eff894 mov ecx,dword ptr [eax+30h] ;(12) ECX опять содержит указатель shadowIndexes.
07eff897 mov edx,edi
>>> 07eff mov eax,dword ptr [ecx] ;(12) Опять получение MethodTable
;(12) для класса shadowIndexes (это ArrayList)
07eff89b call dword ptr [eax+0A0h] ;(12) Вызов shadowIndexes[i]
07eff8a1 mov edx,eax
07eff8a3 mov ecx,8121B80h
07eff8a8 call mscorwks!JIT_ChkCastClass (791e381f) ;(12)
;(12) Приведение типа: ((Index)this.shadowIndexes[i])
07eff8ad mov esi,eax
07eff8af cmp dword ptr [esi],eax
07eff8b1 mov ecx,esi
07eff8b3 call dword ptr ds:[8121C44h] ;(12) Вызов Index.InitRecords();
07eff8b9 mov edx,dword ptr ds:[2208EC4h]
07eff8bf mov ecx,esi
07eff8c1 call dword ptr ds:[8121C54h] ;(12) Вызов Index.OnListChanged();
;(12) Что характерно, функция Index.Reset(),
;(12) которая состоит вызова
;(12) InitRecords и OnListChanged
;(12) также встроена в код,
07eff8c7 inc edi ;(10) это i++ из цикла for.
07eff8c8 cmp edi,ebx ;(10) это
07eff8ca jl 07eff891 ;(10) и это - "i < count". Переход на начало цикла.
;(10) обычно так циклы и реализованы
;(10) в дотнет: блок проверки на
;(10) продолжение цикла находится в конце.
07eff8cc mov dword ptr [ebp-0Ch],0
07eff8d3 mov dword ptr [ebp-8],0FCh
07eff8da push 7EFF8EEh
07eff8df jmp 07eff8e1
07eff8e1 mov eax,dword ptr [ebp-14h]
07eff8e4 mov dword ptr [eax+30h],0 ;(17) зануление shadowIndexes
;(17) в блоке finally.
07eff8eb pop eax
07eff8ec jmp eax
07eff8ee mov dword ptr [ebp-8],0
07eff8f5 pop ebx
07eff8f6 pop esi
07eff8f7 pop edi
07eff8f8 mov esp,ebp
07eff8fa pop ebp
07eff8fb ret
Строка на которой генерируется исключение, отмечена знаком ">>>", и становится ясно, что виноват не глючный какой-то элемент внутри списка shadowIndexes, а сам массив — где-то в процессе накручивания цикла значение shadowIndexes становися null.
Определить виновника теперь довольно просто.
Посмотрим на поля DataTable (0x1990a1d4 — это тот самый this, значение из ECX):
0:000> dd 0x1990a1d4 + 2c
1990a200 1990a80c 1990a80c 15b03c88 0142ea34
1990a210 00000000 011e11f4 00000000 00000000
1990a220 01200c24 00000000 00000000 00000000
1990a230 0124ca7c 155796bc 15579758 00000000
1990a240 00000000 00000000 155a4c5c 00000000
1990a250 00000000 00000000 00000000 00000000
1990a260 15ae8eec 00000000 00000000 1990a824
1990a270 1990a418 1990a480 1990a4e8 00000002
По адресу 1990a200 находятся indexes (поле со смещением 2с), рядом с ним — shadowIndexes. Cтавим точку остановки на запись в эту область памяти.
0:000> ba w 4 1990a204
И запускаем программу, которая через несколько незабываемых мгновений валится обратно в отладчик. Значит, дествительно, по интересующему нас адресу было записано некое значение.
0:000> g
Breakpoint 12 hit
eax=1990a80c ebx=00000000 ecx=1990a80c edx=1990a204 esi=1990a1d4 edi=1990a1d4
eip=0100304a esp=0012e288 ebp=0012e2b4 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
0100304a 81f81c825715 cmp eax,1557821Ch
Видим, что текущая нструкция — «cmp eax,1557821Ch». На первый взгляд это странно. Показана она потому, что такая точка останова срабатывает после события записи.
Команда !clrstack выводит стек вызовов, полный рефлекшена (последний метод в стеке System.Reflection.RuntimeMethodInfo.InternalInvoke, что не очень-то для нас полезно).
Ипользуем dumpstack.
0:000> !dumpstack -ee
Thread 0
Current frame:
ChildEBP RetAddr Caller,Callee
0012e284 0814f725 (MethodDesc 0x4a631c0 +0x2d System.Data.DataTable.RecordStateChanged)
0012e2b4 08144d6d (MethodDesc 0x4a63280 +0x1c5 System.Data.DataTable.SetNewRecord)
0012e314 08145b29 (MethodDesc 0x812c488 +0x29 System.Data.DataRow.EndEdit)
0012e328 081458d9 (MethodDesc 0x812c3c8 +0x131 System.Data.DataRow.set_Item)
0012e348 03206651 (MethodDesc 0x812c3a8 +0x49 System.Data.DataRow.set_Item)
0012e360 0320bac2 (MethodDesc 0x7ec3760 +0x5a Com.Product.Core.Entities.EntityAdapter.DBAssign)
0012e394 2ea362d7 (MethodDesc 0x7ec3d90 +0x3f Com.Product.Core.Entities.Request.set_Price)
0012e3b8 2ea359ea (MethodDesc 0x7c8faa0 +0x252 Com.Product.App.RequestForm.UpdatePrice
0012e494 0cefdf89 (MethodDesc 0x7d50218 +0x1a1 Com.Product.App.RequestForm.panel1_RadioButtonsValueRealyChanged)
0012e4b0 7b8808cd (MethodDesc 0x7b9e38c8 +0x45 System.Windows.Forms.Control.AccessibilityNotifyClients)
0012e4c4 0cefd04c (MethodDesc 0xb989dd8 +0x4c Com.Product.CommonControls.DBControls.NullableRadioButtonsPanel.OnRadioButtonsValueReallyChanged)
0012e4e4 7b92a038 (MethodDesc 0x7ba1ea48 +0x68 System.Windows.Forms.RadioButton.set_Checked)
0012e4f0 0cefcda1 (MethodDesc 0xb989d78 +0x111 Com.Product.CommonControls.DBControls.NullableRadioButtonsPanel.set_RadioButtonsValue)
0012e760 799dd8c1 (MethodDesc 0x79bb1468 +0x141 System.Reflection.RuntimeMethodInfo.InternalInvoke)
0012e7a4 799dd768 (MethodDesc 0x79bb1458 +0x18 System.Reflection.RuntimeMethodInfo.Invoke)
0012e7bc 7b1f5c81 (MethodDesc 0x7b328e20 +0x139 System.ComponentModel.ReflectPropertyDescriptor.SetValue)
0012e810 7b8a1973 (MethodDesc 0x7ba38350 +0xd3 System.Windows.Forms.Binding.SetPropValue)
0012e82c 7b8a16d8 (MethodDesc 0x7ba38330 +0x50 System.Windows.Forms.Binding.FormatObject)
0012e840 7b8a1888 (MethodDesc 0x7ba38340 +0x38 System.Windows.Forms.Binding.PushData)
0012e848 7b8a311c (MethodDesc 0x7ba38938 +0x8c System.Windows.Forms.BindingManagerBase.PushData)
0012e85c 7b8bcde7 (MethodDesc 0x7ba38cf8 +0x37 System.Windows.Forms.CurrencyManager.CurrencyManager_PushData)
0012e87c 7b8a1996 (MethodDesc 0x7ba38350 +0xf6 System.Windows.Forms.Binding.SetPropValue)
0012e88c 7b8bdc33 (MethodDesc 0x7ba38df8 +0x5b System.Windows.Forms.CurrencyManager.OnItemChanged)
0012e8ac 7b8a1878 (MethodDesc 0x7ba38340 +0x28 System.Windows.Forms.Binding.PushData)
0012e8b4 7b8a1ad9 (MethodDesc 0x7ba38390 +0x59 System.Windows.Forms.Binding.UpdateIsBinding)
0012e8bc 7b8bdff0 (MethodDesc 0x7ba38e88 +0x110 System.Windows.Forms.CurrencyManager.UpdateIsBinding)
0012e8d0 7b8bdec8 (MethodDesc 0x7ba38e78 +0x8 System.Windows.Forms.CurrencyManager.UpdateIsBinding)
0012e8d4 7b8bd78c (MethodDesc 0x7ba38db8 +0x1c4 System.Windows.Forms.CurrencyManager.List_ListChanged)
0012e900 08181927 (MethodDesc 0x812ce50 +0x37 System.Data.DataView.OnListChanged)
0012e928 10c456ac (MethodDesc 0x812ce40 +0x34 System.Data.DataView.IndexListChanged)
0012e930 10525133 (MethodDesc 0x812ce30 +0x33 System.Data.DataView.FireEvent)
0012e940 10c45663 (MethodDesc 0x31fada0 +0x2b System.Data.DataViewListener.IndexListChanged)
0012e950 0814c5bc (MethodDesc 0x81219d0 +0x1c System.Data.Index.OnListChanged)
0012e958 07eff8c7 (MethodDesc 0x4a63200 +0x8f System.Data.DataTable.ResetIndexes)
0012e984 08186f34 (MethodDesc 0x31fcff0 +0x234 System.Data.Merger.MergeTable)
0012e9b0 08185790 (MethodDesc 0x31fd020 +0x48 System.Data.Merger.MergeTableData)
0012e9dc 0818e31f (MethodDesc 0x31fcfe0 +0x2f System.Data.Merger.MergeTable)
0012e9ec 0818e2b1 (MethodDesc 0x7ec0ce8 +0x61 System.Data.DataSet.Merge)
0012ea00 0cef8056 (MethodDesc 0x7c8f8d0 +0x61e Com.Product.App.RequestForm.InitEdit)
0012ec3c 26a117e7 (MethodDesc 0x10eaee98 +0x57 Com.Product.Core.Entities.EntityContextCache..ctor)
0012ec50 0cef79df (MethodDesc 0x7c8f8c0 +0x57 Com.Product.App.RequestForm.InitEdit)
0012ec74 0818d593 (MethodDesc 0x7ec0448 +0x2b Com.CommonComponents.Tools.PerformanceTimer.sAddCheckPoint)
0012ec84 0cef7933 (MethodDesc 0x7c8da68 +0x8b Com.Product.Tickets.Ticket.OnInit)
0012ecb4 0cef7706 (MethodDesc 0x7d509e8 +0x6e Com.Product.App.RequestForm.OnInit)
0012ecf4 0cef766b (MethodDesc 0x7c8da58 +0x1b Com.Product.Tickets.Ticket.Init)
....
Ну вот, собственно, и виновник нашелся. Цветом в стеке выделенны ключевые вызовы: ResetIndexes вызывает событие OnListChanged, которое плавно перетекает в биндинг, который вызывает функцию приложения UpdatePrice. Эта функция по стечению обстоятельств меняет данные в датасете, что и приводит к преждевременному зануления shadowIndexes.
Несколько движений технологичным нано-молотком сишарпа, виновник наказан, программа работает. Ура!