Как отладить кросс-скомпилированную программу QEMU с помощью GDB?

#gdb #qemu #riscv

#gdb #qemu #riscv

Вопрос:

У меня возникли проблемы с отладкой простой программы, запущенной в QEMU с помощью GDB. GDB, похоже, не может найти, где я нахожусь в программе (в том смысле, что она всегда отображает ?? мое текущее местоположение), и она никогда не достигает ни одной точки останова, которую я установил.

В одном терминале я запускаю QEMU:

 $ cat add.c
int main() {
    int x = 9;
    int v = 1;
    while (1) {
        int q = x   v;
    }
    return 0;
}

$ riscv64-unknown-elf-gcc add.c -g
$ qemu-system-riscv64 -gdb tcp::1234 -drive file=a.out,format=raw
  

И в другом терминале я запускаю GDB:

 $ riscv64-unknown-elf-gdb a.out
GNU gdb (GDB) 8.2.90.20190228-git
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3 : GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-apple-darwin17.7.0 --target=riscv64-unknown-elf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...
(gdb) target remote :1234
Remote debugging using :1234
0x0000000000000000 in ?? ()
(gdb) list
1       int main() {
2           int x = 9;
3           int v = 1;
4           while (1) {
5               int q = x   v;
6           }
7           return 0;
8       }
(gdb) b main
Breakpoint 1 at 0x1018e: file add.c, line 2.
(gdb) b 5
Breakpoint 2 at 0x1019a: file add.c, line 5.
(gdb) b _start
Breakpoint 3 at 0x10114
(gdb) b 4
Breakpoint 4 at 0x101a8: file add.c, line 4.
(gdb) c
Continuing.
  

Я никогда не достигал точки останова, хотя программа должна выполняться бесконечно. Кажется странным, что она отображается 0x0000000000000000 in ?? () …но, может быть, это нормально?

Что я здесь делаю не так? Как я могу пошагово выполнить эту программу?

Ответ №1:

Я думаю, вам не хватает скрипта компоновщика и некоторого кода запуска — отказ от ответственности: я новичок в riscv.

Вы найдете много информации по этим двум темам в Интернете, но вам в основном нужно указать, где ваша программа будет расположена в оперативной памяти, чтобы установить стек и инициализировать указатель фрейма:
Это требуется, если вы хотите иметь возможность вызывать функции и объявлять автоматические переменные C, такие как a, b, c в вашей программе.

Для целей этого примера я использовал набор инструментов для Windows от Kendryte (версия для Linux доступна здесь), а версия qemu для Windows, полученная здесь.

1) Сценарий компоновщика: в примере используется слегка измененный пример сценария компоновщика по умолчанию, используемого riscv64-unknown-elf-ld:

 riscv64-unknown-elf-ld --verbose > riscv64-virt.ld
  

Отредактируйте riscv64-virt.ld и сохраните только строки, разделенные:

 ==================================================
  

Добавьте описание расположения памяти виртуальной машины qemu-system-riscv64:

 OUTPUT_ARCH(riscv)
MEMORY
{
/* qemu-system-risc64 virt machine */
   RAM (rwx)  : ORIGIN = 0x80000000, LENGTH = 128M 
}
ENTRY(_start)
  

Используйте ORIGIN(RAM) и LENGTH(RAM) вместо жестко закодированных значений и укажите __stack_top символ:

  PROVIDE (__executable_start = SEGMENT_START("text-segment", ORIGIN(RAM))); . = SEGMENT_START("text-segment", ORIGIN(RAM))   SIZEOF_HEADERS;
 PROVIDE(__stack_top = ORIGIN(RAM)   LENGTH(RAM));
  

Кстати, существует несколько способов изучения структуры памяти целевого компьютера системы qemu, но я обычно просматриваю файл дерева устройств:

 qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb
dtc -I dtb -O dts -o riscv-virt.dts riscv-virt.dtb
  

