Poprawianie kroku języka .NET w debugerze

135

Po pierwsze przepraszam za długość tego pytania.

Jestem autorem IronScheme . Ostatnio ciężko pracowałem nad emitowaniem przyzwoitych informacji debugowania, aby móc używać „natywnego” debuggera .NET.

Chociaż częściowo się to udało, napotykam na początkowe problemy.

Pierwszy problem dotyczy steppingu.

Ponieważ Scheme jest językiem wyrażeń, wszystko jest zwykle zawijane w nawiasy, w przeciwieństwie do głównych języków .NET, które wydają się być oparte na instrukcjach (lub wierszach).

Oryginalny kod (Schemat) wygląda następująco:

(define (baz x)
  (cond
    [(null? x) 
      x]
    [(pair? x) 
      (car x)]
    [else
      (assertion-violation #f "nooo" x)]))

Celowo umieściłem każde wyrażenie w nowej linii.

Emitowany kod przekształca się w C # (przez ILSpy) wygląda następująco:

public static object ::baz(object x)
{
  if (x == null)
  {
    return x;
  }
  if (x is Cons)
  {
    return Builtins.Car(x);
  }
  return #.ironscheme.exceptions::assertion-violation+(
     RuntimeHelpers.False, "nooo", Builtins.List(x));
}

Jak widać, całkiem proste.

Uwaga: Jeśli kod zostałby przekształcony w wyrażenie warunkowe (? :) w C #, całość byłaby tylko jednym krokiem debugowania, miej to na uwadze.

Oto wyjście IL z numerami źródła i linii:

  .method public static object  '::baz'(object x) cil managed
  {
    // Code size       56 (0x38)
    .maxstack  6
    .line 15,15 : 1,2 ''
//000014: 
//000015: (define (baz x)
    IL_0000:  nop
    .line 17,17 : 6,15 ''
//000016:   (cond
//000017:     [(null? x) 
    IL_0001:  ldarg.0
    IL_0002:  brtrue     IL_0009

    .line 18,18 : 7,8 ''
//000018:       x]
    IL_0007:  ldarg.0
    IL_0008:  ret

    .line 19,19 : 6,15 ''
//000019:     [(pair? x) 
    .line 19,19 : 6,15 ''
    IL_0009:  ldarg.0
    IL_000a:  isinst [IronScheme]IronScheme.Runtime.Cons
    IL_000f:  ldnull
    IL_0010:  cgt.un
    IL_0012:  brfalse    IL_0020

    IL_0017:  ldarg.0
    .line 20,20 : 7,14 ''
//000020:       (car x)]
    IL_0018:  tail.
    IL_001a:  call object [IronScheme]IronScheme.Runtime.Builtins::Car(object)
    IL_001f:  ret

    IL_0020:  ldsfld object 
         [Microsoft.Scripting]Microsoft.Scripting.RuntimeHelpers::False
    IL_0025:  ldstr      "nooo"
    IL_002a:  ldarg.0
    IL_002b:  call object [IronScheme]IronScheme.Runtime.Builtins::List(object)
    .line 22,22 : 7,40 ''
//000021:     [else
//000022:       (assertion-violation #f "nooo" x)]))
    IL_0030:  tail.
    IL_0032:  call object [ironscheme.boot]#::
       'ironscheme.exceptions::assertion-violation+'(object,object,object)
    IL_0037:  ret
  } // end of method 'eval-core(033)'::'::baz'

Uwaga: Aby uniemożliwić debugerowi po prostu podświetlenie całej metody, ustawiam punkt wejścia metody na szerokość tylko 1 kolumny.

Jak widać, każde wyrażenie jest poprawnie odwzorowywane na linię.

Teraz problem z steppingiem (testowany na VS2010, ale ten sam / podobny problem na VS2008):

Nie są IgnoreSymbolStoreSequencePointsone stosowane.

  1. Wywołaj baz z null arg, działa poprawnie. (null? x), po którym następuje x.
  2. Wywołaj baz z argumentem Cons, działa poprawnie. (null? x), a następnie (para? x), a następnie (samochód x).
  3. Zadzwoń do baz z innym argumentem, nie udaje się. (null? x), a następnie (para? x), a następnie (samochód x), a następnie (asercja-naruszenie ...).

Podczas aplikacji IgnoreSymbolStoreSequencePoints(zgodnie z zaleceniami):

  1. Wywołaj baz z null arg, działa poprawnie. (null? x), po którym następuje x.
  2. Zadzwoń do baz z argumentem Cons, nie udaje się. (null? x) to (para? x).
  3. Zadzwoń do baz z innym argumentem, nie udaje się. (null? x), a następnie (para? x), a następnie (samochód x), a następnie (asercja-naruszenie ...).

W tym trybie stwierdzam również, że niektóre linie (nie pokazane tutaj) są nieprawidłowo podświetlone, są wyłączone o 1.

Oto kilka pomysłów, jakie mogą być przyczyny:

  • Połączenia ogonowe dezorientują debugger
  • Nakładające się lokalizacje (nie pokazane tutaj) dezorientują debuger (robi to bardzo dobrze podczas ustawiania punktu przerwania)
  • ????

Drugim, ale również poważnym problemem jest to, że debugger w niektórych przypadkach nie łamie / nie trafia punktów przerwania.

Jedynym miejscem, w którym mogę sprawić, by debugger działał poprawnie (i konsekwentnie), jest punkt wejścia metody.

Sytuacja trochę się poprawia, gdy IgnoreSymbolStoreSequencePointsnie jest stosowana.

Wniosek

Możliwe, że debugger VS jest po prostu błędny :(

Bibliografia:

  1. Dokonywanie debugowania języka CLR / .NET

Aktualizacja 1:

Mdbg nie działa w przypadku zestawów 64-bitowych. Więc to się skończyło. Nie mam już 32-bitowych maszyn do przetestowania. Aktualizacja: Jestem pewien, że to nie jest duży problem, czy ktoś ma poprawkę? Edycja: Tak, głupi ja, po prostu uruchom mdbg w wierszu polecenia x64 :)

Aktualizacja 2:

Utworzyłem aplikację C # i próbowałem przeanalizować informacje o wierszu.

Moje ustalenia:

  • Po każdej brXXXinstrukcji musisz mieć punkt sekwencji (jeśli nie jest poprawny, czyli „#line hidden”, wyemituj a nop).
  • Przed jakąkolwiek brXXXinstrukcją wyemituj '#line hidden' i a nop.

Zastosowanie tego nie rozwiązuje jednak problemów (samo?).

Ale dodanie następujących daje pożądany efekt :)

  • Następnie retwyemituj '#line hidden' i a nop.

Używa trybu, w którym IgnoreSymbolStoreSequencePointsnie jest stosowany. Po zastosowaniu niektóre kroki są nadal pomijane :(

Oto wyjście IL, gdy zastosowano powyższe:

  .method public static object  '::baz'(object x) cil managed
  {
    // Code size       63 (0x3f)
    .maxstack  6
    .line 15,15 : 1,2 ''
    IL_0000:  nop
    .line 17,17 : 6,15 ''
    IL_0001:  ldarg.0
    .line 16707566,16707566 : 0,0 ''
    IL_0002:  nop
    IL_0003:  brtrue     IL_000c

    .line 16707566,16707566 : 0,0 ''
    IL_0008:  nop
    .line 18,18 : 7,8 ''
    IL_0009:  ldarg.0
    IL_000a:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_000b:  nop
    .line 19,19 : 6,15 ''
    .line 19,19 : 6,15 ''
    IL_000c:  ldarg.0
    IL_000d:  isinst     [IronScheme]IronScheme.Runtime.Cons
    IL_0012:  ldnull
    IL_0013:  cgt.un
    .line 16707566,16707566 : 0,0 ''
    IL_0015:  nop
    IL_0016:  brfalse    IL_0026

    .line 16707566,16707566 : 0,0 ''
    IL_001b:  nop
    IL_001c:  ldarg.0
    .line 20,20 : 7,14 ''
    IL_001d:  tail.
    IL_001f:  call object [IronScheme]IronScheme.Runtime.Builtins::Car(object)
    IL_0024:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_0025:  nop
    IL_0026:  ldsfld object 
      [Microsoft.Scripting]Microsoft.Scripting.RuntimeHelpers::False
    IL_002b:  ldstr      "nooo"
    IL_0030:  ldarg.0
    IL_0031:  call object [IronScheme]IronScheme.Runtime.Builtins::List(object)
    .line 22,22 : 7,40 ''
    IL_0036:  tail.
    IL_0038:  call object [ironscheme.boot]#::
      'ironscheme.exceptions::assertion-violation+'(object,object,object)
    IL_003d:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_003e:  nop
  } // end of method 'eval-core(033)'::'::baz'

Aktualizacja 3:

Problem z powyższym „semi-fix”. Peverify zgłasza błędy we wszystkich metodach z powodu noppo ret. Naprawdę nie rozumiem problemu. Jak można przeprowadzić nopweryfikację przerwy po ret. To jest jak martwy kod (poza tym, że NIE jest to nawet kod) ... No cóż, eksperymenty trwają.

Aktualizacja 4:

Po powrocie do domu usunąłem „nieweryfikowalny” kod, działając na VS2008 i jest znacznie gorzej. Być może rozwiązaniem może być uruchomienie nieweryfikowalnego kodu w celu prawidłowego debugowania. W trybie „zwolnienia” wszystkie dane wyjściowe nadal byłyby weryfikowalne.

Aktualizacja 5:

Teraz zdecydowałem, że mój powyższy pomysł jest na razie jedyną realną opcją. Chociaż wygenerowany kod jest nieweryfikowalny, nie znalazłem jeszcze żadnego VerificationException. Nie wiem, jaki wpływ będzie miał ten scenariusz na użytkownika końcowego.

Jako bonus, moja druga kwestia również została rozwiązana. :)

Oto mały screencast z tego, z czym skończyłem. Uderza w punkty przerwania, wykonuje odpowiednie kroki (in / out / over), itp. W sumie pożądany efekt.

Ja jednak nadal nie akceptuję tego jako sposobu na zrobienie tego. Wydaje mi się to zbyt hakerskie. Przydałoby się potwierdzenie prawdziwego problemu.

Aktualizacja 6:

Właśnie miałem zmianę, aby przetestować kod na VS2010, wydaje się, że są pewne problemy:

  1. Pierwsza rozmowa teraz nie działa poprawnie. (assertion-violation ...) zostaje trafiony. Inne przypadki działają dobrze. Jakiś stary kod emitował niepotrzebne pozycje. Usunięto kod, działa zgodnie z oczekiwaniami. :)
  2. Co ważniejsze, punkty przerwania kończą się niepowodzeniem przy drugim wywołaniu programu (przy użyciu kompilacji w pamięci, zrzucanie zestawu do pliku wydaje się ponownie uszczęśliwiać punkty przerwania).

Oba te przypadki działają poprawnie pod VS2008. Główną różnicą jest to, że w VS2010 cała aplikacja jest kompilowana dla .NET 4, a pod VS2008, kompiluje się do .NET 2. Obie działają w wersji 64-bitowej.

Aktualizacja 7:

Jak wspomniano, mam mdbg działający pod 64-bitowym. Niestety, ma również problem z punktem przerwania, w którym nie działa, jeśli ponownie uruchomię program (oznacza to, że zostanie ponownie skompilowany, więc nie używa tego samego zestawu, ale nadal używa tego samego źródła).

Aktualizacja 8:

Mam złożony błąd na stronie MS Połącz dotyczące kwestii punktu przerwania.

Aktualizacja: naprawiona

Aktualizacja 9:

Po dłuższym zastanowieniu jedynym sposobem na uszczęśliwienie debugera wydaje się być SSA, więc każdy krok może być izolowany i sekwencyjny. Jeszcze nie udowodniłem tego poglądu. Ale wydaje się to logiczne. Oczywiście, wyczyszczenie temps z SSA przerwie debugowanie, ale jest to łatwe do przełączenia, a pozostawienie ich nie wiąże się z dużym obciążeniem.

leppie
źródło
Wszelkie pomysły mile widziane, spróbuję je i dołączę wyniki do pytania. Pierwsze 2 pomysły, wypróbuj mdbg i napisz ten sam kod w C # i porównaj.
leppie
6
Jakie jest pytanie?
George Stocker,
9
Myślę, że jego pytanie brzmi „dlaczego mój język nie działa poprawnie?”
McKay,
1
Jeśli nie przejdziesz peverify, nie będziesz w stanie działać w sytuacjach częściowego zaufania, na przykład z dysku zmapowanego lub w wielu konfiguracjach hostingu wtyczek. Jak generujesz swój IL, refleksję, wysyłanie lub przez tekst + ilasm? W obu przypadkach zawsze możesz wpisać .line 16707566,16707566: 0,0 '' w ten sposób, że nie musisz się martwić, że po powrocie nie postawią nic.
Hippiehunter
@Hippiehunter: Dzięki. Niestety bez nops, stepping nie powiedzie się (sprawdzę to ponownie dla pewności). Sądzę, że to ofiara, którą musiałbym złożyć. To nie tak, że VS może działać nawet bez uprawnień administratora :) Przy okazji używając Reflection.Emit przez DLR (bardzo zhakowany wczesny rozgałęziony).
leppie

Odpowiedzi:

26

Jestem inżynierem w zespole Visual Studio Debugger.

Popraw mnie, jeśli się mylę, ale wydaje się, że jedynym problemem, jaki pozostał, jest to, że podczas przełączania z plików PDB na dynamiczny format symboli kompilacji .NET 4 są pomijane niektóre punkty przerwania.

Prawdopodobnie potrzebowalibyśmy powtórzenia, aby dokładnie zdiagnozować problem, jednak oto kilka uwag, które mogą pomóc.

  1. VS (2008+) może działać jako użytkownik niebędący administratorem
  2. Czy jakieś symbole są ładowane za drugim razem? Możesz przetestować, włamując się (poprzez wyjątek lub wywołanie System.Diagnostic.Debugger.Break ())
  3. Zakładając, że symbole się ładują, czy jest jakaś reprodukcja, którą możesz nam przesłać?
  4. Prawdopodobna różnica polega na tym, że format symboli dla kodu skompilowanego dynamicznie jest w 100% inny między .NET 2 (strumień PDB) i .NET 4 (myślę, że IL DB, że to nazwali?)
  5. Dźwięk „nop” jest w porządku. Zobacz zasady generowania niejawnych punktów sekwencji poniżej.
  6. W rzeczywistości nie musisz emitować rzeczy na różnych liniach. Domyślnie VS wykona „instrukcje symboli”, gdzie jako twórca kompilatora zdefiniujesz, co oznacza „instrukcja-symbol”. Więc jeśli chcesz, aby każde wyrażenie było oddzielną rzeczą w pliku symboli, to zadziała dobrze.

JIT tworzy niejawny punkt sekwencji w oparciu o następujące zasady: 1. Instrukcje IL nop 2. Puste punkty stosu IL 3. Instrukcja IL bezpośrednio po instrukcji wywołania

Jeśli okaże się, że potrzebujemy powtórzenia, aby rozwiązać Twój problem, możesz zgłosić błąd połączenia i bezpiecznie przesyłać pliki za pośrednictwem tego nośnika.

Aktualizacja:

Zachęcamy innych użytkowników, u których występuje ten problem, do wypróbowania wersji Developer Preview programu Dev11 dostępnej pod adresem http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=27543 i skomentowania wszelkich opinii. (Musi kierować na 4,5)

Aktualizacja 2:

Leppie zweryfikował poprawkę, aby działała dla niego w wersji Beta Dev11 dostępnej pod adresem http://www.microsoft.com/visualstudio/11/en-us/downloads, zgodnie z opisem błędu połączenia https://connect.microsoft. com / VisualStudio / feedback / details / 684089 / .

Dzięki,

Łukasz

Luke Kim
źródło
Wielkie dzięki. W razie potrzeby spróbuję zrobić reprodukcję.
leppie
Jeśli chodzi o potwierdzenie, tak, masz rację, ale nadal wolałbym rozwiązywać problemy z krokami bez zrywania weryfikowalności. Ale jak powiedziałem, jestem gotów poświęcić to, gdybym mógł udowodnić, że to nigdy nie przyniesie efektu runtime VerificationException.
leppie
Jeśli chodzi o punkt 6, to po prostu nadałem mu „liniowy” charakter tego pytania. Debugger faktycznie wkracza / wychodzi / nadwyrażenia, tak jak zamierzam, aby działało :) Jedynym problemem z podejściem jest ustawienie punktów przerwania w wyrażeniach „wewnętrznych”, jeśli wyrażenie „zewnętrzne” je obejmuje; debugger w VS ma tendencję do tworzenia najbardziej zewnętrznego wyrażenia jako punktu przerwania.
leppie
Myślę, że wyizolowałem problem z punktem przerwania. Podczas pracy w środowisku CLR2 punkty przerwania wydają się być ponownie oceniane podczas ponownego uruchamiania kodu, aczkolwiek nowy kod. W CLR4 punkty przerwania „przyklejają się” tylko do oryginalnego kodu. Zrobiłem mały screencast zachowania CLR4 @ screencast.com/t/eiSilNzL5Nr . Zwróć uwagę, że nazwa typu zmienia się między wywołaniami.
leppie
Zgłosiłem błąd w połączeniu. Powtórka jest dość łatwa :) connect.microsoft.com/VisualStudio/feedback/details/684089
leppie
3

