Как я могу следить за тем, что помещается в стандартный буфер вывода, и прерываться, когда конкретная строка помещается в канал? - PullRequest
16 голосов
/ 23 ноября 2011

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

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

Ответы [ 4 ]

24 голосов
/ 23 ноября 2011

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

Так что вы можете, по крайней мере, сломать что-нибудьнаписано на стандартный вывод.Метод в основном включает установку точки останова на системном вызове 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.

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

3 голосов

catch + strstr состояние

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

Более того, он более устойчив к буферизации printf(), поскольку может даже перехватывать строки, которые печатаются при нескольких вызовах printf().

x86_64 версия:

define stdout
    catch syscall write
    commands
        printf "rsi = %s\n", $rsi
        bt
    end
    condition $bpnum $rdi == 1 && strstr((char *)$rsi, "$arg0") != NULL
end
stdout qwer

Тестовая программа:

#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    write(STDOUT_FILENO, "asdf1", 5);
    write(STDOUT_FILENO, "qwer1", 5);
    write(STDOUT_FILENO, "zxcv1", 5);
    write(STDOUT_FILENO, "qwer2", 5);
    printf("as");
    printf("df");
    printf("qw");
    printf("er");
    printf("zx");
    printf("cv");
    fflush(stdout);
    return EXIT_SUCCESS;
}

Итог: перерывы в:

  • qwer1
  • qwer2
  • fflush. Предыдущий printf на самом деле ничего не печатал, они были помещены в буфер! write syacall произошел только на fflush.

Примечания:

  • $bpnum благодаря Тромею: https://sourceware.org/bugzilla/show_bug.cgi?id=18727
  • rdi: регистр, который содержит номер системного вызова Linux в x86_64, 1 для write
  • rsi: первый аргумент системного вызова, для write он указывает на буфер
  • strstr: стандартный вызов функции C, поиск подсовпадений, возвращает NULL, если не найден

Протестировано в Ubuntu 17.10, gdb 8.0.1.

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

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

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.

Пояснение:

2 голосов
/ 19 мая 2016

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

STDIO WRAPPERРЕШЕНИЕ

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

Это работает в Linux и предназначено для libc, я не знаю для c ++ IOSTREAM, также, если программаиспользуйте write напрямую, он пропустит это.

Вот оболочка для захвата 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);
}

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

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

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

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

LD_PRELOAD=$PWD/libio_helper.so; gdb test

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

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

ОБЪЯСНЕНИЯ

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

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

2 голосов
/ 17 января 2014

Ответ Энтони потрясающий. После его ответа я опробовал другое решение на 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
        )

По сравнению с write API в 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. Так что очень легко написать условие:

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

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

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

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

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

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

Решение Win32

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

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

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

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

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

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

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

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

x64 Solution

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

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

  1. Установить точку останова в _write_nolock.

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

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

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

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

Post

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...