В разделе, описывающем память, говорится, что она начинается с 0x80000000:

 memory@80000000 {
    device_type = "memory";
    reg = <0x0 0x80000000 0x0 0x8000000>;
}; 
  

riscv64-virt.ld:

 /* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2018 Free Software Foundation, Inc.
   Copying and distribution of this script, with or without modification,
   are permitted in any medium without royalty provided the copyright
   notice and this notice are preserved.  */
OUTPUT_FORMAT("elf64-littleriscv", "elf64-littleriscv",
          "elf64-littleriscv")
OUTPUT_ARCH(riscv)
MEMORY
{
/* qemu-system-risc64 virt machine */
   RAM (rwx)  : ORIGIN = 0x80000000, LENGTH = 128M 
}
ENTRY(_start)
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", ORIGIN(RAM))); . = SEGMENT_START("text-segment", ORIGIN(RAM))   SIZEOF_HEADERS;
  PROVIDE(__stack_top = ORIGIN(RAM)   LENGTH(RAM));
  .interp         : { *(.interp) }
  .note.gnu.build-id : { *(.note.gnu.build-id) }
  .hash           : { *(.hash) }
  .gnu.hash       : { *(.gnu.hash) }
  .dynsym         : { *(.dynsym) }
  .dynstr         : { *(.dynstr) }
  .gnu.version    : { *(.gnu.version) }
  .gnu.version_d  : { *(.gnu.version_d) }
  .gnu.version_r  : { *(.gnu.version_r) }
  .rela.dyn       :
    {
      *(.rela.init)
      *(.rela.text .rela.text.* .rela.gnu.linkonce.t.*)
      *(.rela.fini)
      *(.rela.rodata .rela.rodata.* .rela.gnu.linkonce.r.*)
      *(.rela.data .rela.data.* .rela.gnu.linkonce.d.*)
      *(.rela.tdata .rela.tdata.* .rela.gnu.linkonce.td.*)
      *(.rela.tbss .rela.tbss.* .rela.gnu.linkonce.tb.*)
      *(.rela.ctors)
      *(.rela.dtors)
      *(.rela.got)
      *(.rela.sdata .rela.sdata.* .rela.gnu.linkonce.s.*)
      *(.rela.sbss .rela.sbss.* .rela.gnu.linkonce.sb.*)
      *(.rela.sdata2 .rela.sdata2.* .rela.gnu.linkonce.s2.*)
      *(.rela.sbss2 .rela.sbss2.* .rela.gnu.linkonce.sb2.*)
      *(.rela.bss .rela.bss.* .rela.gnu.linkonce.b.*)
      PROVIDE_HIDDEN (__rela_iplt_start = .);
      *(.rela.iplt)
      PROVIDE_HIDDEN (__rela_iplt_end = .);
    }
  .rela.plt       :
    {
      *(.rela.plt)
    }
  .init           :
  {
    KEEP (*(SORT_NONE(.init)))
  }
  .plt            : { *(.plt) }
  .iplt           : { *(.iplt) }
  .text           :
  {
    *(.text.unlikely .text.*_unlikely .text.unlikely.*)
    *(.text.exit .text.exit.*)
    *(.text.startup .text.startup.*)
    *(.text.hot .text.hot.*)
    *(.text .stub .text.* .gnu.linkonce.t.*)
    /* .gnu.warning sections are handled specially by elf32.em.  */
    *(.gnu.warning)
  }
  .fini           :
  {
    KEEP (*(SORT_NONE(.fini)))
  }
  PROVIDE (__etext = .);
  PROVIDE (_etext = .);
  PROVIDE (etext = .);
  .rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
  .rodata1        : { *(.rodata1) }
  .sdata2         :
  {
    *(.sdata2 .sdata2.* .gnu.linkonce.s2.*)
  }
  .sbss2          : { *(.sbss2 .sbss2.* .gnu.linkonce.sb2.*) }
  .eh_frame_hdr : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
  .eh_frame       : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) }
  .gcc_except_table   : ONLY_IF_RO { *(.gcc_except_table
  .gcc_except_table.*) }
  .gnu_extab   : ONLY_IF_RO { *(.gnu_extab*) }
  /* These sections are generated by the Sun/Oracle C   compiler.  */
  .exception_ranges   : ONLY_IF_RO { *(.exception_ranges
  .exception_ranges*) }
  /* Adjust the address for the data segment.  We want to adjust up to
     the same address within the page on the next page up.  */
  . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
  /* Exception handling  */
  .eh_frame       : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) }
  .gnu_extab      : ONLY_IF_RW { *(.gnu_extab) }
  .gcc_except_table   : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) }
  .exception_ranges   : ONLY_IF_RW { *(.exception_ranges .exception_ranges*) }
  /* Thread Local Storage sections  */
  .tdata      :
   {
     PROVIDE_HIDDEN (__tdata_start = .);
     *(.tdata .tdata.* .gnu.linkonce.td.*)
   }
  .tbss       : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
  .preinit_array     :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  }
  .init_array     :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
    KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
    PROVIDE_HIDDEN (__init_array_end = .);
  }
  .fini_array     :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
    KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
    PROVIDE_HIDDEN (__fini_array_end = .);
  }
  .ctors          :
  {
    /* gcc uses crtbegin.o to find the start of
       the constructors, so we make sure it is
       first.  Because this is a wildcard, it
       doesn't matter if the user does not
       actually link against crtbegin.o; the
       linker won't look for a file to match a
       wildcard.  The wildcard also means that it
       doesn't matter which directory crtbegin.o
       is in.  */
    KEEP (*crtbegin.o(.ctors))
    KEEP (*crtbegin?.o(.ctors))
    /* We don't want to include the .ctor section from
       the crtend.o file until after the sorted ctors.
       The .ctor section from the crtend file contains the
       end of ctors marker and it must be last */
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
    KEEP (*(SORT(.ctors.*)))
    KEEP (*(.ctors))
  }
  .dtors          :
  {
    KEEP (*crtbegin.o(.dtors))
    KEEP (*crtbegin?.o(.dtors))
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
    KEEP (*(SORT(.dtors.*)))
    KEEP (*(.dtors))
  }
  .jcr            : { KEEP (*(.jcr)) }
  .data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) }
  .dynamic        : { *(.dynamic) }
  . = DATA_SEGMENT_RELRO_END (0, .);
  .data           :
  {
    *(.data .data.* .gnu.linkonce.d.*)
    SORT(CONSTRUCTORS)
  }
  .data1          : { *(.data1) }
  .got            : { *(.got.plt) *(.igot.plt) *(.got) *(.igot) }
  /* We want the small data sections together, so single-instruction offsets
     can access them all, and initialized data all before uninitialized, so
     we can shorten the on-disk segment size.  */
  .sdata          :
  {
    __global_pointer$ = .   0x800;
    *(.srodata.cst16) *(.srodata.cst8) *(.srodata.cst4) *(.srodata.cst2) *(.srodata .srodata.*)
    *(.sdata .sdata.* .gnu.linkonce.s.*)
  }
  _edata = .; PROVIDE (edata = .);
  . = .;
  __bss_start = .;
  .sbss           :
  {
    *(.dynsbss)
    *(.sbss .sbss.* .gnu.linkonce.sb.*)
    *(.scommon)
  }
  .bss            :
  {
   *(.dynbss)
   *(.bss .bss.* .gnu.linkonce.b.*)
   *(COMMON)
   /* Align here to ensure that the .bss section occupies space up to
      _end.  Align after .bss to ensure correct alignment even if the
      .bss section disappears because there are no input sections.
      FIXME: Why do we need it? When there is no .bss section, we don't
      pad the .data section.  */
   . = ALIGN(. != 0 ? 64 / 8 : 1);
  }
  . = ALIGN(64 / 8);
  . = SEGMENT_START("ldata-segment", .);
  . = ALIGN(64 / 8);
  _end = .; PROVIDE (end = .);
  . = DATA_SEGMENT_END (.);
  /* Stabs debugging sections.  */
  .stab          0 : { *(.stab) }
  .stabstr       0 : { *(.stabstr) }
  .stab.excl     0 : { *(.stab.excl) }
  .stab.exclstr  0 : { *(.stab.exclstr) }
  .stab.index    0 : { *(.stab.index) }
  .stab.indexstr 0 : { *(.stab.indexstr) }
  .comment       0 : { *(.comment) }
  /* DWARF debug sections.
     Symbols in the DWARF debugging sections are relative to the beginning
     of the section so we begin them at 0.  */
  /* DWARF 1 */
  .debug          0 : { *(.debug) }
  .line           0 : { *(.line) }
  /* GNU DWARF 1 extensions */
  .debug_srcinfo  0 : { *(.debug_srcinfo) }
  .debug_sfnames  0 : { *(.debug_sfnames) }
  /* DWARF 1.1 and DWARF 2 */
  .debug_aranges  0 : { *(.debug_aranges) }
  .debug_pubnames 0 : { *(.debug_pubnames) }
  /* DWARF 2 */
  .debug_info     0 : { *(.debug_info .gnu.linkonce.wi.*) }
  .debug_abbrev   0 : { *(.debug_abbrev) }
  .debug_line     0 : { *(.debug_line .debug_line.* .debug_line_end ) }
  .debug_frame    0 : { *(.debug_frame) }
  .debug_str      0 : { *(.debug_str) }
  .debug_loc      0 : { *(.debug_loc) }
  .debug_macinfo  0 : { *(.debug_macinfo) }
  /* SGI/MIPS DWARF 2 extensions */
  .debug_weaknames 0 : { *(.debug_weaknames) }
  .debug_funcnames 0 : { *(.debug_funcnames) }
  .debug_typenames 0 : { *(.debug_typenames) }
  .debug_varnames  0 : { *(.debug_varnames) }
  /* DWARF 3 */
  .debug_pubtypes 0 : { *(.debug_pubtypes) }
  .debug_ranges   0 : { *(.debug_ranges) }
  /* DWARF Extension.  */
  .debug_macro    0 : { *(.debug_macro) }
  .debug_addr     0 : { *(.debug_addr) }
  .gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
  /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}
  

