gdb
hypervisor
BitVisor
BitVisorDay 22

BitVisorにgdbstubを実装してゲストOSをデバッグできるようにした話

この記事はBitVisor Advent Calendar 22日目の記事です.

BitVisorにgdbstubを実装してGDB protocolを喋れるようにしてみました。
これでBitVisor上で動作しているゲストOSをリモートマシンのgdbを通してデバッグできるようになります。
Motivationとか詳しい事はAVTOKYOで発表したので、以下の資料をご覧ください。
https://speakerdeck.com/rkx1209/more-efficient-remote-debugging-with-thin-hypervisor
本記事では主に実装面について書きます。

GDB Remote Serial Protocol

ご存知の方も多いと思いますが、gdbはtarget remoteコマンドでリモート越しにデバッグを行う機能を備えています。
これはgdbのクライアント側からGDB Remote Serial Protocol(以下RSP)の規格に沿ったパケットをデバッグ対象のマシンと送受信する事で成り立っています。
例えばクライアントがbreakpointとcontinueコマンドを実行すると「この位置でbreakpoint打ってくれ。んでcontinueしてくれ」というRSPパケットを生成して対象マシンに送りつけます。で、受信側は「OK 〜にbreakpoint打って continueした。あ、continueしたら〜で止まったけどどうする?」というような感じでまたレスポンスを返すわけですね。このRSPパケット受信処理の実装をgdbstubと言って、gdbstubが実装されてるマシンならgdbでリモートデバッグできるわけです。
実際のbreakpointコマンドのパケットやり取りは下図のような感じになってます。
exchange-break.png
Howto: GDB Remote Serial Protocol Writing a RSP Serverより

RSP Packet

さて、上図のパケットやり取りを見てみるとm114,4とかcとか謎の文字列がやり取りされてますね。例えばm114,4はアドレス114から4byte読んで結果を返してくれ、という意味でcはcontinueです。BitVisorのgdbstubでサポートしているパケットは現状以下のとおりです。

Packet Description
? Report why the target halted.
c, C Continue the target (possibly with a particular signal).
g and G. Read or write general registers.
qC and H. Report the current thread or set the thread for subsequent operations.
m and M. Read or write main memory.
p and P. Read or write a specific register.
qSupported. Report the features supported by the RSP server.
z and Z. Clear or set breakpoints or watchpoints.

RSP on BitVisor

さて、上記のパケット受信処理をどのようにして行うかはgdbstubの実装依存です。QEMUやKVMなどのハイパーバイザはgdbstubでゲストOSへのbreakpointの挿入や、メモリの読み書き等のインターフェースを実装、提供しています。
BitVisorのgdbstubも基本的に同じで、ゲストのメモリ読み書きはgmm_access.hを利用しています。

debug/gdbstub.c

static int gdb_handle_packet(GDBState *s, const char *line_buf) {
  const char *p;
  u32 thread;
  int ch, reg_size, type, res;
  char buf[MAX_PACKET_LENGTH];
  u8 mem_buf[MAX_PACKET_LENGTH];
  unsigned long addr, len;

  printf("command='%s'\n", line_buf);

  p = line_buf;
  ch = *p++;
  switch(ch) {
  case '?':
    /* XXX: Is it correct to fix thread id to '1'? */
    snprintf(buf, sizeof(buf), "T%02xthread:%02x;", GDB_SIGNAL_TRAP, 1);
    gdb_put_packet(s, buf);
    break;
  case 'c':
    if (*p != '\0') {
      addr = strtol(p, (char **)&p, 16);
    }
    gdb_continue(s);
    break;
  case 'g':
    len = 0;
    for (addr = 0; addr < GENERAL_REG_MAX; addr++) {
      reg_size = gdb_read_register(mem_buf + len, addr);
      len += reg_size;
    }
    memtohex(buf, mem_buf, len);
    gdb_put_packet(s, buf);
    break;
  case 'm':
    addr = strtol(p, (char **)&p, 16);
    if (*p == ',')
        p++;
    len = strtol(p, NULL, 16);

    /* memtohex() doubles the required space */
    if (len > MAX_PACKET_LENGTH / 2) {
        gdb_put_packet (s, "E22");
        break;
    }

    if (target_memory_rw (addr, mem_buf, len, false) != 0) {
        gdb_put_packet (s, "E14");
    } else {
        memtohex(buf, mem_buf, len);
        gdb_put_packet(s, buf);
    }
    break;
  case 'p':
    addr = strtol(p, (char **)&p, 16);
    reg_size = gdb_read_register(mem_buf, addr);
    if (reg_size) {
      memtohex(buf, mem_buf, reg_size);
      gdb_put_packet(s, buf);
    } else {
      gdb_put_packet(s, "E14");
    }
    break;
  case 'q':
  case 'Q':
    if (is_query_packet(p, "Supported", ':')) {
        snprintf(buf, sizeof(buf), "PacketSize=%x", MAX_PACKET_LENGTH);
        gdb_put_packet(s, buf);
        break;
    } else if (strcmp(p,"C") == 0) {
        gdb_put_packet(s, "QC1");
        break;
    }
    goto unknown_command;
  case 'z':
  case 'Z':
    type = strtol(p, (char **)&p, 16);
    if (*p == ',')
        p++;
    addr = strtol(p, (char **)&p, 16);
    if (*p == ',')
        p++;
    len = strtol(p, (char **)&p, 16);
    if (ch == 'Z')
        res = gdb_breakpoint_insert(addr, len, type);
    else
        res = gdb_breakpoint_remove(addr, len, type);
    if (res >= 0)
        gdb_put_packet(s, "OK");
    else
        gdb_put_packet(s, "E22");
    break;

  case 'H':
    type = *p++;
    thread = strtol(p, (char **)&p, 16);
    if (thread == -1 || thread == 0) {
        gdb_put_packet(s, "OK");
        break;
    }
    switch (type) {
    case 'c':
        gdb_put_packet(s, "OK");
        break;
    case 'g':
        gdb_put_packet(s, "OK");
        break;
    default:
        gdb_put_packet(s, "E22");
        break;
    }
    break;
  case 'T':
    thread = strtol(p, (char **)&p, 16);
    gdb_put_packet(s, "OK");
    break;
  default:
    unknown_command:
      /* put empty packet */
      buf[0] = '\0';
      gdb_put_packet(s, buf);
      break;
  }
  return RS_IDLE;
}