Jestem inżynierem w zespole SharpDevelop Debugger :-)

Czy rozwiązałeś problem?

Czy próbowałeś debugować go w SharpDevelop? Jeśli jest błąd w .NET, zastanawiam się, czy musimy zaimplementować jakieś obejście. Nie wiem o tym problemie.

Czy próbowałeś debugować go w ILSpy? Szczególnie bez symboli debugowania. Byłoby to debugowanie kodu C #, ale powiedziałoby nam, czy instrukcje IL można ładnie debugować. (Pamiętaj, że debugger ILSpy jest w wersji beta)

Szybkie uwagi na temat oryginalnego kodu IL:

  • .line 19,19: 6,15 '' występuje dwukrotnie?
  • .line 20,20: 7,14 '' nie zaczyna się w niejawnym punkcie sekwencji (stos nie jest pusty). jestem zmartwiony
  • .line 20,20: 7,14 '' zawiera kod „samochód x” (dobry) oraz „#f nooo x” (zły?)
  • w odniesieniu do nop po ret. A co ze stloc, ldloc, ret? Myślę, że C # używa tej sztuczki, aby ret odrębny punkt sekwencji.

David

dsrbecky
źródło
+1 Dzięki za informację zwrotną, jako pierwszy punkt, niefortunny efekt uboczny generatora kodu, ale wydaje się nieszkodliwy. Po drugie, właśnie tego potrzebuję, ale widzę twój punkt widzenia, przyjrzę się temu. Nie jestem pewien, co masz na myśli, mówiąc o ostatnim punkcie.
leppie
Trzeci punkt dotyczył tego, że .line 22,22: 7,40 '' powinno znajdować się przed IL_0020. Lub powinno być coś przed IL_0020, w przeciwnym razie kod nadal liczy się jako .line 20,20: 7,14 ''. Czwarty punkt to „ret, nop” może zostać zastąpiony przez „sloc, .line, ldloc, ret”. Widziałem już ten wzór, może był zbędny, ale może miał powód.
dsrbecky
Nie mogę zrobić stloc, ldloc przed ret, ponieważ stracę wywołanie ogonem. Ach rozumiem, odnosiliście się do oryginalnego dzieła?
leppie