пятница, 8 июня 2012 г.

IOCTL декодер

Бывает нужно из IOCTL извлечь номер функции или определить буферизированный запрос или нет. Есть вот такой онлайн декодер . Мы написали аналогичный по функциональности скрипт для windbg.
Пользоваться просто:
2: kd> !py ctlcode 0x1401c8
Device type: FILE_DEVICE_NETWORK_FILE_SYSTEM   Function: 0x72(114)   Method: BUFFERED  Access:  FILE_ANY_ACCESS
Взять можно здесь.

пятница, 1 июня 2012 г.

Отладка WFP callout драйверов

Начиная с Windows Vista MS упорно продвигает свой фреймворк WFP ( Windows Filtering Platform ). В предверии Windows 8 Microsoft еще более закручивают гайки: теперь продукт не может получить логотип о совместимоcти с Windows 8, если он содержит TDI или LSP фильтры. Между нами, скажу по секрету шепотом свое мнение: WFP - это полное говно, писать на нем определенные вещи крайне неудобно. Но это все эмоции, говно - не говно, отлаживать драйвера надо. Кто знаком с WFP, тот помнит, что там все делает через одно место, а именно через функцию classsifyFn. В зависимости от версии WFP, эта функция может имееть разные сигнатуры, но в любом случае, они содержат параметеры inFixedValues и inMetaValues в которых, собственно, и содержится вся информация о сетевом событии. И конечно, при отладке хочется посмотреть, что содержат эти переменные. Но это не так просто, inFixedValues - это массив переменной длины, а в inMetaValues доступность полей регулируется флагами. Поэтому, я решил написать python скрипт, который будет выводить эту информацию.

Сам скрипт можно взять здесь: wfp.py download.
Использовать его довольно просто: Ставим брейкпойнт на интересующей нас ф. classifyFn. При срабатывании брейкпойнта, вводим команду: !py wfp /fixed poi(inFixedValues).
Примерный вывод команды:

FWPS_INCOMING_VALUES0:
 Layer: FWPS_LAYER_STREAM_V4
 Value: 7
    FWPS_FIELD_STREAM_V4_IP_LOCAL_ADDRESS
      Type: FWP_UINT32
      Value: 0xaf4087c
    FWPS_FIELD_STREAM_V4_IP_LOCAL_ADDRESS_TYPE
      Type: FWP_UINT8
      Value: 0x1
    FWPS_FIELD_STREAM_V4_IP_REMOTE_ADDRESS
      Type: FWP_UINT32
      Value: 0x40040b19
    FWPS_FIELD_STREAM_V4_IP_LOCAL_PORT
      Type: FWP_UINT16
      Value: 0xc02b
    FWPS_FIELD_STREAM_V4_IP_REMOTE_PORT
      Type: FWP_UINT16
      Value: 0x50
    FWPS_FIELD_STREAM_V4_DIRECTION
      Type: FWP_UINT32
      Value: 0x0
    FWPS_FIELD_STREAM_V4_FLAGS
      Type: FWP_UINT32
      Value: 0x0
Для 64 битной платформы, потребуется указать регистр rcx ( именно через него передается inFixedValues ):
!py wfp /fixed @rcx.
Для вывода метаинформации следует использовать команду:
!py wfp /meta poi(inMetaValues)
FWPS_INCOMING_METADATA_VALUES0:
    FWPS_METADATA_FIELD_FLOW_HANDLE: 0x210
    FWPS_METADATA_FIELD_SYSTEM_FLAGS: 0x1

Теперь на примере данного скрипта рассмотрим некоторые приемы написания скрипта для pykd. Первым делом, нам нужно получить доступ к полям переменной inFixedValues. Это делается так:

inFixedValue = typedVar( "FWPS_INCOMING_VALUES0_", addr )
dprintln( " Layer: " + fwpsLayer[ inFixedValue.layerId ] )
dprintln( " Value: %d" % inFixedValue.valueCount )
Класс typedVar позволяет осуществить удобный доступ к полям стурктур - просто как к аттрибутам. Именно так выводится количество записей в массиве:
dprintln( " Value: %d" % inFixedValue.valueCount )
Поле inFixedValue.layerId содержит численный идентификатор уровня фильтрации. Но мы хотим получить его символьное представление, которое задается энумератором FWPS_BUILTIN_LAYERS_. И тут нам приходит на помощь новый функционал pykd версии 0.1.x:
fwpsLayer = typeInfo( "FWPS_BUILTIN_LAYERS_" ).asMap()
Вызов typeInfo( "FWPS_BUILTIN_LAYERS_" ) возвращает информацию о типе FWPS_BUILTIN_LAYERS_. А метод asMap() конвертирует его в словарь вида: { число : "имя" }. Таким образом, зная идентификатор уровня фильтрации мы легко конвертируем его в символьное представление:
dprintln( " Layer: " + fwpsLayer[ inFixedValue.layerId ] )
Теперь нам необходимо вывести информацию о каждом элементе массива inFixedValue.incomingValue. Это можно было бы сделать с помощью цикла:
values = []
for i in range( 0, inFixedValue.valueCount ):
    values.append( typedVar( "FWPS_INCOMING_VALUE0_", inFixedValue.incommingValue + typeInfo("FWPS_INCOMING_VALUE0_").size() * i ).value )
Но есть способ гораздо проще, через специальный метод для работы с массивами:
values = [ x.value for x in typedVarArray( inFixedValue.incomingValue, "FWPS_INCOMING_VALUE0_", inFixedValue.valueCount ) ]
typedVarArray возвращает список переменных типа typedVar с inFixedValue.valueCount элементов и начинающийся с адреса inFixedValue.incomingValue. Далее мы используем генератор списков, чтобы трансформировать список.

Ну вот, собственно и все. Надеюсь данный скрипт будет полезен в работе и в качестве примера работы c pykd.

среда, 30 мая 2012 г.

Release 0.1.0.14

Доброго всем дня! Новая ветка pykd 0.1.x переведена в публичный статус. Релиз 0.1.0.14 рекомендован для использования. Страница загрузки: Pykd 0.1.0.14

Основные отличия от предыдущей ветки 0.0.x:

1. Для доступа к символьной информации используется библиотека MS DIA. Это позволило нам реализовать то, что не получалось сделать через Debug Engine: Доступ к битовым полям; Работа с многомерными массивами; Работа с энумераторами.

2. Новый класс dbgClient позволяет работать одновременно с несколькими отладочными сессиями.

3. Команда !pycmd реализует полноценную python консоль

4. Улучшена работа с классами исключений

5. Исправлено множество багов, написано более 200 тестов, покрывающих практически весь функционал.

6. Наконец то написано ( пока с некоторыми пробелами ) руководство пользователя

Внимание: API претерпел небольшие изменения, так что, возможно, придется переписать некоторые скрипты. Но изменения в API небольшие и портирование скриптов на pykd 0.1.x не должно отнять много времени. Кроме того, часть функционала пока не реализована ( в основном, это касается создания собственных типов через интерфейс класса typeInfo ). В следующих релизах мы обязательно восполним все пробелы.

Большое спасибо всем, кто использует наш продукт! Как всегда, мы будем очень рады любым отзывам, предложениям и, особенно, указанием на наши ошибки.

Большое спасибо всем участникам проекта, трудившимся над веткой 0.1.x! Работа длилась почти целый год. За это время код был серьезно модифицирован, существенная часть его была переписана заново. Надеемся, пользователи оценят наши усилия.


понедельник, 6 февраля 2012 г.

Обработка исключений в Boost.Python

Мы в нашем проекте стараемся использовать исключения для сигнализации об ошибках. Почему? Причин несколько: удобно при отладке - если что-то пошло не так, мы сразу видим где и что произошло и можем перейти непосредственно к исправлению ошибки не теряя времени на отладку; пользователю приходится быть более дисциплинированным - ведь исключение нельзя проигнорировать, в отличие от проверки возвращаемого значения; удобно при разработке - вместо прокидывания кода ошибки через все функции, мы просто кидаем исключение, которое к тому же может быть гораздо информативнее. В результате, мы верим, что весь комплекс pykd + скрипт становится надежным инструментом.

Но разберемся, как же исключения попадают из С++ кода, написанного с помощью Boost::Python в виртуальную машину Python? Будем двигаться итеративно.

Итерация 0: Генерируем python исключение

Сгенерировать исключение в ВМ python просто. Для этого служит функция PyErr_SetString. В нее нужно передать указатель на тип исключения и сторку с описанием ошибки. Например так:
PyErr_SetString( PyExc_IndexError, "Index out of range" );
Не очень то удобно использовать эту конструкцию внтури С++ кода. Хотелось бы кидать С++ исключение и чтобы оно волшебным образом превращалось в python исключение. К счастью, boost::python предоставляет эту возможность.

Итерация 1: транслируем исключение из C++ в python

В общем все довольно просто. Любое исключение, возникшее во время выполнения python программы внутри модуля, использующего boost::python отлавливается. Существует механизм, с помощью которого можно поучаствовать в обработке такого исключения. Для этого необходимо зарегистрировать функцию-транслятор с помощью вызова register_exception_translator. Вот как это делается:

// класс исключения, для использования в C++ коде
class PyException : public std::exception
{
public:

    PyException( PyObject*  pyObj, const std::string &desc ) :
        std::exception( desc.c_str() ),
        m_typeObj( pyObj )
        {}    

    static
    void
    exceptionTranslate(const PyException &e ) {
        PyErr_SetString( e.m_typeObj, e.what() );
    }

private:

    PyObject*       m_typeObj;
};

void myFunc() {
    throw PyException( PyExc_IndexError, "Index out of range" );
}


BOOST_PYTHON_MODULE( my_module )
{
    // регистрируем транслятор
    boost::python::register_exception_translator<PyException>( &PyException::exceptionTranslate );

    // функция, вызываемая из python программы
    boost::python::def( "myFunc", &myFunc );
}

Прекрасно, генерировать исключения python теперь удобно. Но я хочу использовать собственный тип исключений. Возможно ли это? В общем да...

Итерация 2: Использование собственного типа исключений.


Идея простая. Boost::python дает возможность импортировать типы в python программу. Наш план таков: импортируем тип в python, регистрируем функцию-транслятор С++ исключения и в коде функции-трансялтора используем наш тип исключения, который мы ранее импортировали ( и запомнили! ) в python.
// класс исключения, для использования в C++ коде
class MyException : public std::exception
{
public:

    MyException ( const std::string &desc ) :
        std::exception( desc.c_str() )
        {}    

    static void setTypeObject(PyObject *p) {
        exceptTypeObject = p;
        python::register_exception_translator<MyException>( &exceptionTranslate );
    }

private:

   static PyObject  *exceptTypeObject;

    static
    void
    exceptionTranslate(const MyException &e ) {
        boost::python::object  pyExcept(e);
        PyErr_SetObject( exceptTypeObject , pyExcept.ptr());
    }
};

void myFunc() {
    throw MyException( "Something's wrong" );
}


BOOST_PYTHON_MODULE( my_module )
{
    // функция, вызываемая из python программы
    boost::python::def( "myFunc", &myFunc );

    // регистрирем тип исключения и его транслятор
    MyException::setTypeObject( 
        boost::python::class_<MyException>( "MyException", "MyException error" ).ptr()
    );
}

Вот на этом мы могли бы остановиться. Могли бы, если бы мир был совершенен. На самом деле, после чуть более тщательного тестирования, мы приходим к заключению, что работают наши исключения не совсем верно. А именно:
не работает инструкция raise MyException - вместо генерации исключения обозначенного типа генерирует питоновское исключение, общий смысл которого в двух словах сводится к тому, что тип MyException не пригоден для генерации исключений. Вторая проблема заключается в том, что при попытке сделать иерархию исключений мы сталкиваемся с тем, что неверно работает фильтрация исключений, а именно: инструкция except BaseException не ловит исключения дочерних типов. Короче говоря, имплементация наших исключений весьма далека не только от идеала, но и просто от обычных python исключений. Будем разбираться.

Итерация 3: разбираемся

Изучив внимательно текст ошибки при попытке возбудить исключение raise MyExceptionException. Кроме того, тут надо сделать небольшой ( самостоятельный ;) ) экскурс в анатомию питонов и изучить вопрос о типах и метатипах. Если в двух словах, то тип в питоне - это такой же объект и ,соответственно, может быть динамически создан вызовом конструктора у некого типа. Вот этот "некий" тип и является метатипом. Покопавшись внутрях boost::python можно придти к заключению, что все типы, которые он импортирует, создаются через один и тот же метаттип. В итоге наш MyException имеет метатип такой же, как и другие типы, импортированные из boost::python. И он не имеет родственных связей с типами питоновских исключений. А это негативно влияет на обработку исключений питоном. Наш дальнейший план каким-то образом унаследовать MyException от питоновского типа Exception. Как же это сделать то????

Итерация 4: Как же это сделать то????

Делать то нечего, приходится отказаться от регистрации типов исключений через бустовый class_. Подсмотрев в его исходники, находим место, где создается новый питоновский тип и переписываем его примерно так:

using boost::python;

// Базовым типом у нас будет PyExc_Exception
handle<>   basedtype = handle<>(PyExc_Exception);

// Всякий объект в питоне имеет свой список свойств
dict       ob_dict;
ob_dict["__doc__"] = "MyException"

// Объект в питоне кстате может иметь много пап. Но у нас будет один - самый лучший! 
tuple      ob_bases = make_tuple( basedtype );

// Вот тут весь оргазм: 
// Py_TYPE(basedtype.get()) - вернет нам тип от объекта PyExc_Exception. Тип типа - это метатип, мы это выясняли
// object( boost::python::handle<>(Py_TYPE(basedtype.get()) ) ) - это будет у нас объект меаттипа
// Что такоей вызов SomeType( a, b, c) ? - это конструирование объекта этого типа
// Теперь собираем пазл во-едино: мы создаем новый тип, который имеет такой же метатип как и PyExc_Exception
// и является наследником PyExc_Exception
object     ob = object( boost::python::handle<>(Py_TYPE(basedtype.get()) ) )( "MyException", ob_bases, ob_dict );

// остается пристроить новорожденного в приличное место, а именно в скоуп модуля
scope().attr( "MyException" ) = ob;

Теперь остается привести все в божеский вид.

Итерация 5: божеский вид

Для приведения в божеский вид нам надо написать какой то шаблон, по своим функциям хоть чуть-чуть напоминающий class_. Как минимум, хочется наследования, пусть даже не множественного. В результате, родился такой вот шаблон. Он далек от изящества к сожалению, но у него доброе сердце )).

template< class TExcept >
struct exceptPyType{

     static python::handle<>     pyExceptType;

};

template< class TExcept, class TBaseExcept = python::detail::not_specified >
class exception  {


public:

    exception( const std::string& className, const std::string& classDesc ) 
    {
        python::handle<>   basedtype;      

        if ( boost::is_same<TBaseExcept, python::detail::not_specified>::value )
        {
            basedtype = python::handle<>(PyExc_Exception);
        }
        else
        {
            basedtype = exceptPyType<TBaseExcept>::pyExceptType;
        }

        python::dict       ob_dict;
       
        ob_dict["__doc__"] = classDesc;

        python::tuple      ob_bases = python::make_tuple( basedtype );

        python::object     ob = python::object( python::handle<>(Py_TYPE(basedtype.get()) ) )( className, ob_bases, ob_dict );

        python::scope().attr( className.c_str() ) = ob;

        exceptPyType<TExcept>::pyExceptType = python::handle<>( ob.ptr() );

        python::register_exception_translator<TExcept>( &exceptionTranslate );
    }

    static
    void
    exceptionTranslate(const TExcept &e ) {

        python::object      exceptObj = python::object( exceptPyType<TExcept>::pyExceptType )( e.what() );

        PyErr_SetObject( exceptPyType<TExcept>::pyExceptType.get(), exceptObj.ptr());
    }

};

Но со стороны описания модуля выглядит все не так уж и плохо:

BOOST_PYTHON_MODULE( mymodule )
{
  exception<MyException>( "MyException", "description" );
  exception<MyExceptionEx,MyException>( "MyExceptionEx", "description" );
}

Заключение

Почему же нам пришлось прибегать к таким извращениям? Очевидно, что boost::python далек от совершенства )). Чего же в нем не хватает? Было бы классно, если бы существовала форма функции class_, позволяющая задать базовый класс через указатель на питоноский тип.





пятница, 16 декабря 2011 г.

Новая линейка Pykd 0.1.x доступна для тестирования

Хочу представить вам новую, кардинально переработанную версию pykd.

Страница для закачки: download

Сейчас ветка 0.1.x находится в статусе альфа и имеет ряд недоделок, которые будут постепенно устраняться. Мы очень надеемся, чтои вы найдете время протестировать новую версию и сообщить нам свое мнение о новом функционале и найденных ошибках. Это позволит быстрее довести продукт до стабильного состояния.

Основные фичи новой ветки:
1) Работа с символами через библиотеку MS DIA. Это позволило реализовать доступ к битовым полям, типам-энумераторам, работать с многомерными массивами и указателями на массивы
2) Доступ к DIA выведен в интерфейс для Python. Так что можно работать с отладочной информацией самостоятельно
3) Поддерживается работа с несколькими клиентами одновременно, т.е. можно из одного скрипта управлять отладкой нескольких приложений или систем.
4) Несколько изменен API
5) Для windbg команда !pycmd предоставляет теперь полноценную консоль python, идентичную стандартной. Для команды !py улучшен вывод информации об ошибках, в том числе синтаксических

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

Дальнейший план развития такой:
Ветка 0.0.x больше развиваться не будет, будем только фиксить баги и, возможно, что-то потом интегрируем из 0.1.x.

Спасибо, что используете наш продукт :).

понедельник, 10 октября 2011 г.

Обработка исключений и точек останова

В pykd v. 0.0.20 появился новый полезный функционал: обработка исключений и точек останова. Возможность обработки этих событий реализована новыми методами класса debugEvent: onBreakpoint и onException. Для использования этого функционала, как и в случае с событиями загрузки/выгрузки модулей, необходимо реализовать свои вышеописанные обработчики. Сведения о произошедшем событии передаются в эти методы словарями, ключи и типы значений которых указаны в документации. Следует отметить, что для события срабатывания точки останова количество сформированных пар, может быть меньше, чем описано: если точка останова не обладает каким-либо свойством, данные не заносятся в словарь. А состав словаря, передаваемого в обработчик исключения, более стабилен: варьируется только список параметров исключения. Но даже, если у исключения нет параметров, то в словаре все равно окажется список, который будет пуст.

У pykd уже был класс bp, который позволяет ставить/снимать точку останова и назначать собственный обработчик. Этот класс так остался, так как имеет более простой интерфейс, но он был расширен новым конструктором, в который передается только целевой адрес. При установке такой точки, вместо вызова обработчика просто возвращается статус DEBUG_STATUS_BREAK.

Новый функционал debugEvent::onBreakpoint реализован не как замена, а как альтернатива существующему механизму. То есть в одном и том же скрипте можно использовать как механизм bp, так и переопределение метода debugEvent::onBreakpoint. Это было сделано для того, что бы можно было управлять исключениями и точками останова из одного класса - debugEvent.

Мы достаточно мало освещаем тот факт, что pykd не только расширение к WinDbg, но и полноценный модуль для языка python, который позволяет получить доступ к API Debug Engine. Поэтому пример использования обработки исключений будет нацелен именно на такой подход: samples\watchDog.py. В начале скрипт, если переданы аргументы командной строки, стартует указанный отлаживаемый процесс. Затем он ставит обработчик исключения и ожидает возникновения исключения. Но это так же полноценный скрипт для pykd, функционирующего как расширение WinDbg. Например, при вызове из WinDbg можно не указывать параметров, тогда скрипт будет следить за исключениями на отлаживаемом объекте.

Важной особенностью логики скрипта является пропуск исключений, для которых FirstChance == True. Это необходимо для того, что бы пропускать исключения, которые возможно будут обработаны самим отлаживаемым объектом, например если исключение произошло в блоке SEH'а.

Теперь запускаем скрипт командной строкой "python watchDog.py test.exe". Тем самым pykd от лица процесса python.exe создаст отлаживаемый процесс test.exe, который запустится на исполнение с предварительно установленным обработчиком исключений. В test.exe специально был включен следующий код:

__try { *(char *)0 = 1; } __except(EXCEPTION_EXECUTE_HANDLER) {}
*(char *)3 = 1;


Результат исполнения скрипта следующий:

*** shit happens
Exception code : EXCEPTION_ACCESS_VIOLATION
Exception flags : 0
Exception record : 0x0
Exception address : 0x40122C(00401010) test!main+0x21c | (00401250) test!Define_the_symbol__ATL_MIXED::Thank_you::Thank_you

Parameters :
0x1
0x3

eax=00000000 ebx=0018fa64 ecx=00000000 edx=00000000 esi=00401222 edi=00000000eip=774c15ee esp=0018fa50 ebp=0018ff40 iopl=0 nv up ei pl zr na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246ntdll!ZwRaiseException+0x12:774c15ee 83c404 add esp,4

ChildEBP RetAddr Args to Child
0018ff40 004017f3 00000001 00302330 00302370 ntdll!ZwRaiseException+0x120018ff88
76ae339a 7efde000 0018ffd4 774d9ed2 test!__tmainCRTStartup+0xfb0018ff94
774d9ed2 7efde000 62b6a849 00000000 kernel32!BaseThreadInitThunk+0x120018ffd4
774d9ea5 0040184a 7efde000 ffffffff ntdll!RtlInitializeExceptionChain+0x630018ffec
00000000 0040184a 7efde000 00000000 ntdll!RtlInitializeExceptionChain+0x36

Как видно по второму параметру исключения (адрес памяти - 0x3) скрипт поймал именно необрабатываемое исключение: использования неверного указателя *(char *)3 = 1. Если включить дополнительную трассировку (например, вывод в консоль) для FirstChance == True, то можно увидеть, что onException срабатывает 3-а раза, первые 2-а из которых игнорируются, так как могут быть обработаны отлаживаемым объектом.

воскресенье, 9 октября 2011 г.

Release 0.0.20

Всем доброго дня! Спешу представить очередной релиз pykd - версия 0.0.20.

Для тех, кто предпочитает словам дела, страница загрузки и удачи в использовании! Я же хотел кратко пробежаться по составу релиза:

Управление отладчиком:


Добавлены следующие функции: breakin, attachProcess, attachKernel, debuggerPath
Наличие функции breakin позволяет написать с помощью pykd свой отладчик. А функции attachProcess и attachKernel в этом помогут ( напомню, что ранее уже были реализованы функции loadDump и createProcess ).

Функция debuggerPath возвращает полный путь к исполняемому модулю программы, использующей pykd. Если вы, к примеру, захотите модифицировать файл symsrv.ini - она может быть полезна.

В ближайшее время мы опубликуем тутор по разработке отладчика на PySide. Следите за блогом!

класс cpuReg


Дополнен такой казалось бы малозначительной деталью, как конструктор с параметром "индекс регистра". Это позволяет реализовать перечисление всех регистров CPU:
def getRegisterSet(self):
    regSet=[]
    try:
        i = 0
        while True:
            reg = pykd.cpuReg(i)
            regSet.append(reg)
            i += 1

    except pykd.BaseException:
        pass

    return regSet

класс typedVar


Добавлен конструктор, принимающий имя переменной нужного типа. При этом сам тип указывать не надо, он будет взят из отладочной информации. Т.е достаточно написать:
var = typedVar( myModule.myVar )

Это особенно удобно при работе с переменными, тип которых заранее не известен. Например, с шаблонами С++.

класс disasm


Добавлен метод assembly, позволяющий изменять код по укзанному ( по умолчанию: по текущему ) смещению:
nt = loadModule( "nt" )
dasm = disasm( nt.NtCreateFile )
dasm.assembly( "int 3" )

getProcessorType


Возвращает тип процессора. Функция вернет строку, описывающую тип физического процессора. Сравните с getProcessorMode, которая возвращает текущий режим. Вот так, к примеру, можно было бы реализовать команду !sw из стандартного расширения wow64ext:
if getProcessorType() == "X64":
  setProcessorMode( {"X64": "X86", "X86": "X64"}[ getProcessorMode() ] )

багфикс


9219: улучшена поддержка многопоточности. Функции, которые могут надолго заблокировать исполнение python программы ( такие как go() и dbgCommand() ) сохраняют состояние потока, что позволяет планировщику python выполнять код в других потоках

9518: Исправлен баг с выводом текста через команду dprint(ln). До этого текст, содержащий символ '%' выводился неправильно ( обычно, обрезался ).

9555: Метод name() класса dbgModuleClass возвращал строку, содержащую нулевой символ. В итоге при использовании данного метода вывод мог повреждаться ( обрезаться )