ハードウェアブレークポイント

上記コードの'z'/'Z'パケットの処理がブレークポイントの追加処理です。gdb_breakpoint_insertというやつです。
BitVisorはIntel VT-xを利用しているためソフトウェアブレークポイントよりもハードウェアブレークポイントを実装したほうが何かと都合が良いです。

debug/gdbstub.c

static int sync_debug_reg(void)
{
  const u8 type_code[] = {
      [GDB_BREAKPOINT_HW] = 0x0,
      [GDB_WATCHPOINT_WRITE] = 0x1,
      [GDB_WATCHPOINT_ACCESS] = 0x3
  };
  const u8 len_code[] = {
      [1] = 0x0, [2] = 0x1, [4] = 0x3, [8] = 0x2
  };
  Breakpoint *bp;
  unsigned long dr7 = 0x0600;
  int n = 0;
  LIST1_FOREACH (bp_list, bp) {
    vt_write_dr(n, bp->addr);
    dr7 |= (2 << (n * 2)) |
        (type_code[bp->type] << (16 + n * 4)) |
        ((u32)len_code[bp->len] << (18 + n * 4));
    n++;
  }
  vt_write_dr(DEBUG_REG_DR7, dr7);
  return 0;
}

static int hw_breakpoint_insert(unsigned long addr, unsigned long len, int type)
{
  if (nb_hw_breakpoint > DEBUG_REG_MAX)
    return -1;
  Breakpoint *bp = alloc (sizeof *bp);
  if (!bp)
    return -1;
  bp->addr = addr, bp->len = len, bp->type = type;
  LIST1_ADD (bp_list, bp);
  nb_hw_breakpoint++;
  sync_debug_reg ();
  return 0;
}

ハードウェアブレークポイントはIntelのDRレジスタと呼ばれる特殊なレジスタに値を書き込むことで設定できます。
具体的にはDR0-DR3の4つのレジスタそれぞれにブレークポイントを打ちたいアドレスを入れておき、DR7というレジスタに各アドレスごとのブレークポイントの種類(rwx, global等)を書き込んでおくという物です。

こいつを実現するためにDRレジスタへの読み書きヘルパー関数も実装してあります。

core/asm.h

static inline void
asm_wridr0 (ulong dr0)
{
    asm volatile ("mov %0, %%dr0"
              :
              : "rm" ((ulong)dr0));
}
static inline void
asm_wridr1 (ulong dr1)
{
    asm volatile ("mov %0, %%dr1"
              :
              : "rm" ((ulong)dr1));
}
static inline void
asm_wridr2 (ulong dr2)
{
    asm volatile ("mov %0, %%dr2"
              :
              : "rm" ((ulong)dr2));
}
static inline void
asm_wridr3 (ulong dr3)
{
    asm volatile ("mov %0, %%dr3"
              :
              : "rm" ((ulong)dr3));
}

static inline void
asm_rddr0 (ulong *dr0)
{
    asm volatile ("mov %%dr0,%0"
              : "=r" (*dr0));
}
static inline void
asm_rddr1 (ulong *dr1)
{
    asm volatile ("mov %%dr1,%0"
              : "=r" (*dr1));
}
static inline void
asm_rddr2 (ulong *dr2)
{
    asm volatile ("mov %%dr2,%0"
              : "=r" (*dr2));
}
static inline void
asm_rddr3 (ulong *dr3)
{
    asm volatile ("mov %%dr3,%0"
              : "=r" (*dr3));
}
static inline void
asm_rddr7 (ulong *dr7)
{
    asm volatile ("mov %%dr7,%0"
              : "=r" (*dr7));
}

このDRレジスタによるハードウェアブレークポイントは設定後、ヒットするとDebug Exception(#DB, INT 0x1)が発生します。
ただしここで注意が必要なのが、このDebug ExceptionはデフォルトのBitVisorではVMExitしてくれません。
なのでVT-xのException Fieldを適切に設定する必要があります。(下記コミット参照)
Add Debug Exception flag to VMExit exception filed

ネットワーク通信

今回のgdbstubはクライアントとネットワークで通信するように実装しました。
そのためにlwipを利用してコネクションの接続や送受信処理等実装しています。説明がめんどくさいので下のコードを見てください。

ip/gdb_remote.c

/*
 * Copyright (c) 2017 Ren Kimura <rkx1209dev@gmail.com>
 * All rights reserved.
 */

#include <debug/gdbstub.h>
#include "lwip/tcp.h"

enum server_states {
  ES_NONE = 0,
  ES_ACCEPTED,
  ES_RECEIVED,
  ES_CLOSING
};

struct gdb_server_state {
  enum server_states state;
  struct tcp_pcb *pcb;
};

static struct tcp_pcb *gdb_pcb;
static struct gdb_server_state *gdb_state;

static void gdb_server_close(struct tcp_pcb *tpcb, struct gdb_server_state *gs)
{
  tcp_arg(tpcb, NULL);
  tcp_sent(tpcb, NULL);
  tcp_recv(tpcb, NULL);

  if (gs != NULL) {
    mem_free(gs);
  }
  tcp_close(tpcb);
}

static err_t gdb_server_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
  return ERR_OK;
}

static void _gdb_server_send(struct tcp_pcb *tpcb, struct pbuf *data)
{
  err_t wr_err = ERR_OK;
  wr_err = tcp_write (tpcb, data->payload, data->len, 1);
  if (wr_err == ERR_OK) {
    tcp_output(tpcb);
    u16_t plen = data->len;
    printf ("send: %s(%d)\n", ((char *)data->payload), data->len);
    pbuf_free (data);
  } else if (wr_err == ERR_MEM) {
    printf ("%s: overflow send buffer\n", __func__);
  }
}

void gdb_server_send(u8_t *buf, u16_t len)
{
  struct pbuf *ptr = pbuf_alloc (PBUF_TRANSPORT, len, PBUF_RAM);
  struct tcp_pcb *cur_pcb = gdb_state->pcb;
  if (!ptr) {
    panic ("%s: pbuf_alloc failed\n", __func__);
  }
  memcpy (ptr->payload, buf, len);
  LWIP_ASSERT("cur_pcb != NULL", cur_pcb != NULL);
  tcp_sent (cur_pcb, gdb_server_sent);
  _gdb_server_send (cur_pcb, ptr);
}

static err_t gdb_server_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
  struct gdb_server_state *gs;
  err_t ret_err;
  gs = (struct gdb_server_state *)arg;
  if (p == NULL) {
    printf("gdbserver: Disconnected from client\n");
    gs->state = ES_CLOSING;
    gdb_server_close (tpcb, gs);
    ret_err = ERR_OK;
  } else {
    gdb_chr_receive (p->payload, p->len);
    tcp_recved (tpcb, p->len);
    pbuf_free (p);
  }
  return ret_err;
}

static err_t gdb_server_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
  LWIP_UNUSED_ARG(arg);
  LWIP_UNUSED_ARG(err);
  printf ("%s: Accept\n", __func__);
  /* commonly observed practive to call tcp_setprio(), why? */
  tcp_setprio(newpcb, TCP_PRIO_MIN);

  gdb_state = (struct gdb_server_state *)mem_malloc(sizeof(struct gdb_server_state));
  if (!gdb_state)
    panic ("%s: mem_malloc failed\n", __func__);
  gdb_state->pcb = newpcb;
  /* pass newly allocated pcb to our callbacks */
  tcp_arg(newpcb, gdb_state);
  tcp_recv(newpcb, gdb_server_recv);
  printf("gdbserver: connected from client!\n");
  return ERR_OK;
}

void gdb_server_init (int port)
{
  gdb_stub_init();
  gdb_pcb = tcp_new();
  if (gdb_pcb != NULL)
  {
    err_t err;

    err = tcp_bind(gdb_pcb, IP_ADDR_ANY, port);
    if (err == ERR_OK)
    {
      gdb_pcb = tcp_listen(gdb_pcb);
      tcp_accept(gdb_pcb, gdb_server_accept);
      printf ("%s: Initilization complete\n", __func__);
    }
    else
    {
      panic ("%s: tcp_bind failed\n", __func__);
    }
  }
  else
  {
    panic ("%s: tcp_new failed\n", __func__);
  }
}

おわりに

これでBitVisorを起動してゲストからtarget remoteして、breakpointとか打つとシステム全体がちゃんと停止してくれます。
continueすると再開します。いろいろ捗ります。嬉しいね。
使い方はbitvisor-gdbのREADMEを見てください。