2) startup.s: (титры: здесь и здесь).

 .section .init, "ax"
.global _start
_start:
    .cfi_startproc
    .cfi_undefined ra
    .option push
    .option norelax
    la gp, __global_pointer$
    .option pop
    la sp, __stack_top
    add s0, sp, zero
    jal zero, main
    .cfi_endproc
    .end
  

добавьте.c: (ваш код)

 int main() {
    int a = 4;
    int b = 12;
    while (1) {
        int c = a   b;
    }
    return 0;
}
  

3) компиляция / компоновка и создание списка:

 riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections -nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld -o add.elf startup.s add.c
riscv64-unknown-elf-objdump -D  add.elf > add.objdump
  

4) запуск qemu в консоли:

 qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234,ipv4  -kernel add.elf
  

Я не уверен, что параметры qemu, которые вы использовали: -drive file=a.out,format=raw
верны, и я думаю, что это не так, но я не тратил время на проверку и использовал параметры, которые я обычно использую: -kernel add.elf

4) запуск gdb в другой консоли (здесь я использую GDB, которую я скомпилировал с поддержкой TUI для mingw64 для собственного удобства).

 riscv64-elf-gdb --tui  add.elf
  

введите описание изображения здесь

 (gdb) target remote localhost:1234
Remote debugging using localhost:1234
main () at add.c:5
(gdb) p a
$1 = 4
(gdb) p b
$2 = 12
(gdb) p c
$3 = 16
(gdb)
  

