среда, 7 декабря 2022 г.

CIL Tools v2.5 is released


The new version contains the following changes:

CilTools.BytecodeAnalysis

  • Add IReflectionInfo interface to enable custom properties on reflection objects (implemented by classes in CilTools.Metadata)
  • Add CilInstruction.ToSyntax()
  • Add Disassembler.GetAssemblyManifestSyntaxNodes
  • Add IParamsProvider interface (enables getting method parameters without resolving external assembly references, implemented by classes in CilTools.Metadata)
  • Support parameters and return type custom attributes
  • Support field custom attributes
  • Support .override and .vtentry directives
  • Support events in type disassembler
  • Support vararg sentinel (...) in method signatures
  • Support specialname and rtspecialname attributes on methods
  • Skip assembly name for types in the same assembly
  • Escape special characters in identifiers
  • Escape slash in string literals
  • Fix base type syntax in GetTypeDefSyntax
  • Fix TypeSpec.IsValueType for byref target types and generics
  • Fix extra whitepaces after directive names
  • Fix constructors to have void return type in disassembled CIL
  • Fix ldtoken syntax for types
  • Fix CilParserException when exception handler block closes after the last instruction in the method body
  • Fix literal syntax for enum and boolean types
  • Fix serializable attribute handling
  • Fix type name representation in syntax API (now namespace is handled as a separate identifier token)
  • Fix detection of <Module> type (global fields and functions)

CilTools.Runtime

  • Implement GetReferencedAssemblies and ManifestModule on DynamicMethodsAssembly

CilTools.CommandLine

  • Add view-source command
  • Add fileinfo command
  • Add support for viewing assembly manifest to view command
  • Add assembly disassembling support to disasm command
  • Roll-forward to next major versions of .NET Runtime
The view-source command enables you to display source code of the specified method based on info in debugging symbols (if they are present) or decompiled source (currently only a limited decompilation support for methods without body, i.e, abstract or pinvoke). 

The fileinfo command displays information about assembly files, such as PE header data or assembly-level custom attributes.

The Roll-forward feature means that you can run the app on .NET 5.0+, even though it targets .NET Core 3.1.

CIL View

  • Add Source Link support for Portable PDB symbols
  • Add source code syntax highlighting
  • Add instruction info
  • Add IL syntax highlighting in SourceViewWindow
  • Add support for viewing assembly manifest and Export assembly to file menu command
  • Add Recent files menu
  • Add HTML help
  • Change default filter in Open File dialog to include all supported file types (instead of only .dll and .exe)
  • Disable formatted view for .il files larger then 1 MB
  • Support viewing separate types from .il files
  • Load .il files in background

New Source view UI look with syntax highlighting:



суббота, 9 июля 2022 г.

ErrLib v1.1 update

The new version of ErrLib was released. Changes in the new version: 



понедельник, 2 мая 2022 г.

Обновление Small Media Player - v2.4

Вышла новая версия Small Media Player: загрузить на Github Releases.

Изменения в новой версии:

  • Теперь для воспроизведения файла проигрыватель сначала пытается использовать режим Media Foundation, а если файл воспроизвести в нем невозможно, использует DirectShow (ранее было наоборот)
  • Добавлена возможность просмотра информации о форматах мультимедиа в режиме Media Foundation
  • Переработан регулятор громкости, так чтобы изменение уровня громкости в режимах Media Foundation и DirectShow происходило одинаково
  • Исправлены проблемы с производительностью при перемотке на большой промежуток времени в режиме Media Foundation
  • Исправлено блокирование воспроизведения модальным диалоговым окном при возникновении ошибки в режиме Media Foundation
  • Исправлена ошибка Access violation при завершении воспроизведения файла в режиме Media Foundation
  • Исправлена некорректная перерисовка окна видео во время паузы
  • Исправлена ошибка при попытке воспроизвести видеофайл на машине, не имеющей ни одного активного звукового устройства

четверг, 21 апреля 2022 г.

Подключение к базам данных в С++ с помощью ODBC

Хотя С++ обычно используется для низкоуровневых программ, иногда в нем возникает необходимость работать с реляционными базами данных. Для этого существует стандартный программный интерфейс ODBC. В данной статье мы рассмотрим использование ODBC на примере Windows, SQL Server Express и Visual Studio.

Обзор ODBC

Open Database Connectivity (ODBC) - процедурный программный интерфейс, который позволяет прикладным программам выполнять SQL-запросы к различным источникам данных и получать результаты в табличной форме. Модуль, который добавляет поддержку определенных источников данных (т.е. СУБД), называется драйвером. ODBC можно использовать в С/С++ напрямую, в .NET через управляемые обертки, или в других языках, если их разработчики предоставляют для этого стандартные библиотеки.

