вторник, 21 февраля 2023 г.

Случай с загадочной ошибкой "out of bounds" из CreateUri и memmove

Это перевод The case of the mysterious "out of bounds" error from CreateUri and memmove. Автор: Реймонд Чен.

Один клиент пытался понять, почему его программа вылетала с ошибкой E_BOUNDS ("out of bounds"), возбуждаемой из вызова метода CreateUri:
combase!RoOriginateErrorW+0x50
wincorlib!Platform::Details::ReCreateFromException+0x40
contoso!`__abi_translateCurrentException'::`1'::catch$0+0x10
contoso!memmove+0x217f4
contoso!Windows::Foundation::IUriRuntimeClassFactory::CreateUri+0x44
contoso!Contoso::DashboardView::DashboardView_obj1_Bindings::Update_ViewModel_Layout_Groups+0x50
contoso!Contoso::DashboardView::DashboardView_obj1_Bindings::Update_ViewModel_Layout+0xe4
contoso!Contoso::DashboardView::DashboardView_obj1_Bindings::PropertyChanged+0x1134
contoso!XamlBindingInfo::XamlBindingTrackingBase::PropertyChanged+0x30
Судя по стеку, процедура копирования памяти memmove вызывала высокоуровневое исключение RTL C++/CX E_BOUNDS — что не имеет никакого смысла. Ещё более загадочно то, что memmove была вызвана из метода CreateUri интерфейса IUriRuntimeClassFactory, но код клиента DashboardView вообще не работает с URI. Похоже, что этот стек вызовов - это просто какая-то чепуха.

Что ж, попробуем раскрутить эту чушь.

Что касается таинственного memmove: обратите внимание на смещение 0x217f4. Маловероятно, что размер примитивной функции копирования памяти превышает 100 Кб. Тогда что тут происходит? Ну, это просто какой-то код, который, вероятно, был перемещен в редко используемую кодовую страницу подальше от остального кода. Ну и так случилось, что ближайший к нему символ — memmove:
    xor     ecx,ecx
    call    contoso!__abi_translateCurrentException
    int     3       ; memmove+0x217f4
Да, так и есть: это код повторного возбуждения исключения. Поскольку исключения случаются редко, то оптимизатор может переместить весь связанный с исключениями код в отдельные страницы, чтобы они не занимали драгоценное место в часто используемых страницах кода.

Хорошо, тогда почему же метод CreateUri выбрасывает исключение "out of bounds"?

Подождите, а вы уверены, что это действительно метод CreateUri?

Я посмотрел на записи выше по стеку и спросил себя: "почему код привязки данных (data binding) вызывает CreateUri?".

Код привязки данных автоматически создается компилятором XAML; он не присутствует в исходном коде программы. Вместо того, чтобы пытаться понять, как собрать и построить их проект (чтобы я мог извлечь автоматически сгенерированный файл), возможно, у меня получится разобраться по стеку и исходному коду.

Одно из основных предположений о коде в целом состоит в том, что люди, которые пишут код — не садисты. Это означает, что имена функций обычно описывают то, что они делают, имена переменных обычно описывают то, что они представляют, и так далее. Поэтому, когда я вижу класс с именем DashboardView_obj1_Bindings, я предполагаю, что этот класс предназначен для работы с привязками некоторого объекта внутри DashboardView, и поскольку у него есть метод с именем Update_ViewModel_Layout_Groups, он, вероятно, как-то связан с обновлением привязки чего-то с именем, включающим в себя слова ViewModel, Layout и Groups.

Я просмотрел DashboardView.xaml в исходном коде программы и, действительно, нашёл там слово ViewModel в элементах, которые, как оказалось, связаны с привязкой данных:
<ContentControl
    Grid.Row="0"
    x:Name="TogglesGroup"
    IsTabStop="False"
    Width="360"
    Content="{x:Bind ViewModel.Layout.Groups[0], Mode=OneWay}"
    ContentTemplateSelector="{StaticResource DashboardGroupTemplateSelector}"/>
Это было не первое использование x:Bind в разметке XAML, что не соответствует obj1, но другие части совпадают (Layout и Groups), поэтому я списал это на "возможно, компилятор XAML создаёт привязки в порядке, отличном от порядка их появления в разметке».

Как эта привязка могла вызвать исключение "out of bounds"? Ну, тут записана операция с индексом 0, так что, возможно, коллекция Groups была пуста?

Я посмотрел на метод Update_ViewModel_Layout_Groups, чтобы убедиться, что эта теория соответствует действительности:
; Update_ViewModel_Layout_Groups:
    test    rdx,rdx
    je      ...
    mov     qword ptr [rsp+8],rbx
    mov     qword ptr [rsp+18h],rbp

    push    rsi
    push    rdi
    push    r14
    sub     rsp,20h
    mov     rbp,rdx
    mov     rsi,rcx
    test    r8d,0C0000001h
    je      ...

    xor     edx,edx
    mov     rcx,rbp
    call    contoso!Windows::Foundation::IUriRuntimeClassFactory::CreateUri
Функция начинается с раннего завершения (без формирования стекового фрейма), если первый параметр равен нулю (это код метода C++, поэтому RCX содержит Self, а RDX содержит первый "настоящий" параметр метода). Я понятия не имею, как работает связывание, поэтому предположим, что это часть его логики.

А если параметр не равен нулю, то мы создаем правильный фрейм стека, проверяем некоторые биты в третьем параметре и, если они установлены, мы вызываем... простите, что? CreateUri с nil? Это же бессмысленно. XAML не запрашивает URI, зачем ему пытаться создать URI из пустой строки?

Опытный разработчик сообразит, что клиент (снова) был обманут оптимизатором.

Параметр Self для вызова CreateUri должен быть IUriRuntimeClassFactory, но это не то, что мы передаем: мы передаем первый формальный параметр (который не является IUriRuntimeClassFactory). Это значит, что перед нами вовсе не вызов CreateUri.

На самом деле, это — вызов метода GetAt интерфейса IVector, а параметр метода равен нулю, потому что нам нужен объект с нулевым индексом. Методы GetAt и CreateUri были свёрнуты оптимизатором (что означает, что они занимают одно и то же место в машинном коде), потому что они побайтно идентичны! Они оба реализуют логику "вызови метод с одним параметром по индексу 6 из VMT объекта". Для IUriRuntimeClassFactory этим методом будет CreateUri со строковым параметром, а для IVector этот метод — GetAt, а параметр — индекс.

Благодаря этому объяснению клиент понял, что у него действительно была нерешенная проблема, которая гласила: "Если наш файл настроек поврежден, то у нас не будет групп", а это исключение просто являлось альтернативным проявлением этой проблемы.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.