Возможно, это было немного длинновато, но я надеюсь, что это поможет.
Пожалуйста, обратите внимание, что код запуска достаточно хорош для вашего кода, но отсутствуют некоторые важные инициализации, такие как копирование раздела данных из flash в RAM (здесь не актуально) и очистка раздела .bss.

Комментарии:

1. Это имеет смысл, спасибо. Тем не менее, я попробовал ваше предложение и все еще вижу то же поведение.

2. Я восстановил свой первый длинный ответ, поскольку пример работал. Я отвечу на любые дополнительные вопросы, если это необходимо (и если я смогу ..). Я предположил со вторым ответом, поскольку у меня нет набора инструментов, который вы используете, и ваша команда компиляции не удалась в моем случае — моя ошибка.

3. Только что увидел ваш новый, более длинный ответ — спасибо за подробности. Я проработаю это завтра и сообщу о результатах. Если это вообще полезно, набор инструментов, который я использую, является предварительно созданным отсюда: sifive.com/boards

4. Сборка / отладка с использованием набора инструментов SiFive также работала нормально.

5. спасибо за этот отличный ответ. Следующий вопрос: почему мне не нужно ничего этого делать при запуске программ в «обычных» условиях, например, при запуске с ОС на моем ноутбуке? GCC, по-видимому, связывается с набором файлов по умолчанию crt и компоновщика, которые предположительно настраивают стек и устанавливают __executable_start . Ожидают ли эти среды выполнения по умолчанию, что наличие операционной системы поможет? Если да, то как именно это работает?

Ответ №2:

Использование riscv-gnu-toolchain built with glibc — гораздо более простой метод отладки программ riscv, если только вы не отлаживаете какую-либо программу системного уровня, где вы должны использовать riscv64-unknown-elf-gcc вместо riscv64-unknown-linux-gnu-gcc . Для такой простой программы, как ваша, add.c использование пользовательского пространства qemu-riscv и glibc riscv-gnu-toolchain может избавить вас от многих проблем. (Эти инструменты можно установить, следуя командам, перечисленным внизу)


К тому времени, когда я пишу этот ответ, существует две разные версии набора инструментов riscv: одна, созданная с помощью newlib которой предоставляется riscv64-unknown-elf-* , а другая с помощью glibc которой предоставляется riscv64-unknown-linux-gnu-* . Существует также две версии qemu: qemu-system-riscv64 для отладки ядер или программ на «голом металле» и qemu-riscv64 для отладки программ пользовательского пространства, скомпилированных с помощью libc.

Для простых программ, таких как add.c , можно отладить ее с помощью второго типа набора инструментов:

  • Скомпилировать: riscv64-unknown-linux-gnu-gcc add.c -o add -g

  • Выполнить: qemu-riscv64 -L /opt/riscv/sysroot/ -g 1234 add -S

  • Затем запустите GDB: riscv64-unknown-linux-gnu-gdb add

    Внутри GDB:

    • target remote:1234
    • b main
    • c

И программа должна прерваться на главном входе.

(Другой вариант — статически связать программу: riscv64-unknown-linux-gnu-gcc add.c -o add -g -static и тогда qemu-riscv64 -g 1234 add -S тоже должно сработать)


Я не нашел много документов, в которых упоминается qemu riscv в пользовательском пространстве. Все, что я нашел, это статьи, в которых говорилось о том, как использовать qemu для отладки ядер ОС с помощью RISC-V ISA. Для удобства других новичков в riscv, таких как я, я покажу далее, как создавать упомянутые инструменты.

 wget https://download.qemu.org/qemu-5.0.0.tar.xz
tar xvJf qemu-5.0.0.tar.xz
cd qemu-5.0.0 # higher versions might have problems
./configure --target-list=riscv64-linux-user,riscv64-softmmu
make -j$(nproc)
sudo make install
  

, где riscv64-softmmu дает вам qemu-system-riscv64 и riscv64-linux-user дает вам qemu-riscv64 .

 git clone https://github.com/riscv/riscv-gnu-toolchain.git
sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev libncurses5-dev
./configure --prefix=/opt/riscv --enable-multilib
sudo make linux # this provides you the glibc set of tools (the ones we need here)
sudo make # this provides you the newlib set of tools