ODBC реализует несколько стандартов:

В Windows SDK для Visual C++ реализация ODBC находится в заголовочном файле sqlext.h и библиотеках odbc32.lib и odbccp32.lib.

Основные функции ODBC, которые нам понадобятся для подключения к БД:

  • SQLDriverConnect - Подключение к источнику данных
  • SQLPrepare - Подготовка запроса (компилирует запрос, чтобы последующие многократные выполнения этого запроса происходили быстрее)
  • SQLExecute - Выполнение запроса
  • SQLFetch - Получение записи из результатов запроса
  • SQLGetData - Получение поля записи
  • SQLDisconnect - Закрытие соединения с источником данных

Список всех функций можно посмотреть в ODBC Reference.

Источники данных ODBC

Для подключения к источнику данных необходимо, чтобы на компьютере был установлен драйвер. В Windows входят стандартные драйверы для следующих источников:

  • Microsoft Access (2003 и ранее)
  • Microsoft Excel (2003 и ранее)
  • Microsoft SQL Server (старая версия)
  • Paradox
  • dBASE
  • Текстовые файлы

Все эти драйвера 32-битные. Так как DLL драйвера грузится приложением, это значит, что их можно использовать только из 32-битного приложения. 64-битные драйвера включены только в серверные Windows, и для их использования нужен MDAC 2.7 SDK (ODBC 64-Bit Information).

Чтобы использовать другие СУБД или добавить поддержку 64-битных приложений в клиентских ОС, понадобится скачать и установить драйвера. Например, драйвер SQL Server можно скачать здесь: https://docs.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server

Источник данных задается строкой подключения, которая имеет следующий вид:

Driver={<Driver>};DSN='';SERVER=<Server>;DATABASE=<Database>;

Параметры строки соединения

Driver: имя драйвера. Распространенные значения:

  • SQL Server
  • SQL Server Native Client XX.0 (где ХХ - версия. Например SQL Server Native Client 11.0 для SQL 2012)
  • Microsoft Access Driver (*.mdb, *.accdb)
  • Microsoft Excel Driver (*.xls)

Драйвер “SQL Server” - это старый стандартный драйвер, который включен в Windows, но не поддерживает возможности новых версий. SQL Server Native Client - это уже более новая версия, которую надо устанавливать.

  • Server: путь к экземпляру вида (IP или имя сервера)\(имя экземпляра). Точка означает localhost. Если используется экземпляр по умолчанию, имя экземпляра и косую черту нужно опустить
  • Database: имя БД, уже присоединенной к серверу SQL Server.
  • AttachDBFileName: путь к файлу БД SQL Server для подключения (Внимание: При таком сценарии он будет открыт монопольно, пока вы работаете с соединением!).
  • DBQ: путь к файлу БД для Access и других СУБД
  • CREATE_DB: путь к файлу БД, если его нужно создать
  • DSN: имя именованного источника данных (его можно создать в панели управления: Администрирование - Источники данных ODBC)

Полный список доступен в документации: DSN and Connection String Keywords and Attributes

Примеры строк соединения для разных драйверов:

Driver={SQL Server};DSN='';SERVER=.\sqlexpress;DATABASE=mydatabase;
Driver={Microsoft Excel Driver (*.xls)};DSN='';CREATE_DB="C:\test\newfile.xls";DBQ=C:\test\newfile.xls;READONLY=0;
Driver={Microsoft Access Driver (*.mdb, *.accdb)};DSN='';DBQ=C:\users.mdb

Пример кода для вывода таблицы из БД

Следующий пример кода на С++ демонстрирует подключение к БД и вывод в консоль содержимого таблицы. Он предполагает, что у вас локально развернут SQL Server .\sqlexpress с базой данных base, в которой создана таблица Users.

#include <stdio.h>
#include <Windows.h>
#include <sqlext.h>
#include <locale.h>

WCHAR szDSN[] = L"Driver={SQL Server};DSN='';SERVER=.\\sqlexpress;DATABASE=base;";
WCHAR query[] = L"SELECT * FROM Users";

void DisplayError(SQLSMALLINT t,SQLHSTMT h) {

    //Получение информации об ошибке SQL
    SQLWCHAR       SqlState[6], Msg[SQL_MAX_MESSAGE_LENGTH];
    SQLINTEGER    NativeError;
    SQLSMALLINT   i, MsgLen;
    SQLRETURN     rc;

    SQLLEN numRecs = 0;
    SQLGetDiagField(t, h, 0, SQL_DIAG_NUMBER, &numRecs, 0, 0);  

    i = 1;
    while (i <= numRecs && (rc = SQLGetDiagRec(t, h, i, SqlState, &NativeError,
            Msg, sizeof(Msg), &MsgLen)) != SQL_NO_DATA) {
        wprintf(L"Error %d: %s\n", NativeError, Msg);
        i++;
    }
}

int main()
{   
    HENV    hEnv = NULL;
    HDBC    hDbc = NULL;
    HSTMT hStmt = NULL;
    int iConnStrLength2Ptr;
    WCHAR szConnStrOut[256];
    SQLINTEGER rowCount = 0;
    SQLSMALLINT fieldCount = 0, currentField = 0;
    SQLWCHAR buf[128],colName[128]; 
    SQLINTEGER ret;

    RETCODE rc; //Код статуса ODBC API

    setlocale(LC_ALL, "Russian");

    /* Выделение дескриптора среды */
    rc = SQLAllocEnv(&hEnv);
    /* Выделение дескриптора соединения */
    rc = SQLAllocConnect(hEnv, &hDbc);

    /* Подключение к БД */
    rc = SQLDriverConnect(hDbc, NULL, (WCHAR*)szDSN,
        SQL_NTS, (WCHAR*)szConnStrOut,
        255, (SQLSMALLINT*)&iConnStrLength2Ptr, SQL_DRIVER_NOPROMPT);

    if (SQL_SUCCEEDED(rc))
    {
        /* Подготовка запроса SQL */
        rc = SQLAllocStmt(hDbc, &hStmt);
        rc = SQLPrepare(hStmt, (SQLWCHAR*)query, SQL_NTS);      

        /* Выполнение запроса */
        rc = SQLExecute(hStmt);
        if (SQL_SUCCEEDED(rc))
        {
            wprintf(L"\n- Columns -\n");

            SQLNumResultCols(hStmt, &fieldCount);
            if (fieldCount > 0)
            {   
                for (currentField = 1; currentField <= fieldCount; currentField++)
                {
                    SQLDescribeCol(hStmt, currentField,
                        colName, sizeof(colName), 0, 0, 0, 0, 0);
                    wprintf(L"%d: %s\n", (int)currentField, colName);
                }
                wprintf(L"\n");

                /* Получение записей из результатов запроса */                               

                rc = SQLFetch(hStmt);
                while (SQL_SUCCEEDED(rc))
                {
                    wprintf(L"- Record #%d -\n", (int)rowCount);

                    for (currentField = 1; currentField <= fieldCount; currentField++)
                    {
                        rc = SQLGetData(hStmt, currentField, SQL_C_WCHAR, buf, sizeof(buf), &ret);

                        if (SQL_SUCCEEDED(rc) == FALSE) {
                            wprintf(L"%d: SQLGetData failed\n", (int)currentField);
                            continue;
                        }

                        if (ret <= 0) {
                            wprintf(L"%d: (no data)\n", (int)currentField);
                            continue;
                        }

                        wprintf(L"%d: %s\n", (int)currentField, buf);
                    }                   

                    wprintf(L"\n");
                    rc = SQLFetch(hStmt);
                    rowCount++;
                };                  

                rc = SQLFreeStmt(hStmt, SQL_DROP);

            }
            else
            {
                wprintf(L"Error: Number of fields in the result set is 0.\n");
            }                   

        }
        else {
            wprintf(L"SQL Failed\n");
            DisplayError(SQL_HANDLE_STMT, hStmt);
        }
    }
    else
    {
        wprintf(L"Couldn't connect to %s\n", szDSN);    
        DisplayError(SQL_HANDLE_DBC, hDbc);
    }

    /* Отключение соединения и очистка дескрипторов */
    SQLDisconnect(hDbc);
    SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
    SQLFreeHandle(SQL_HANDLE_ENV, hEnv);

    getchar();
    return 0;
}

воскресенье, 27 марта 2022 г.

Диагностика ошибок в программах с помощью дампов памяти

Обычно для поиска и устранения проблем в программах разработчики используют интегрированную среду разработки (IDE), запуская под отладчиком живой процесс и исследуя его память. Но иногда проблема возникает редко и только в специфичных условиях, поэтому ее не получается воспроизвести на машине разработчика, где установлен отладчик. Тогда есть несколько путей: удаленная отладка, использование логов или данных телеметрии, а также запись и анализ дампов памяти. Последний способ мы и рассмотрим в данной статье. Рассматривать работу с дампами мы будем на примере Windows 7+ и Visual Studio 2012+. Статья в основном рассчитана на разработчиков, но может также быть полезна администраторам и продвинутым пользователям.

Бесплатную для некоммерческого использования версию Visual Studio можно скачать здесь: https://visualstudio.microsoft.com/ru/vs/community/. При установке выберите рабочую нагрузку “Desktop development with C++”.

Настройка компьютера для записи дампов пользовательской памяти

Много в интернете понаписано про использование дампов памяти ядра для диагностики синих экранов (BSoD), но менее известно, что аналогичный механизм можно использовать и для обычных приложений (уровня пользователя). Но по умолчанию такие дампы при сбоях программ не сохраняются, нужны специальные настройки в реестре: Collecting User-Mode dumps.

  1. Открываем редактор реестра (Пуск - Выполнить - regedit).
  2. Переходим в раздел HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting
  3. Создаем раздел LocalDumps, если его нет
  4. Создаем в разделе ключи:
  • DumpFolder (Расширяемый строковый параметр/REG_EXPAND_SZ) - Путь к каталогу, в который будут сохраняться дампы
  • DumpType (Параметр DWORD) - Тип дампа (задаем значение 1 - минидамп)
  • DumpCount (Параметр DWORD) - Максимальное количество дампов в каталоге, по умолчанию 10

Минидамп отличается от полного дампа только компактностью, т.е. данные сжимаются более эффективно. Вся необходимая информация для диагностики в минидамп включена, поэтому смело выбираем его. Подробнее о типах дампов можно почитать здесь: User mode dump files.

Максимальное количество дампов можно задать для экономии дискового пространства, на случай, если какая-то программа постоянно будет падать и плодить дампы. Когда количество дампов превысит максимальное, старые будут перезаписаны.

Готово, теперь при критическом сбое в любой программе (необработанное исключение SEH/C++/.NET) в указанном каталоге будет создан файл дампа памяти (.dmp). Файл дампа содержит информацию об адресном пространстве упавшего процесса в момент сбоя, которую мы далее будет анализировать.

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

Тестовая программа для демонстрации сбоя

Чтобы продемонстрировать работу с дампами, напишем такую простую программу на С++:

#include <stdio.h>

int Divide(int x, int y)
{
	return x/y;
}

void Crash(){
	printf("%d",Divide(1,0));
}

int main()
{
	Crash();	
}

Как можно видеть из кода, программа пытается разделить целое число на ноль, и поэтому должна выдать при запуске необработанное исключение SEH. Соберем программу как консольное приложение С++ в Visual Studio (можно использовать Express), в конфигурации Debug. Запустите программу и убедитесь, что она упала и дамп записался. Если вы переносите программу на другой компьютер, переносите ее вместе в символами (файлом .pdb), иначе анализ дампа будет затруднен.

Анализ дампа памяти в помощью Visual Studio

Популярный инструмент для анализа дампов - это WinDBG, но часто проще использовать Visual Studio. Для анализа дампа берем файл .dmp, нажимаем по нему правой кнопкой мыши и выбираем Открыть с помощью - Visual Studio. Отроется окно Visual Studio:

Дамп, открытый в Visual Studio

Нажимаем в нем Отладка с использованием только машинного кода (Debug with Native only). Откроется окно, в котором отображаются данные об исключении:

Исключение в Visual Studio

В окне можно увидеть код исключения (NTSTATUS), его описание, информацию о последовательности вызовов функций, которая привела к нему, и даже строку кода в файле .cpp исходного кода, на которой остановилось выполнение программы. Нажмите Прервать (Break), чтобы остановить отладку и изучить состояние программы более подробно. В окне Стек вызовов (Call stack) отображаются функции, последовательность которых привела к сбою. Каждый элемент списка называется кадром стека. Для кадра стека отображается модуль (EXE/DLL) и название функции после восклицательного знака. Двойным щелчком по функции можно перейти к ее исходникам, если они доступны. Также можно использовать меню Debug - Windows - Threads для отображения всех потоков, которые были активны в момент ошибки. В этом окне можно перейти и к стекам других потоков.

Из рисунка также видно, что для кадра стека из kernel32.dll не отобразилось имя функции. Это происходит, потому что символы для системных библиотек по умолчанию не загружаются. Рассмотрим, как включить загрузку символов.

Настройка загрузки символов в Visual Studio

