Изучаем С используя GDB

  • Tutorial
Перевод статьи Аллана О’Доннелла Learning C with GDB.

Исходя из особенностей таких высокоуровневых языков, как Ruby, Scheme или Haskell, изучение C может быть сложной задачей. В придачу к преодолению таких низкоуровневых особенностей C, как ручное управление памятью и указатели, вы еще должны обходиться без REPL. Как только Вы привыкнете к исследовательскому программированию в REPL, иметь дело с циклом написал-скомпилировал-запустил будет для Вас небольшим разочарованием.

Недавно мне пришло в голову, что я мог бы использовать GDB как псевдо-REPL для C. Я поэкспериментировал, используя GDB как инструмент для изучения языка, а не просто для отладки, и оказалось, что это очень весело.

Цель этого поста – показать Вам, что GDB является отличным инструментом для изучения С. Я познакомлю Вас с несколькими моими самыми любимыми командами из GDB, и продемонстрирую каким образом Вы можете использовать GDB, чтобы понять одну из сложных частей языка С: разницу между массивами и указателями.

Введение в GDB


Начнем с создания следующей небольшой программы на С – minimal.c:

int main()
{
    int i = 1337;
    return 0;
}

Обратите внимание, что программа не делает абсолютно ничего, и даже не имеет ни одной команды printf. Теперь окунемся в новый мир изучения С используя GBD.

Скомпилируем эту программу с флагом -g для генерирования отладочной информации, с которой будет работать GDB, и подкинем ему эту самую информацию:

$ gcc -g minimal.c -o minimal
$ gdb minimal

Теперь Вы должны молниеносно оказаться в командной строке GDB. Я обещал вам REPL, так получите:

(gdb) print 1 + 2
$1 = 3

Удивительно! print – это встроенная команда GDB, которая вычисляет результат С-ного выражения. Если Вы не знаете, что именно делает какая-то команда GDB, просто воспользуйтесь помощью – наберите help name-of-the-command в командной строке GDB.

Вот Вам более интересный пример:

(gbd) print (int) 2147483648
$2 = -2147483648

Я упущу разъяснение того, почему 2147483648 == -2147483648. Главная суть здесь в том, что даже арифметика может быть коварная в С, а GDB отлично понимает арифметику С.

Теперь давайте поставим точку останова в функции main и запустим программу:

(gdb) break main
(gdb) run

Программа остановилась на третьей строчке, как раз там, где инициализируется переменная i. Интересно то, что хотя переменная пока и не проинициализирована, но мы уже сейчас можем посмотреть ее значение, используя команду print:

(gdb) print i
$3 = 32767

В С значение локальной неинициализированной переменной не определено, поэтому полученный Вами результат может отличаться.

Мы можем выполнить текущую строку кода, воспользовавшись командой next:

(gdb) next
(gdb) print i
$4 = 1337

Исследуем память используя команду X


Переменные в С – это непрерывные блоки памяти. При этом блок каждой переменной характеризуется двумя числами:

1. Числовой адрес первого байта в блоке.
2. Размер блока в байтах. Этот размер определяется типом переменной.

Одна из отличительных особенностей языка С в том, что у Вас есть прямой доступ к блоку памяти переменной. Оператор & дает нам адрес переменной в памяти, а sizeof вычисляет размер, занимаемый переменной памяти.

Вы можете поиграть с обеими возможностями в GDB:

(gdb) print &i
$5 = (int *) 0x7fff5fbff584
(gdb) print sizeof(i)
$6 = 4

Говоря нормальным языком, это значит, что переменная i размещается по адресу 0x7fff5fbff5b4 и занимает в памяти 4 байта.

Я уже упоминал выше, что размер переменной в памяти зависит от ее типа, да и вообще говоря, оператор sizeof может оперировать и самими типами данных:

(gdb) print sizeof(int)
$7 = 4
(gdb) print sizeof(double)
$8 = 8

Это означает, что по меньшей мере на моей машине, переменные типа int занимают четыре байта, а типа double – восемь байт.

В GDB есть мощный инструмент для непосредственного исследования памяти – команда x. Эта команда проверяет память, начиная с определенного адреса. Также она имеет ряд команд форматирования, которые обеспечиваю точный контроль над количеством байт, которые Вы захотите проверить, и над тем, в каком виде Вы захотите вывести их на экран. В случае каких либо трудностей, наберите help x в командной строке GDB.

Как Вы уже знаете, оператор & вычисляет адрес переменной, а это значит, что можно передать команде x значение &i и тем самым получить возможность взглянуть на отдельные байты, скрывающиеся за переменной i:

(gdb) x/4xb &i
0x7fff5fbff584: 0x39    0x05    0x00    0x00

Флаги форматирования указывают на то, что я хочу получить четыре (4) значения, выведенные в шестнадцатеричном (hex) виде по одному байту (byte). Я указал проверку только четырех байт, потому что именно столько занимает в памяти переменная i. Вывод показывает побайтовое представление переменной в памяти.

Но с побайтовым выводом связана одна тонкость, которую нужно постоянно держать в голове – на машинах Intel байты хранятся в порядке “от младшего к старшему” (справа налево), в отличии от более привычной для человека записи, где младший байт должен был бы находиться в конце (слева направо).

Один из способов прояснить этот вопрос – это присвоить переменной i более интересное значение и опять проверить этот участок памяти:

(gdb) set var i = 0x12345678
(gdb) x/4xb &i
0x7fff5fbff584: 0x78    0x56    0x34    0x12

Исследуем память с командой ptype


Команда ptype возможно одна из моих самых любимых. Она показывает тип С-го выражения:

(gdb) ptype i
type = int
(gdb) ptype &i
type = int *
(gdb) ptype main
type = int (void)

Типы в С могут становиться сложными, но ptype позволяет исследовать их в интерактивном режиме.

Указатели и массивы


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

Итак, нам нужен код программы с массивом array.c:

int main()
{
    int a[] = {1, 2, 3};
    return 0;
}

Скомпилируйте ее с флагом -g, запустите в GDB, и с помощь next перейдите в строку инициализации:

$ gcc -g arrays.c -o arrays
$ gdb arrays
(gdb) break main
(gdb) run
(gdb) next

На этом этапе Вы сможете вывести содержимое переменной и выяснить ее тип:

(gdb) print a
$1 = {1, 2, 3}
(gdb) ptype a
type = int [3]

Теперь, когда наша программа правильно настроена в GDB, первое, что стоит сделать – это использовать команду x для того, чтобы увидеть, как выглядит переменная a “под капотом”:

(gdb) x/12xb &a
0x7fff5fbff56c: 0x01  0x00  0x00  0x00  0x02  0x00  0x00  0x00
0x7fff5fbff574: 0x03  0x00  0x00  0x00

Это означает, что участок памяти для массива a начинается по адресу 0x7fff5fbff56c. Первые четыре байта содержат a[0], следующие четыре – a[1], и последние четыре хранят a[2]. Действительно, Вы можете проверить и убедится, что sizeof знает, что a занимает в памяти ровно двенадцать байт:

(gdb) print sizeof(a)
$2 = 12

До этого момента массивы выглядят такими, какими и должны быть. У них есть соответствующий массивам типы и они хранят все значения в смежных участках памяти. Однако, в определенных ситуациях, массивы ведут себя очень схоже с указателями! К примеру, мы можем применять арифметические операции к a:

(gdb) print a + 1
$3 = (int *) 0x7fff5fbff570

Нормальными словами, это означает, что a + 1 – это указатель на int, который имеет адрес 0x7fff5fbff570. К этому моменту Вы должны уже рефлекторно передавать указатели в команду x, итак посмотрим, что же получилось:

(gdb) x/4xb a + 1
0x7fff5fbff570: 0x02  0x00  0x00  0x00


Обратите внимание, что адрес 0x7fff5fbff570 ровно на четыре единицы больше, чем 0x7fff5fbff56c, то есть адрес первого байта массива a. Учитывая, что тип int занимает в памяти четыре байта, можно сделать вывод, что a + 1 указывает на a[1].

На самом деле, индексация массивов в С является синтаксическим сахаром для арифметики указателей: a[i] эквивалентно *(a + i). Вы можете проверить это в GDB:

(gdb) print a[0]
$4 = 1
(gdb) print *(a + 0)
$5 = 1
(gdb) print a[1]
$6 = 2
(gdb) print *(a + 1)
$7 = 2
(gdb) print a[2]
$8 = 3
(gdb) print *(a + 2)
$9 = 3

Итак, мы увидели, что в некоторых ситуациях a ведет себя как массив, а в некоторых – как указатель на свой первый элемент. Что же происходит?

Ответ состоит в следующем, когда имя массива используется в выражении в С, то оно “распадается (decay)” на указатель на первый элемент. Есть только два исключения из этого правила: когда имя массива передается в sizeof и когда имя массива используется с оператором взятия адреса &.

Тот факт, что имя a не распадается на указатель на первый элемент при использовании оператора &, порождает интересный вопрос: в чем же разница между указателем, на который распадается a и &a?

Численно они оба представляют один и тот же адрес:

(gdb) x/4xb a
0x7fff5fbff56c: 0x01  0x00  0x00  0x00
(gdb) x/4xb &a
0x7fff5fbff56c: 0x01  0x00  0x00  0x00

Тем не менее, типы их различны. Как мы уже видели, имя массива распадается на указатель на его первый элемент и значит должно иметь тип int *. Что же касается типа &a, то мы можем спросить об этом GDB:

(gdb) ptype &a
type = int (*)[3]

Говоря проще, &a – это указатель на массив из трех целых чисел. Это имеет смысл: a не распадается при передаче оператору & и a имеет тип int [3].

Вы можете проследить различие между указателем, на который распадается a и операцией &a на примере того, как они ведут себя по отношению к арифметике указателей:

(gdb) print a + 1
$10 = (int *) 0x7fff5fbff570
(gdb) print &a + 1
$11 = (int (*)[3]) 0x7fff5fbff578

Обратите внимание, что добавление 1 к a увеличивает адрес на четыре единицы, в то время, как прибавление 1 к &a добавляет к адресу двенадцать.

Указатель, на который на самом деле распадается a имеет вид &a[0]:

(gdb) print &a[0]
$11 = (int *) 0x7fff5fbff56c

Заключение


Надеюсь, я убедил Вас, что GDB – это изящная исследовательская среда для изучения С. Она позволяет выводить значение выражений с помощью команды print, побайтово исследовать память командой x и работать с типами с помощью команды ptype.

Если Вы планируете и далее экспериментировать с изучением С с помощью GDB, то у меня есть некоторые предложения:

1. Используйте GDB для работы над The Ksplice Pointer Challenge.
2. Разберитесь, как структуры хранятся в памяти. Как они соотносятся с массивами?
3. Используйте дизассемблерные команды GDB, чтобы лучше разобраться с программированием на ассемблере. Особенно весело исследовать, как работает стек вызова функции.
4. Зацените “TUI” режим GDB, который обеспечивает графическую ncurses надстройку над привычным GDB. На OS X, Вам вероятно придется собрать GDB из исходников.

От переводчика: Традиционно для указания ошибок воспользуйтесь ЛС. Буду рад конструктивной критике.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 30
  • +1
    Было бы круто еще строки и структуры расколупать таким же образом.
    • 0
      К сожалению, у автора это единственная статья по теме. Но мне в голову приходила такая же мысль и я подумывал над тем, чтобы самому заняться этим вопросом…
      • 0
        Вы можете сделать это сами, и посмотреть, как компилятор выравнивает свойства. В принципе, не rocket science.
        В молодости, когда интернет был дорогой и знания приобретались на ошибках, я ковырял память с помощью указателя на байт (или массив байт), и просто смотрел в Borland C++ 5.02 с помощью watch.
      • +6
        Хорошая статья. Тут речь идёт об изучении C, а мне например, GDB очень помог разобраться с ассемблером. Упомянутая команда x действительно мощная, с помощью неё можно и регистры процессора отслеживать во время отладки (не сочтите за К.О. :)).
        • +4
          Сейчас как раз в процессе работы над переводом по теме изучения ассемблера с помощью GDB :)
          • +1
            Дааа, будет интересно почитать, особенно если, как примеры, подробно показать с картинками передачу аргументов при вызове функции через регистры (на 64-битной архитектуре) и через стек (на 32-битной архитектуре), сохранение значений вызывающей функции и т.д.
            • +1
              Картинок не обещаю, но кое-какая визуализация будет.
              • +1
                Зачем ждать, если можно самому?
                Чем мне всегда нравился больше С по сравнению с Ruby, Scheme,… — можно посмотреть, что на самом деле происходит: скомпилил (без оптимизации), поставил точку останова, «Open disassembler at ...» и смотришь, что происходит.
                Но вообще gdb куда сложнее, чем C, на мой взгляд (потому название статьи немного рушит шаблоны). Лично мне куда более приятно пользоваться гуёвыми оболочками, будь то MSVS или QtCreator.
          • +6
            «На фоне Haskell изучение C — настоящее испытание» — o rly?)
            • +3
              А почему бы и нет? Если понял Хаскель, то Си становятся чем-то инопланетным и наоборот. Совершенно разные миры.
              • +2
                Смайлик как-бы намекает на несерьезность моего предыдущего комментария)
                А если серьезно, то, как мне кажется, если человек смог понять и изучитьХаскель, то Си ему выучить — раз плюнуть, даже несмотря на абсолютно различные идеологии и парадигмы.
              • 0
                Да ладно уж. Если взять какую-нибудь объективную метрику, например насколько сложно обучить человека, никогда не сталкивавшегося с программированием, до того, чтобы он смог написать какую-нибудь там простую программу, то Си окажется сильно сложнее Хаскеля.
                • +2
                  Не самая лучшая объективная метрика, очень уж сильно она зависит от той самой «простой программы».

                  Не спорю
                  module Main where
                  main = putStrLn "Hello, world!"
                  

                  выглядит проще, чем
                  #include <stdio.h>
                  
                  int main(void)
                  {
                      printf("Hello, world!\n");
                      return 0;
                  }
                  

                  Но если начать объяснять, что же скрывается за этим `putStrLn`, то придется потратить кучу времени на объяснение, зачем такие выкрутасы. Можно, конечно, и не объяснять, а просто сказать, что эта штука выводит строку на экран, но тогда можно ли сказать, что вы обучили человека Хаскелю? Только на крайне базовом уровне, которого для написания чуть более сложных программ уже не достаточно.
              • 0
                Еще кстати очень часто рекомендуют использовать не голый GDB, а фронденд для него, ввиде такой штуки как DDD. Всё никак не попопробую. Я думаю тут многие ей пользовались. Стоит ведь использовать? Намного ли удобнее?
                • 0
                  по небольшому опыту могу сказать, что «быстренько запустить и позырить на стек из дампа коры» — более чем подходит. Отладка с просмотром переменных — тоже довольно удобно разместить watch-имое на экране и двигаться по шагам.
                  • 0
                    Тогда уж сразу KDbg.
                  • 0
                    REPL? in Pure C? Выражаясь в стиле Ритчи — если тебе нужен пайтон, ты знаешь где его взять — а вообще без обид, лишний раз убеждаюсь в том какими разными путями люди приходят к одному и тому-же.
                    • 0
                      Ссылка в посте на The Ksplice Pointer Challenge внезапно оказалось любопытной, кто читал пост и не смотрел, рекомендую.
                      • –10
                        Пройдёт ещё лет 20 и линюксовые красноглазики откроют для себя турбо-дебагер (1989) или даже M$ visual studio, и мир никогда не станет прежним.
                        • 0
                          Бывают ситации, когда программа выполняется только где-то на сервере, и единственный способ понять в чем проблема — запуск gdb по ssh из консоли.
                          • –9
                            А это не к тому, что отладчик не нужен, сам, бывалоча, корки смотрел этим самым GDB… Это к тому, что забыть его надо было как страшный сон ещё в конце 80х!
                            • 0
                              А чем он плох?
                              • –7
                                Командной строкой.
                                • +2
                                  gdb не ограничивается командной строкой же. Можно использовать любой фронтенд. А как отладчик он, имхо, очень хорош. За довольно продолжительное время работы с ним на большом проекте(несколько миллионов строк кода) не видел особых проблем. Visual studio, к примеру, на том же коде тормозит при отладке намного больше, хотя это субъективно, конечно.
                                  Кроме того, на linux у gdb есть некоторые очень удобные фичи(например, чекпоинты и возможность отладки дочерних процессов), которых нет в той же Visual studio. Хотя может и есть, но я про них не знаю.

                                  В общем, он ничем не хуже других хороших дебаггеров. Мир не изменится, красноглазики уже давно открыли для себя все что нужно. :)
                                  • –4
                                    Фронтэнд?
                                    Это DDD или KDbg? Чем такой дебагер и такой фронтэнд, уж лучше старая добрая отладочная печать.

                                    Век строчных редакторов кончился в 70е годы. Я не знаю ни одного извращенца, который набирает тексты в ed.
                                    Но gdb живее всех живых, хотя по уровню технологии UI это тот-же ed — строчный дебагер.
                                    • 0
                                      Ниже привели пример фронтэнда NVIDIA Nsight Tegra.
                                  • +2
                                    NVIDIA Nsight Tegra, плагин для студии, позволяющий собирать и отлаживать нативные приложения для Андроид в среде Visual Studio. Угадаете бэкенд отладчика?
                              • 0
                                Вы не поверите, но можно подключиться по протоколу gdb по цепочке, используя на машине с пользователем таки графическую обёртку. Да, бывают ситуации, когда быстрее что-то набрать в консоли — не спорю, да и никто ж не отказывается от gdb. Но это — достаточно сложный инструмент, для новичка, изучающего C, это не нужно.
                                • 0
                                  Да знаю, в том же Eclipse можно подключиться, но надо настраивать, и проблемы с конфигурированием есть.

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