Um die Funktionsweise von Programmen und Betriebssystemen zu erforschen, gibt es viele verschiedene Tools. Virtuelle Maschinen, IDEs, intelligente Notizblöcke, IDAs, Radare, Hex-Editoren, Pe-Editoren und sogar mehr als hundert Sysinternals-Dienstprogramme - all dies wird durchgeführt, um viele Routinevorgänge zu vereinfachen. Aber manchmal kommt ein Moment, in dem Sie feststellen, dass Ihnen bei all dieser Vielfalt ein kleines Dienstprogramm fehlt, das einfach einen banalen und einfachen Job macht. Sie können Skripte in Python oder Powershell auf dem Knie schreiben, aber oft können Sie sich solche Handwerke nicht ohne Tränen ansehen und sie mit Ihren Kollegen teilen.
In letzter Zeit ist diese Situation wieder zu mir gekommen. Und ich entschied, dass es Zeit war, einfach ein ordentliches Dienstprogramm zu nehmen und zu schreiben. Ich werde in einem der nächsten Artikel über das Dienstprogramm berichten, aber ich werde jetzt über eines der Probleme während der Entwicklung berichten.
Der Fehler tritt folgendermaßen auf: Wenn Sie viele Textzeilen in das Standard-TextBox-Steuerelement einfügen, werden beim
Aufruf der Funktion
GetLineText () ab einem bestimmten Index falsche Zeilen zurückgegeben.
Das Falsche ist, dass
GetLineText () , obwohl die Zeilen aus dem installierten Text stammen, aber weiter entfernt sind, einfach einige Zeilen überspringt. Der Fehler tritt mit einer sehr großen Anzahl von Zeilen auf. Also habe ich sie getroffen - ich habe versucht, 25 Megabyte Text in einer TextBox anzuzeigen. Die Arbeit mit den letzten Zeilen ergab einen unerwarteten Effekt.
Google
schlägt vor, dass der Fehler seit 2011 besteht und Microsoft es nicht eilig hat, etwas zu beheben.
Beispiel
Es gibt keine besonderen Anforderungen für die .NET-Version. Wir erstellen ein Standard-WPF-Projekt und füllen die Dateien folgendermaßen aus:
MainWindow.xaml
<Window x:Class="wpf_textbox.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:wpf_textbox" mc:Ignorable="d" Title="WTF, WPF?" Height="350" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="20"/> <RowDefinition Height="20"/> </Grid.RowDefinitions> <TextBox Grid.Row="0" Margin="5" Name="txt" AcceptsReturn="True" AcceptsTab="True" /> <Button Grid.Row="1" Content="Fire 1!" Click="btn_OnClick" /> <Button Grid.Row="2" Content="Fire 2!" Click="btn2_OnClick" /> </Grid> </Window>
MainWindow.cs (Überspringen mit und Namespace)
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void btn_OnClick(object sender, RoutedEventArgs e) { var sb = new StringBuilder(); for (int i = 0; i < 90009; i++) sb.AppendLine($"{i}"); txt.Text = sb.ToString(); } private void btn2_OnClick(object sender, RoutedEventArgs e) { var sb = new StringBuilder(); for (var i = 1; i < 7; i++) sb.AppendLine("req: " + 150 * i + ", get: " + txt.GetLineText(150 * i).Trim()); for (var i = 1; i < 7; i++) sb.AppendLine("req: " + 15000 * i + ", get: " + txt.GetLineText(15000 * i).Trim()); txt.Text = sb.ToString(); } }
Die Anwendung besteht aus einer TextBox und zwei Schaltflächen. Klicken Sie zuerst auf "Feuer 1!" (Fülle die TextBox mit Zahlen), dann "Fire 2!" (Es werden Zeilen nach Zahlen angefordert und gedruckt).
Erwartetes Ergebnis:
req: 150, get: 150
req: 300, get: 300
req: 450, get: 450
req: 600, get: 600
req: 750, get: 750
req: 900, get: 900
req: 15000, get: 15000
req: 30000, get: 30000
req: 45000, get: 45000
req: 60000, get: 60000
req: 75000, get: 75000
req: 90000, get: 90000
Realität:

Es ist ersichtlich, dass für Indizes unter 1000 - alles in Ordnung, aber für große 15000 - Verschiebungen begonnen haben. Und je weiter, desto mehr.
Erforschen Sie den Fehler
Wir decken den Teil des Resolvers auf, der für die Anzeige des .NET-Quellcodes verantwortlich ist, sowie die spezielle Klasse "Expander of Opportunities und Überwindung von Einschränkungen basierend auf Reflection".
Reflection Capability Extender und Limiter public static class ReflectionExtensions { public static T GetFieldValue<T>(this object obj, string name) { var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var field = obj.GetType().GetField(name, bindingFlags); if (field == null) field = obj.GetType().BaseType.GetField(name, bindingFlags); return (T)field?.GetValue(obj); } public static object InvokeMethod(this object obj, string methodName, params object[] methodParams) { var methodParamTypes = methodParams?.Select(p => p.GetType()).ToArray() ?? new Type[] { }; var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; MethodInfo method = null; var type = obj.GetType(); while (method == null && type != null) { method = type.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null); var intfs = type.GetInterfaces(); if (method != null) break; foreach (var intf in intfs) { method = intf.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null); if (method != null) break; } type = type.BaseType; } return method?.Invoke(obj, methodParams); } }
Empirisch stellen wir fest, dass in einem bestimmten Beispiel das Problem im Bereich der Linie 8510 beginnt. Wenn Sie
txt.GetLineText (8510) anfordern , wird "8510" zurückgegeben. Für 8511 - 8511 und für 8512 - plötzlich 8513.
Wir betrachten die Implementierung von
GetLineText () in einer TextBox:

Wir überspringen die Prüfungen in den ersten Zeilen und sehen den Aufruf
GetStartPositionOfLine () . Es scheint, dass das Problem in dieser Funktion liegen sollte, da für die falsche Zeile die falsche Position des Zeilenanfangs zurückkehren sollte.
Wir rufen unseren Code ein:
var o00 = txt.InvokeMethod("GetStartPositionOfLine", 8510); var o01 = txt.InvokeMethod("GetStartPositionOfLine", 8511); var o02 = txt.InvokeMethod("GetStartPositionOfLine", 8512);
Und die Wahrheit - der Versatz des ersten Objekts (der Anfang der 8510. Zeile) wird als 49950 Zeichen für das zweite Objekt - 49956 und das dritte - 49968 angegeben. Zwischen den ersten beiden 6 Zeichen und zwischen den nächsten 12. Störung - dies ist die fehlende Zeile.
Gehen Sie in
GetStartPositionOfLine () :

Wieder überspringen wir die Startprüfungen und schauen uns die tatsächlichen Aktionen an. Zunächst wird der Punkt berechnet, der zur Linie mit der
lineIndex- Nummer gehen soll. Die Höhe aller Linien wird genommen und die halbe Höhe der Linie wird addiert, um zu ihrer Mitte zu gelangen.
Wir sehen uns
this.VerticalOffset und
this.HorizontalOffset nicht an - sie sind Nullen.
Wir berücksichtigen in unserem Code:
var lineHeight = (double) txt.InvokeMethod("GetLineHeight", null); var y0 = lineHeight * (double)8510 + lineHeight / 2.0 - txt.VerticalOffset; var y1 = lineHeight * (double)8511 + lineHeight / 2.0 - txt.VerticalOffset; var y2 = lineHeight * (double)8512 + lineHeight / 2.0 - txt.VerticalOffset;
Die Werte sind vernünftig, korreliert mit der Logik, alles ist in Ordnung. Wir gehen weiter entlang des
GetStartPositionOfLine () - Codes - wir interessieren uns für die folgende aussagekräftige Zeile (die erste innerhalb der Bedingung), die wie ein Krokodil aussieht und mit einem Aufruf von
GetTextPositionFromPoint () endet.
Wir öffnen die Herausforderungen und ziehen sie durch Reflexion. Bitte beachten Sie, dass einige Schnittstellen aufgrund von Sichtbarkeitsbeschränkungen nicht für uns verfügbar sind. Daher müssen wir mit derselben Reflection auf sie verweisen.
var renderScope = (txt.GetFieldValue<FrameworkElement>("_renderScope") as IServiceProvider);
Die resultierenden Objekte zeigen alle die gleichen Offsets - 49950, 49956, 49568. Wir gehen tiefer in die Implementierung von
GetTextPositionFromPoint () in der TextBoxView ein.

In sieht
GetLineIndexFromPoint () vielversprechend aus. Rufen Sie Ihren Code an.
var o20 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y0), true); var o21 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y1), true); var o22 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y2), true);
Wir bekommen 8510, 8511 und 8513 - Bingo! Zur Umsetzung:

Selbst mit bloßem Auge ist klar, dass dies eine binäre Suche ist.
_lineMetrics - Eine Liste der Zeichenfolgenmerkmale (Start, Länge,
Rahmenbreite ). Ich reibe freudig meine Stifte - ich dachte, dass sie, wie so oft, vergessen haben, irgendwo
+1 zu kleben oder
> statt
> = zu setzen . Kopieren Sie die Funktion in den Code und debuggen Sie sie. Aufgrund der geschlossenen Natur der
_lineMetrics- Typen ziehen
wir sie durch Reflexionen heraus,
_lineHeight haben wir sie früher erhalten. Gesamt:
var lm = textView.GetFieldValue<object>("_lineMetrics"); var c = (int)lm.InvokeMethod("get_Count"); var lineMetrics = new List<Tuple<int,int,int,double>>(); for (var i = 0; i < c; i++) { var arr_o = lm.InvokeMethod("get_Item", i); var contLength = arr_o.GetFieldValue<int>("_contentLength"); var length = arr_o.GetFieldValue<int>("_length"); var offset = arr_o.GetFieldValue<int>("_offset"); var width = arr_o.GetFieldValue<double>("_width"); lineMetrics.Add(new Tuple<int, int, int, double>(contLength, length, offset, width)); } var o30 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y0), true); var o31 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y1), true); var o32 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y2), true); private int GetLineIndexFromPoint(List<Tuple<int, int, int, double>> lm, double _lineHeight, Point point, bool snapToText) { if (point.Y < 0.0) return !snapToText ? -1 : 0; if (point.Y >= _lineHeight * (double)lm.Count) { if (!snapToText) return -1; return lm.Count - 1; } int index = -1; int num1 = 0; int num2 = lm.Count; while (num1 < num2) { index = num1 + (num2 - num1) / 2; var lineMetric = lm[index]; double num3 = _lineHeight * (double)index; if (point.Y < num3) num2 = index; else if (point.Y >= num3 + _lineHeight) { num1 = index + 1; } else { if (!snapToText && (point.X < 0.0 || point.X >= lineMetric.Item4)) { index = -1; break; } break; } } if (num1 >= num2) return -1; return index; }
Wir kommen nicht zum Debuggen. o30, o31 und o32 sind 8510, 8511 bzw. 8512. So wie sie sein sollten! Aber o20, o21 und o22 stimmen nicht mit ihnen überein. Wie so? Wir haben den Code fast nicht geändert. Fast? Und hier kommt die Einsicht.
var lh = textView.GetFieldValue<double>("_lineHeight");

Das ist der Grund - der Unterschied beträgt
0,0009375 . Wenn wir außerdem die Anhäufung von Fehlern schätzen - wir multiplizieren mit 8511, erhalten wir 7,9790625. Dies ist nur etwa die Hälfte von lineHeight, und daher fliegt der Punkt bei der Berechnung der Koordinaten außerhalb der gewünschten Linie und fällt auf die nächste. Dieselbe Variable (in ihrer Bedeutung) wurde auf zwei verschiedene Arten berechnet und stimmte plötzlich nicht mehr überein.
Daraufhin habe ich beschlossen aufzuhören. Es ist möglich, wirklich herauszufinden, warum sich die Spaltenhöhe als unterschiedlich herausgestellt hat, aber ich sehe nicht viel Sinn. Es ist zweifelhaft, dass Microsoft dies beheben wird, daher schauen wir uns Krücken an, um sie zu umgehen. Reflexionskrücke - stellen Sie die richtige
_lineHeigh entweder an der einen oder anderen Stelle ein. Es klingt dumm, wahrscheinlich langsam und höchstwahrscheinlich unzuverlässig. Sie können auch Ihre eigenen Zeilen parallel zur TextBox verwalten und Zeilen daraus entnehmen, da das Abrufen der Zeilennummer an der Cursorposition ordnungsgemäß funktioniert.
Fazit
Von unerfahrenen Programmierern können Sie häufig etwas über Fehler im Compiler oder in Standardkomponenten hören. In Wirklichkeit sind sie nicht so häufig, aber dennoch ist niemand vor ihnen sicher. Haben Sie keine Angst, in das benötigte Werkzeug zu schauen - es ist aufregend und interessant.
Schreibe einen guten Code!
Andere Blog-Artikel
→
Maschinelles Lernen bei offensiver SicherheitKein Auto kann mich ersetzen. Muhaha ha. Ich hoffe es
→
Wo in IPv6 ein Anführungszeichen eingefügt werden sollDie Jungs wissen, wo und was sie schieben müssen, um es gut zu machen. Nach solchen Worten werden sie mich sicher durch einen Roboter ersetzen. Mi! UHF!