главная->Статьи->Отладка

    

Краткое содержание
1. Как можно отлаживать программу.
2. Логирование.
3. Пошаговая отладка с помощью GDB.
4. Пошаговая отладка с помощью связки gdb-server/cross-platform-gdb.
5. Посмертный анализ core-dump-a.
6. Использование Map-файла при отладке.
7. Ссылки.

1.Как можно отлаживать программу

Все обычно начинают писать серьёзную программу с того, что добавляют к ней возможность вести логи её работы. Это очень правильно. Здесь можно только добавить, что неплохо добавить в программу перехват всех сигналов, которые она может получить, и логировать также получение этих сигналов.
Часто бывает так, что одних только логов не хватает. Бывает, что хочется программу отладить по шагам. В Linux для этого есть отладчик GDB. проблема только в том, что раз у нас не x86 платформа, то и отладчик нам нужен для нашей платформы, а не для x86. Как этот отладчик собрать из исходников, будет рассказано дальше.
Бывает, что наша платформа имеет очень мало свободного места в файловой системе, или у неё нет терминального выхода (или он занят) или просто по какой-то причине отлаживаемая программа находится очень далеко, и мы не хотим запускать gdb через терминальную сессию. В этой ситуации можно отладиться с помощью связки gdb-server/gdb. При этом gdb-server должен быть нативным для данной платформы, а gdb - должен быть кросс-отладчиком, т.е. работать на x86, но при этом понимать исполняемый файл целевой платформы.
Бывает также так, что просто по какой-то причине мы вообще не можем использовать никакой из отладчиков (например, между нами и целевой платой нет постоянной устойчивой и достаточно быстрой связи или вообще связи нет), или по какой-то причине под отладчиком программа работает без сбоев, а без отладчика падает. В этой ситуации нам может помочь core-файл (или core-dump). Это посмертный слепок с программы, в момент, когда она приводит к непоправимой ошибке, по нему можно узнать внутри какой функции и в какой строчке произошла ошибка, значения локальных переменных, проследить всю цепочку вызовов.
map-файл - это файл, в котором отображены все видимые линкеру "символы" (т.е. имена функций и переменных), и их адреса, по которым они располагаются. map-файл очень часто бывает полезен, когда вы видите сообщение отладчика, что инструкция вашей программы по адресу 0xXXXXXXXX, обратилась по адресу 0xYYYYYYYY. С помощью map-файла можно выяснить, где в вашей программе какие функции и какие переменные располагаются (есть некоторые тонкости, но всё же map-файл - полезная штуковина).

2.Логирование

Логирование можно делать по разному: писать в файл, в поток вывода ошибок, или вообще использовать стандартный демон логирования, в любом случае, API подсистемы логирования должен быть удобным. Я под удобством подразумеваю то, что:
1) должна быть возможность выводить форматированные сообщения в стиле printf.
2) должна быть некая иерархическая система важности сообщений, чтоб можно было по желанию включать или выключать сообщения в зависимости от уровня важности.
Здесь приведу простейшие функции и макросы для логирования. В реальности надо добавлять разные детали в этот механизм, например, путь к файлу должен задаваться, а не быть жестко вбитым. Возможно, кому-то нужно писать время, когда появилось сообщение, и т.д.

#include <stdarg.h>
#include <stdio.h>
#include <ctype.h> //isprint

#define MESSAGE_SIZE_MAX 300
#define LOG_PATH "/tmp/log.txt"

void log(const char *format, ...)
{
    va_list va;
    char tmp[MESSAGE_SIZE_MAX];
    static FILE *log_file = NULL;
    if (log_file == NULL)
    {
        log_file = fopen(LOG_PATH,"a");
    }
    va_start(va, format);
    vsnprintf(tmp, MESSAGE_SIZE_MAX, format, va);
    va_end(va);
    tmp[MESSAGE_SIZE_MAX-1] = 0;
    tmp[MESSAGE_SIZE_MAX-2] = '\n';
    fprintf(log_file,"%s\n",tmp);
    fflush(log_file);
}

