Дело о пропавшем индексе

    Дело, конечно, не такое интересное, как у Руссиновича, но, надеюсь, будет полезно некотроым разработчикам. Основная цель изложения — показать средства, с помощью которых мы можем анализировать поведение программы на самом низком уровне.

    Итак, имеется приложение, написанное на 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, который показывает нам Рефлектор, не сказать чтобы уж очень сложный:
    1. internal void ResetIndexes()
    2. {
    3.     this.RecomputeCompareInfo();
    4.     if (this.indexes != null)
    5.     {
    6.         this.SetShadowIndexes();
    7.         try
    8.         {
    9.             int count = this.shadowIndexes.Count;
    10.             for (int i = 0; i < count; i++)
    11.             {
    12.                 ((Index)this.shadowIndexes[i]).Reset();
    13.             }
    14.         }
    15.         finally
    16.         {
    17.             this.shadowIndexes = null;
    18.         }
    19.     }
    20. }
    * 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#, и мы сейчас сопоставим его с кодом ассемблера.

    1. internal void ResetIndexes()
    2. {
    3.     this.RecomputeCompareInfo();
    4.     if (this.indexes != null)
    5.     {
    6.         this.SetShadowIndexes();
    7.         try
    8.         {
    9.             int count = this.shadowIndexes.Count;
    10.             for (int i = 0; i < count; i++)
    11.             {
    12.                 ((Index)this.shadowIndexes[i]).Reset();
    13.             }
    14.         }
    15.         finally
    16.         {
    17.             this.shadowIndexes = null;
    18.         }
    19.     }
    20. }
    * 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.

    Несколько движений технологичным нано-молотком сишарпа, виновник наказан, программа работает. Ура!
    Поделиться публикацией

    Похожие публикации

    Комментарии 21

      +6
      Я, возможно, что-то не понял, но расскажите, а зачем так страшно? зачем уходить в ассемблер? первый фреймворк не поддерживает скачивание кода?
      weblogs.asp.net/scottgu/archive/2007/10/03/releasing-the-source-code-for-the-net-framework-libraries.aspx
      weblogs.asp.net/scottgu/archive/2008/01/16/net-framework-library-source-code-now-available.aspx
        –3
        при чём тут ассемблер? это байт код
          –1
          А вообще спасибо, до этого не видел байт код .net очень интересно!
        +7
        Теперь я люблю GUI-дебаггеры ещё больше…
          0
          Windbg и есть GUI-debugger.

          Вы, вероятно, имеете ввиду source-level дебагеры, windbg тоже source-level, только не для C#.

          Кстати, я их тоже люблю еще больше.
            +1
            Это, конечно, хорошо, но метание туда-сюда между окнами и по менюшкам напрягает, если честно.

            Кроме того, отладки как таковой в рефлекторе нету — это что-то типа «куда я вообще попал?». А для отладки там такой же листинг.
            +5
            бррр… я за Reflector + VS .Net source
              0
              Я тоже за рефлектор, как видите.

              И за .net source, хотя этого с первого взгляда не заметно.

              Был бы у меня код, все бы заняло в десять раз меньше времени.
              +9
              Разбирать дотнет до ассемблерного кода это крайне сурово.
                0
                Спасибо майкрософту, и за .net, и за биндинг, и за датасеты. Верните три часа жизни, сволочи!
                +2
                Исправленна ошибка это всегда приятно. Вот интересно, в какую сумму работодателю вылились такое глубокое копание, ну или проще говоря: сколько человеко-часов ушло на дизассамблирование?
                  0
                  3 часа.
                  0
                  А еще говорят, что C# легок в использовании и освоении…
                    +8
                    Описанный автором детектив можно было и аналитически расследовать, по собственному коду, вообще не пытаясь дизассемблировать сборки и т.д. (хотя признаю, stacktrace не самый понятный).

                    Статья интересна, скорее, фактом подобной возможности, разобрать всё до регистров :)

                    К тому же, работа с нетипизированными датасетами и databiding — не самые очевидные вещи как раз из-за того, что и было описано — в неожиданные моменты могут обработчики зашевелиться, о которых сразу и не подумаешь, что они в принципе могут быть задействованы. И к C# как к языку, все эти ужасы имеют, скорее, косвенное отношение.
                      0
                      Из статьи понятно, что простое присваивание int count = this.shadowIndexes.Count; и последующее выполнение в цикле конструкции ((Index)this.shadowIndexes[i]).Reset(); даёт побочный эффект:
                      переменная count не может оставаться константой в цикле, так как есть вероятность изменения массива shadowIndexes в любую сторону, отчего и будут происходить глюки.

                      Одно из решений проблемы: преобразовать цикл так, чтобы счётчик (переменная i) всегда в границах массива и был всегда меньше значения его текущей верхней границы. Но это решение ведёт к непроизводительным затратам процессорного времени.

                      Другое решение состоит в копировании необходимых данных во временный массив, безопасную работу уже с ним и, наконец, копирование данных обратно в целевую структуру данных. При этом желательно делать блокировку целевой структуры данных на время всех операций от изменения данных (и размера целевого массива) другими сервисами.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Сторонник WinDbg+SOS, но только не на машине, где установлена полноценная IDE со всеми нужными настройками =). А так очень здорово, спасибо за расследование. Всё таки приятно читать чьё-то другое, чем самому ковыряться в дампе ;-).
                        0
                        эту статью не читали?
                        habrahabr.ru/blogs/net/52441/
                          0
                          Мы то читали, а вот вы нет.

                          Что-то мне подсказывает, что статья — выше :0)
                            0
                            ух ты :)

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое