Подтвердить что ты не робот

Как я могу отслеживать, что помещается в стандартный буфер и ломаться, когда определенная строка депонирована в трубе?

В Linux с кодом C/С++, используя gdb, как вы можете добавить точку останова gdb для сканирования входящих строк, чтобы разбить конкретную строку?

У меня нет доступа к определенному библиотечному коду, но я хочу сломать, как только эта библиотека отправит стандартную строку определенной строки, чтобы я мог вернуться в стек и исследовать часть моего кода, вызывающего библиотека. Конечно, я не хочу ждать, пока не произойдет сброс буфера. Это можно сделать? Возможно, процедура в libstdc++?

4b9b3361

Ответ 1

Этот вопрос может быть хорошей отправной точкой: как я могу поставить точку останова на "что-то печатается на терминале" ? в gdb?

Таким образом, вы можете хотя бы сломать всякий раз, когда что-то написано на stdout. Метод в основном включает установку точки останова в syscall write с условием, что первый аргумент 1 (т.е. STDOUT). В комментариях также есть подсказка о том, как вы могли бы также проверить строковый параметр вызова write.

x86 32-разрядный режим

Я придумал следующее и протестировал его с помощью gdb 7.0.1-debian. Кажется, он работает неплохо. $esp + 8 содержит указатель на ячейку памяти строки, переданной в write, поэтому сначала вы передаете ее интегралу, а затем указателю на char. $esp + 4 содержит дескриптор файла для записи в (1 для STDOUT).

$ gdb break write if 1 == *(int*)($esp + 4) && strcmp((char*)*(int*)($esp + 8), "your string") == 0

x86 64-разрядный режим

Если ваш процесс запущен в режиме x86-64, параметры передаются через регистры нуля %rdi и %rsi

$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "your string") == 0

Обратите внимание, что один уровень косвенности удаляется, поскольку мы используем регистры нуля, а не переменные в стеке.

Варианты

В приведенных выше фрагментах могут использоваться функции, отличные от strcmp:

  • strncmp полезен, если вы хотите совместить первое n количество символов строки, написанной
  • strstr можно использовать для поиска совпадений внутри строки, так как вы не всегда можете быть уверены, что строка, которую вы ищет в начале строки, записываемой через функцию write.

Изменить: Мне понравился этот вопрос и наш следующий ответ. Я решил сделать сообщение в блоге об этом.

Ответ 2

Ответ Энтони потрясающий. После его ответа я попробовал другое решение на Windows (x86-64 бит Windows). Я знаю, что этот вопрос здесь для GDB для Linux, однако я считаю, что это решение является дополнением к такому вопросу. Это может быть полезно для других.

Решение для Windows

В Linux вызов printf приведет к вызову API write. И поскольку Linux - это ОС с открытым исходным кодом, мы могли бы отлаживать API. Однако API отличается от Windows, он предоставил ему собственный API WriteFile. Из-за того, что Windows является коммерческой операционной системой с открытым исходным кодом, точки останова не могут быть добавлены в API.

Но некоторые исходные тексты VC публикуются вместе с Visual Studio, поэтому мы можем узнать в исходном коде, где, наконец, называется API WriteFile и устанавливаем там точку останова. После отладки кода примера я обнаружил, что метод printf может привести к вызову _write_nolock, в котором вызывается WriteFile. Функция находится в:

your_VS_folder\VC\crt\src\write.c

Прототип:

/* now define version that doesn't lock/unlock, validate fh */
int __cdecl _write_nolock (
        int fh,
        const void *buf,
        unsigned cnt
        )

По сравнению с API write для Linux:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count); 

У них одинаковые параметры. Таким образом, мы могли бы просто установить condition breakpoint в _write_nolock, просто ссылаясь на вышеприведенные решения, только с некоторыми различиями в деталях.

Портативное решение для Win32 и x64

Очень повезло, что мы могли использовать имя параметров непосредственно в Visual Studio при установке условия для точек останова как на Win32, так и на x64. Поэтому очень легко написать условие:

  • Добавить точки останова в _write_nolock

    УВЕДОМЛЕНИЕ. В Win32 и x64 мало различий. Мы могли бы просто использовать имя функции, чтобы установить местоположение точек останова на Win32. Однако он не будет работать на x64, потому что при входе в функцию параметры не инициализируются. Поэтому мы не могли использовать имя параметра, чтобы установить условие точек останова.

    Но, к счастью, у нас есть некоторая работа: используйте местоположение в функции, а не имя функции, чтобы установить точки останова, например, первую строку функции. Параметры уже инициализированы там. (Я имею в виду использование filename+line number для установки точек останова или прямого открытия файла и установки точки останова в функции, а не входа, кроме первой строки.)

  • Ограничьте условие:

    fh == 1 && strstr((char *)buf, "Hello World") != 0
    

УВЕДОМЛЕНИЕ: здесь все еще проблема, я протестировал два разных способа написать что-то в stdout: printf и std::cout. printf будет записывать все строки в функцию _write_nolock сразу. Однако std::cout передавал символ только символу _write_nolock, что означает, что API будет называться strlen("your string") раз. В этом случае условие не может быть активировано навсегда.

Решение Win32

Конечно, мы могли бы использовать те же методы, что и Anthony: установить условие точек останова на регистры.

Для программы Win32 решение практически одинаково с GDB в Linux. Вы можете заметить, что в прототипе _write_nolock есть декор __cdecl. Это соглашение о вызове означает:

  • Порядок прохождения аргумента - справа налево.
  • Функция вызова выдает аргументы из стека.
  • Соглашение об оформлении имен: символ подчеркивания (_) имеет префикс имен.
  • Выполнение перевода не выполняется.

Ниже приведено описание здесь. И есть пример, который используется для отображения регистров и стеков на веб-сайте Microsoft. Результат можно найти здесь.

Тогда очень просто установить условие точек останова:

  • Установите точку останова в _write_nolock.
  • Ограничьте условие:

    *(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
    

Это тот же метод, что и в Linux. Первое условие - убедиться, что строка записана на stdout. Второй из них соответствует указанной строке.

x64 Решение

Две важные модификация от x86 до x64 - это возможность 64-разрядной адресации и плоский набор из 16 64-разрядных регистров для общего использования. Поскольку увеличение регистров, x64 использует __fastcall как соглашение о вызове. Первые четыре целых аргумента передаются в регистры. Аргументы пять и выше передаются в стек.

Вы можете обратиться к Передача параметровна веб-сайте Microsoft. Четыре регистра (слева направо): RCX, RDX, R8 и R9. Поэтому очень просто ограничить условие:

  • Установите точку останова в _write_nolock.

    УВЕДОМЛЕНИЕ: он отличается от портативного решения выше, мы могли бы просто установить расположение точки останова на функцию, а не на первую строку функции. Причина в том, что все регистры уже инициализированы у входа.

  • Ограничить условие:

    $rcx == 1 && strstr((char *)$rdx, "Hello") != 0
    

Причиной, по которой нам нужен выбор и разыменование на esp, заключается в том, что $esp обращается к регистру esp, и для всех целей и задач есть void*. В то время как регистры здесь хранят непосредственно значения параметров. Поэтому еще один уровень косвенности больше не нужен.

Сообщение

Мне тоже очень нравится этот вопрос, поэтому я перевел сообщение Anthony на китайский язык и поставил свой ответ в нем в качестве дополнения. Сообщение можно найти here. Спасибо за разрешение @anthony-arnold.

Ответ 3

улов

catch + condition - еще один вариант. x86_64:

start
define stdout
    catch syscall write
    commands
        printf "rsi = %s\n", $rsi
        backtrace
    end
    condition $arg0 $rdi == 1 && strstr((char *)$rsi, "$arg1") != 0
end
stdout 2 hello
  • 2 - номер точки останова. 1 - точка останова на main, созданная start.

    К сожалению, я не вижу способа автоматизировать получение этого числа (нет if для catch). Я открыл запрос для этого: https://sourceware.org/bugzilla/show_bug.cgi?id=18727

    start (или run) требуется, потому что вы должны запускать программу для strcmp для загрузки.

  • Замечательная вещь об этом методе заключается в том, что он не зависит от используемого glibc write: он отслеживает фактический системный вызов.

    Таким образом, это сработало бы, даже если glibc имеет другой способ печати, который не проходит через write (я не знаю, имеет ли он).

    Недостатком этого является то, что он не относится к буферизации printf.

Трассирование

Другой вариант, если вы чувствуете интерактивность:

setarch "$(uname -m)" -R strace -i ./stdout.out |& grep '\] write'

Пример вывода:

[00007ffff7b00870] write(1, "a\nb\n", 4a

Скопируйте этот адрес и вставьте его в:

setarch "$(uname -m)" -R strace -i ./stdout.out |& grep -E '\] write\(1, "a'

Преимущество этого метода заключается в том, что вы можете использовать обычные инструменты UNIX для управления выводом strace, и он не требует глубокого GDB-fu.

Пояснение:

Ответ 4

Ответ Энтони очень интересный, и он определенно дает некоторые результаты. Тем не менее, я думаю, что это может пропустить буферизацию printf. Действительно, в Разница между write() и printf(), вы можете прочитать: "printf не обязательно вызывает запись каждый раз. Скорее, printf буферизует свой вывод".

РЕШЕНИЕ STDIO WRAPPER

Следовательно, я пришел с другим решением, которое заключается в создании вспомогательной библиотеки, которую вы можете предварительно загрузить, чтобы обернуть функции, подобные printf. Затем вы можете установить некоторые точки останова в этом источнике библиотеки и backtrace, чтобы получить информацию о программе, которую вы отлаживаете.

Он работает на Linux и нацеливает на libc, я не знаю, для С++ IOSTREAM, также если программа использует запись напрямую, она будет пропустить.

Вот оболочка для захвата printf (io_helper.c).

#include<string.h>
#include<stdio.h>
#include<stdarg.h>

#define MAX_SIZE 0xFFFF

int printf(const char *format, ...){
    char target_str[MAX_SIZE];
    int i=0;

    va_list args1, args2;

    /* RESOLVE THE STRING FORMATING */
    va_start(args1, format);
    vsprintf(target_str,format, args1);
    va_end(args1);

    if (strstr(target_str, "Hello World")){ /* SEARCH FOR YOUR STRING */
       i++; /* BREAK HERE */
    }   

    /* OUTPUT THE STRING AS THE PROGRAM INTENTED TO */
    va_start(args2, format);
    vprintf(format, args2);
    va_end(args2);
    return 0;
}

int puts(const char *s) 
{   
   return printf("%s\n",s);
}

Я добавил puts, потому что gcc имеет тенденцию заменять printf, ставит, когда это возможно. Поэтому я возвращаю его обратно в printf.

Затем вы просто скомпилируете его в общую библиотеку.

gcc -shared -fPIC io_helper.c -o libio_helper.so -g

И вы загружаете его перед запуском gdb.

LD_PRELOAD=$PWD/libio_helper.so; gdb test

Где тест - это программа, которую вы отлаживаете.

Затем вы можете сломаться с break io_helper.c:19, потому что вы скомпилировали библиотеку с -g.

ПОЯСНЕНИЕ

Наше счастье здесь в том, что printf и другие fprintf, sprintf... только здесь, чтобы разрешить вариационные аргументы и называть их эквивалентом "v". (vprintf в нашем случае). Выполнение этой задачи легко, поэтому мы можем сделать это и оставить реальную работу libc с помощью функции "v". Чтобы получить переменные аргументы printf, нам просто нужно использовать va_start и va_end.

Основные преимущества этого метода заключаются в том, что вы уверены, что когда вы ломаетесь, вы находитесь в той части программы, которая выводит вашу целевую строку и что она не остается в буфере. Также вы не делаете никаких предположений на аппаратном обеспечении. Недостатком является то, что вы предполагаете, что программа использует функцию libc stdio для вывода данных.