void
dump(char *title, void *ptr, int n)
{
    int i = 0;
    char string[17] = {0};
    char buf[100];
    int j = 0;
    int r16 = n % 16;

    DBG("%s", title);


    if (NULL != ptr && 0 != n)
    {
        for (i = 0; i < n; i++)
        {
            if (i % 16 == 0)
            {
                if (i != 0)
                {
                    snprintf(buf + j, sizeof(buf) - j, " |%s|", string);
                    DBG("%s", buf);
                    j = 0;
                }
                j += snprintf(buf + j, sizeof(buf) - j, "%08x  ", (unsigned int)(i));
            }

            j += snprintf(buf + j, sizeof(buf) - j, "%02hhx ",*((char*)((char*)ptr + i)));

            if ( (i+1) % 8 == 0 && (i+1) % 16 != 0)
            {//put one more space
                j += snprintf(buf + j, sizeof(buf) - j, " ");
            }

            //if (*(((char*)ptr)+i) >= ' ')
            if (isprint(*(((char*)ptr)+i)))
            {
                string[i%16] = *(((char*)ptr)+i);
            }
            else
            {
                string[i%16] = '.';
            }
        }

        if (r16 != 0)
        {
            string[r16] = '\0';
            n = 16 - r16;
            //padding
            for (i = 0; i < n; i++)
            {
                j += snprintf(buf + j, sizeof(buf) - j, "   ");
            }
            if (n > 8)
            {
                j += snprintf(buf + j, sizeof(buf) - j, " ");
            }
        }

        j += snprintf(buf + j, sizeof(buf) - j," |%s|", string);
        DBG("%s", buf);
    }
}

#define TRACE log("%s:%s:%d",__FILE__,__FUNCTION__,__LINE__);
#define LOG_DBG(...) log(__VA_ARGS__)
//#define LOG_DBG(...)
#define LOG_ERR(...) log(__VA_ARGS__)
//#define LOG_ERR(...)
#define LOG_EMER(...) log(__VA_ARGS__)
//#define LOG_EMER(...)

3.Отладка с помощью GDB

Описывать детально процесс отладки я не буду, он хорошо описан в Книжке Р.Столмена. Нам нужен gdb, но не простой, а на нашу платформу. Где его взять? Собрать из исходников! Для начала, скачаем архив с исходниками gdb и распакуем их:
  mkdir -p ~/work/cross/gdb/downloads
  cd ~/work/cross/gdb/downloads
  wget http://ftp.gnu.org/gnu/gdb/gdb-6.7.1.tar.bz2
  cd ..
  tar xvjf downloads/gdb-6.7.1.tar.bz2
Теперь, чтоб построить бинарники, работающие с кодом для целевой платы, но на x86 машине, надо указать название платформы целевой платы с помощью опции --target. Идентификатор платформы (в нашем случае arm-linux) это префикс ко всем кросс-бинарникам тулчейна (исполняемый файл кросс-компилятора будет называться arm-linux-gcc).
  mkdir -p ~/work/cross/gdb/build/host_x86
  cd ~/work/cross/gdb/build/host_x86
  ../../gdb-6.7.1/configure --prefix=/opt/gdb/arm-linux/cross --target=arm-linux
  make
  sudo make install
Построение нативных бинарников немножечко сложнее. Во-первых, нужно указать платформу, на которой они будут работать (host architecture) - она в нашем случае будет та же, что была в предыдущем случае, т.е. arm-linux. Вторая проблема - отсутствующие необходимые для сборки библиотеки - некоторых просто нет в кросс-тулчейне и их надо построить, прежде чем начинать строить target-native GDB. Данный пример показывает, как справиться с ситуацией, когда вы обнаруживаете, что target-native библиотека "termcap" отсутствует напрочь (процесс кросс-сборки здесь немного другой, если есть недопонимания, используйте ./configure --help ):
  cd ~/work/cross/gdb/downloads
  wget ftp://ftp.gnu.org/gnu/termcap/termcap-1.3.1.tar.gz
  cd ..
  tar xvzf downloads/termcap-1.3.1.tar.gz
  mkdir -p ~/work/cross/gdb/build/termcap
  cd ~/work/cross/gdb/build/termcap
  
  export CC=arm-linux-gcc
  export RANLIB=arm-linux-ranlib
  ../../termcap-1.3.1/configure --host=arm-linux --prefix=$HOME/work/cross/termcap
  make
  make install
Последний момент, на который стоит обратить внимание, это то, что должны ли бинарники быть слинкованы статически. Это важно, потому что целевая плата может не содержать некоторых so-библиотек, которые нужны GDB или GDB-серверу, но с другой стороны, при статической линковке увеличивается размер файлов (очень намного увеличивается). Опцию статической линковки можно задать в переменной окружения LDFLAGS перед запуском скрипта ./configure . Любые дополнительные библиотеки также должны быть указаны как в LDFLAGS, так и в CPPFLAGS, как например:
  export LDFLAGS="-static -L$HOME/work/cross/termcap/lib"
  export CPPFLAGS="-I$HOME/work/cross/termcap/include"
  ../../gdb-6.7.1/configure --prefix=/opt/gdb/arm-linux/native --host=arm-linux --target=arm-linux
  make
  su
  make install
  exit
теперь остаётся только скопировать gdbserver или gdb из /opt/gdb/arm-linux/native/bin в /bin на целевой плате. Про отладку с помощью gdb - как я уже говорил, можно прочитать в Столмене. А вот про отладку с использовавнием com-портового кабеля или через локальную сеть, я напишу подробнее.

4. Пошаговая отладка с помощью связки gdb-server/cross-platform-gdb.

Чтобы немного привести мысли в порядок, маленькая табличка:
работает на x86работает на ARM-плате
Собирает программы для x86gcc
Собирает программы для ARMarm-linux-gcc
Отлаживает программы для ARM-процессораarm-linux-gdbgdb, gdb-server

Далее - практически как комикс это надо интерпретировать - всё в картинках (время - сверху вниз)
терминал PC терминал, соответствующий telnet сессии с платой.
Компиляция программы
timur-tion@timur-ibm:/home/timur-tion/Proj/hello$ 
/opt/OSELAS.Toolchain-1.1.1/arm-xscale-linux-gnu/
gcc-4.0.4-glibc-2.3.6-kernel-2.6.17/bin/arm-xscal
e-linux-gnu-gcc hello.c -O0 -g -o hello
Обратите внимание на опции компиляции -O0 (O ноль - выключение оптимизации) и -g (включение отладочной информации в исполняемый файл)
Вход по телнету на плату
timur-tion@timur-ibm:~$ telnet 192.168.1.50
Trying 192.168.1.50...
Connected to 192.168.1.50
Escape character is '^]'.
Tion-Pro270 login:root
Password:


BusyBox v1.4.2 (2009-06-24 01:26:19 MSD) Bu
ilt-in shell (ash)
Enter 'help' for a list of built-in command
s.

root@Tion-Pro270~
                
Загрузка программы на плату по FTP
timur-tion@timur-ibm:/home/timur-tion/Proj/hello$ 
ftp 192.168.1.50
Connected to 83.220.242.199.
220 FTP server ready.
Name (192.168.1.20:timur-work):root
331 User root OK. Password required
Password:
230 OK. Current directory is /home
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> cd hello
250 OK. Current directory is /home/hello
ftp> put hello
local: hello remote: hello
200 PORT command successful
150 Connecting to port 59742
226-File successfully transferred
226 3.022 seconds (measured here), 3.10 Kbytes pe
r second
9599 bytes sent in 1.52 secs (6.2 kB/s)
ftp> exit
221-Goodbye. You uploaded 10 and downloaded 0 kby
tes.
221 Logout.


                


            
Запуск gdbserver-а
root@Tion-Pro270:~ cd hello/
root@Tion-Pro270:~/hello gdbserver 192.168.1
.20:234 hello
Process hello created; pid = 30988
Listening on port 234

Примечание: указываем IP-адрес откуда будет этот сервер контролироваться и порт
Запуск кросс-отладчика
timur-tion@timur-ibm:/home/timur-tion/Proj/hello$
 /opt/OSELAS.Toolchain-1.1.1/arm-xscale-linux-gnu
/gcc-4.0.4-glibc-2.3.6-kernel-2.6.17/bin/arm-xsca
le-linux-gnu-gdb hello
GNU gdb 6.6
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General 
Public License, and you are welcome to change it
and/or distribute copies of it under certain cond
itions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "s
how warranty" for details.
This GDB was configured as "--host=i686-host-linu
x-gnu --target=arm-xscale-linux-gnu"...
(gdb) 


            
Ход отладочной сессии
(gdb) target remote 192.168.1.50:234
Remote debugging using 192.168.1.50:234
0x40000a70 in _start () at rtld.c:579
579



(gdb) break main
Breakpoint 1 at 0x83d0: file hello.c, line 5.


(gdb) cont
Continuing.
Breakpoint 1, main (argc=1, argv=0xbef87e84) at
 hello.c:5
5    printf("hello, world\n");


(gdb) list
1 #include<stdio.h>
2 
3 int main(int argc, char *argv[])
4 {
5    printf("hello, world\n");
6    return 0;
7 }
8 
9 


(gdb) next
6    return 0;



(gdb) next
7 }
(gdb) continue
Continuing.

Program exited normally.
(gdb) quit
timur-work@timur-ibm:/home/timur-tion/Proj/hello$





Remote debugging from host 192.168.1.20



























hello, world




Child exited with retcode = 0 

Child exited with status 0
GDBserver exiting
root@Tion-Pro270:~/hello 

5. Посмертный анализ core-dump-a.

core-dump это фактически посмертный слепок с программы, т.е. инфа, характеризующая состояние регистров, стека, глобальных переменных и т.д. Core-файл бинарен, т.е анализируется только отладчиком, фактически загружая core-dump в отладчик, мы затаскиваем его (отладчик) в состояние, в котором он оказался бы, если бы программа аварийно завершилась бы под управлением этого отладчика. Поэтому желательно запускать не кросс-отладчик на PC, а нативный gdb для данной платформы и именно на данной платформе (иначе велика вероятность, что при загрузке он начнёт ругаться, что не может загрузить какие-нибудь shared-библиотеки и т.п. это, конечно, не страшно, дамп вы всё равно сможете проанализировать, но хочется, чтоб всё было lege artis). Вы можете заметить, что core-файл у вас не генерируется сам, при отказе программы. Но при таком же отказе на PC, вы всегда получаете сообщение, что core dumped. Скорее всего, проблема в том, что у вас не хватает места на флешке рядом с файлом, который умер. Вообще, для того, чтобы задать путь к core-файлу, надо этот путь записать в /proc/sys/kernel/core_pattern . Например

(1) echo "/tmp/core.%p" > /proc/sys/kernel/core_pattern
%-символы описаны в man 5 core

(2) cat /proc/sys/kernel/core_pattern
(чтоб лишний раз убедиться, что новые параметры прописались)
после этого перед запуском своей программы дал команду

(3) ulimit -c unlimited
вот ссылка на ман по ulimit: http://ss64.com/bash/ulimit.html
этой командой в данном случае мы снимаем ограничения на размер core-файла

(4) запускаем нашу программу

(5) дожидаемся её падения

(6) проверяем, core-файл должен появиться.

(7) анализируем core-файл: gdb /путь/к/программе --core /tmp/core.1986

(8) тут полезны будут следующие команды (это мне посоветовали на linux.org.ru)
(gdb) backtrace full
(gdb) info registers
(gdb) x/16i $pc
(gdb) thread apply all backtrace

6. Использование Map-файла при отладке.

map-файл может быть полезен. Чтоб его получить, нужно добавить опцию "-Xlinker -Map=output.map" в опции компиляции (в LDFLAGS). Нужно только помнить о том, что static функции и переменные не выходят за пределы модуля, в котором они объявлены, поэтому они не будут отражены в map-файле. Более того, возможна такая ситуация:
//INCLUDE section
#include <stdio.h>
//........и т.д.

//PROTOTYPES section
static int foo(int a, int b);
//........и т.д.

//IMPLEMENTATION section
static int foo(int a, int b)
{
    printf("foo(%d,%d)",a,b);
}
Вы получаете map-файл, не видите в нём функции foo, вспоминаете, что она у вас static, убираете модификатор static из определения функции, но забываете убрать из объявления прототипа. В результате функция всё равно остаётся static и не попадает в map-файл. Ещё одно предостережение: нельзя убрать модификатор static, получить map-файл, а потом снова поставить на место модификатор static и ожидать, что расположение функций будет таким же, как в полученном map-файле.

7. Ссылки.

linux.org.ru
Презентация "Crash N' Burn: Writing Linux application fault handlers" - в картинках, для прочтения за 5 минут
Р.Столмен Отладка с помощью GDB
Remote cross-target debugging with GDB and GDBserver By Avi Rozen



Используются технологии uCoz
Используются технологии uCoz