Отладочные символы - это файл, который содержит информацию, необходимую для отладки. В первую очередь это таблица соответствия адресов памяти в исполняемом файле и имен функций, к которым относится код по этому адресу. Также символы могут содержать и информацию о строках файла исходников, соответствующих адресам кода в бинарнике (для продуктов с закрытым кодом этого обычно не будет). Для включения загрузки символов с серверов Microsoft выбираем в меню Options and settings - Debugging - Symbols. В открывшемся окне устанавливаем галку Microsoft symbol servers.

Настройка символов в Visual Studio

Можно включить загрузку всех символов, но тогда они грузиться будут долго. Пока включим лишь для одной библиотеки, для этого установим переключатель в значение Only specified modules, и нажмем Specify modules. В открывшемся окне добавим kernel32.dll, и отметим его галкой. Теперь переоткроем дамп, дождемся загрузки символов, и увидим, что название функции из kernel32.dll появилось:

Стек вызовов в Visual Studio

Для библиотек собственной разработки также нужно будет включить загрузку символов, только вместо сервера символов указать путь к каталогу. Обратите внимание, что в нашем упрощенном случае символы для модуля программы DumpTest.exe загрузились автоматически, так как на компьютере и бинарник программы и символы лежали по тому же пути, что и сохранен в дампе. При переносе дампа на другой компьютер это в общем случае может не соблюдаться, тогда понадобится либо прописать путь для каталога с символами, либо загрузить символы вручную.

Для просмотра списка модулей и информации о загруженных символах выберите в меню Debug - Windows - Modules

Модули в Visual Studio

Если в графе Symbol status отображается, что символы не прогрузились, можно подсунуть их вручную. Для этого нужно нажать по модулю правой кнопкой мыши и выбрать в меню Load symbols from - Symbol path. В появившихся диалоговых окнах нужно будет выбрать исполняемый файл и файл символов (иногда только что-то одно, если второе нашлось автоматически). При этом следует предоставить именно тот исполняемый файл, который использовался процессом, и именно те символы, которые соответствуют этому исполняемому файлу. В исполняемом файле в Debug directory прошивается специальный идентификатор символов; если файл символов ему не соответствует, отладчик пошлет вас подальше…


Таким образом, мы научились сохранять и анализировать дампы памяти для диагностики трудновоспроизводимых сбоев в программах для Windows. В Linux/Unix, к слову, аналогом этого метода будет использование Core dumps и их анализ с помощью GDB. Один из главных плюсов дампов памяти в том, что не нужно вносить никаких изменений в код, как это было бы в случае добавления дополнительного логирования. Если вы хотите реализовать аналогичную диагностику, с получением стеков, через логирование, для этого можно использовать библиотеку ErrLib.

воскресенье, 20 февраля 2022 г.

CIL Tools update 2.4

 A new CIL Tools update is released. The update contains a new project, CilTools.CommandLine, as well as some changes and fixes to existing projects.

Download on GitHub releases

Changes in the new version:

General

  • New project: CilTools.CommandLine

CilTools.BytecodeAnalysis

  • Add support for including bytecode size and source code lines in disassembler output
  • Add support for .entrypoint directive
  • Add support for generic constraints
  • Add support for properties in disassembler
  • Add ICustomMethod interface as a base for custom method implementations to replace CustomMethod base class. This means that custom method implementations can now be derived from MethodInfo or ConstructorInfo.
  • Improve generics support. Generic context is now passed correctly to generic parameter types is more cases; this enables getting generic parameter names and their declaring methods/types.
  • Fix TypeSpec.IsGenericParameter for byrefs
  • Fix ldtoken syntax for methods
  • Fix string literal escaping in disassembler to use ECMA-335 rules

CilTools.Runtime

  • Update custom method implementations to derive from MethodInfo/ConstructorInfo and implement ICustomMethod

CIL View

  • Add support for opening C#/VB code and MSBuild projects
  • Add Show source support
  • Add options to include bytecode size and source code lines (from PDB) in disassembler output
  • Add support for opening IL source files
  • Add support for .entrypoint directive
  • Add Export type to file menu command
  • Add support for generic constraints
  • Add support for disassembling properties
  • Add support for interactive method execution
  • Use .NET Core runtime directory when resolving dependencies for .NET Core assemblies
  • Use runtime directory of the inspected process instead of current runtime directory for assembly resolution when opening a process
  • Load assembly images from memory instead of files when opening a process
  • When an assembly contains a single type, automatically navigate to that type
  • When the type contains only one non-constructor method and no other members (like fields), automatically navigate to that single method when type is selected
  • Change member identifier color to more visible with lower brightness
  • Fix ldtoken syntax for methods
  • Fix string literal escaping in disassembler to use ECMA-335 rules