Skip to content

Instantly share code, notes, and snippets.

@sumomoneko
Last active February 22, 2018 03:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sumomoneko/d8415b14f8eddf74a3eb9bd6d521fab3 to your computer and use it in GitHub Desktop.
Save sumomoneko/d8415b14f8eddf74a3eb9bd6d521fab3 to your computer and use it in GitHub Desktop.
backtrace with gdb-python-pyelftools
#!/bin/env python3
import posixpath
from elftools.dwarf.descriptions import describe_form_class
from elftools.elf.elffile import ELFFile
from elftools.dwarf.compileunit import CompileUnit
from elftools.dwarf.die import DIE
from elftools.dwarf.lineprogram import (LineProgram, LineProgramEntry)
from elftools.dwarf.dwarfinfo import DWARFInfo
from elftools.dwarf.callframe import (RegisterRule, CFARule)
from typing import Tuple, List, Optional, Dict, Callable
# region ターゲット依存の関数
def get_elf_file_name() -> str:
"""
デバッグ情報が含まれているファイル名を得る。
gdb環境下の場合、gdb.objfiles()[0].filenameで見つかる
:return: デバッグ情報をもつファイルパス
"""
import gdb
return gdb.objfiles()[0].filename
def read_uintptr_t(addr: int) -> int:
"""
uintptr_t サイズのデータをメモリから読む
uintptr_t ret = *reinterpret_cast<uintptr_t*>(addr);
:param addr: アドレス
:return: データ
"""
import gdb
uintptr_t = gdb.lookup_type('uintptr_t')
return int(gdb.Value(addr).reinterpret_cast(uintptr_t.pointer()).dereference())
def get_pc() -> int:
"""
プログラムカウンタを取得する
:return: PC値
"""
import gdb
return int(gdb.parse_and_eval("$pc"))
def get_registers() -> List[int]:
"""
DWARFのレジスタ番号に沿って、gdbからレジスタ値を集める
https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=gdb/amd64-tdep.c;h=b589d93940f1f498177ba91273190dc9b0714370;hb=HEAD#l156
:return: DWARFレジスタ番号順のレジスタ値リスト
"""
import gdb
reg_names = ["rax", "rdx", "rcx", "rbx", "rsi", "rdi", "rbp", "rsp",
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15",
"rip",
# "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",
# "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15",
None, None, None, None, None, None, None, None,
None, None, None, None, None, None, None, None,
# "st0", "st1", "st3", "st4", "st5", "st6", "st7",
None, None, None, None, None, None, None, None,
# "mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm7",
None, None, None, None, None, None, None, None,
"eflags",
"es", "cs", "ss", "ds", "fs", "gs", None, None,
None, None, None, None,
None, None,
"mxcsr", "fctrl", "fstat"]
ret = []
for name in reg_names:
if name is not None:
val = int(gdb.parse_and_eval("${}".format(name)))
ret.append(val)
else:
ret.append(-1)
return ret
def unwind_stack(regs: List[int], cfa: int) -> None:
"""
レジスタのうちスタックポインタを保持するレジスタにアドレスを設定する。
x64の場合は$rspが対象。DWARFレジスタ番号は7番なので、そこにアドレス設定する。
:param regs: レジスタの配列(DWARFレジスタ番号順)
:param cfa: スタックポインタとして設定するアドレス
:return: None
"""
regs[7] = cfa
# endregion
def get_file_line_from_address(cu: CompileUnit, addr: int) -> Dict:
"""
compile unit から、ソースコードのファイル名・行数情報を探す
:param cu: compile unit 情報
:param addr: オブジェクト中のアドレス
:return: ソースコードのファイル名・行数
"""
top_die = cu.get_top_DIE() # type: DIE
# コンパイル時のディレクトリ。
# ソースパスはこのディレクトリを基準に相対表記されている
if "DW_AT_comp_dir" in top_die.attributes.keys():
comp_dir = top_die.attributes["DW_AT_comp_dir"].value.decode('utf-8')
else:
comp_dir = ""
line_program = cu.dwarfinfo.line_program_for_CU(cu) # type: LineProgram
for entry in reversed(line_program.get_entries()): # type: LineProgramEntry
if entry.state is not None and not entry.state.end_sequence:
if entry.state.address < addr:
# entryのアドレス範囲に、探しているアドレスが含まれていた
# ファイルのフルパスを求める
fe = line_program["file_entry"][entry.state.file - 1]
name = fe.name.decode('utf-8')
if fe.dir_index != 0:
# コンパイル時ディレクトリでなく、違うディレクトリ(相対パスで表記)にソースが有る場合
d = line_program["include_directory"][fe.dir_index - 1].decode('utf-8')
else:
d = ""
path = posixpath.normpath(posixpath.join(comp_dir, d, name))
ret = dict()
ret["path"] = path
ret["line"] = entry.state.line
return ret
return dict()
def get_func_info(dwarf_info: DWARFInfo, addr: int) -> Optional[Dict]:
"""
addr で示されたアドレスが含まれる関数情報を取得する
:param dwarf_info: DWARF情報
:param addr: プログラムカウンタ
:return: 関数名・アドレスなどの関数情報
"""
# それぞれのコンパイルユニットから
for cu in dwarf_info.iter_CUs():
# DIEをイテレートしながら、
for die in cu.iter_DIEs():
try:
# 関数のDIEを探して、
if die.tag == 'DW_TAG_subprogram':
# 関数が占めるアドレス範囲を探す
low_pc = die.attributes['DW_AT_low_pc'].value
high_pc_attr = die.attributes['DW_AT_high_pc']
high_pc_attr_class = describe_form_class(high_pc_attr.form)
if high_pc_attr_class == 'address':
high_pc = high_pc_attr.value
elif high_pc_attr_class == 'constant':
high_pc = low_pc + high_pc_attr.value
else:
print('Error: invalid DW_AT_high_pc class:{}\n'.format(high_pc_attr_class))
continue
# 指定されたアドレスがこの関数範囲にマッチしていればビンゴ
if low_pc <= addr < high_pc:
ret = dict()
ret["addr"] = addr
ret["cu"] = cu
ret["func_name"] = die.attributes['DW_AT_name'].value.decode("utf-8")
ret["func_addr"] = low_pc
ret["offset_from_func"] = addr - low_pc
ret.update(get_file_line_from_address(cu, addr))
return ret
except KeyError:
continue
return None
def get_prev_frame(cu: CompileUnit,
addr: int,
regs: List[int],
read_mem: Callable[[int], int]) -> Tuple[Optional[int], Optional[List[int]]]:
"""
実行アドレス・レジスタおよびメモリ(主にスタック)から、スタックフレームを特定し、
呼び出し元のアドレス、その時のレジスタを復元する
:param cu: CU情報
:param addr: 実行アドレス
:param regs: DWARF Register Number でインデックスされたレジスタ一覧
:param read_mem: メモリを読む関数。読み出しはポインタ(レジスタ)サイズ
:return: 呼び出し元アドレスとレジスタ
"""
if cu.dwarfinfo.has_CFI():
entries = cu.dwarfinfo.CFI_entries()
else:
# .debug_frame が無い
entries = []
for entry in entries:
if "initial_location" not in entry.header.keys():
continue
start = entry.header.initial_location
end = start + entry.header.address_range
if not (start <= addr < end):
continue
dec = entry.get_decoded()
for row in reversed(dec.table):
if row["pc"] <= addr:
# 戻り先アドレスと、そのレジスタを復元する
cfa_rule = row["cfa"] # type: CFARule
assert cfa_rule.expr is None, "DWARF表現未対応"
cfa = regs[cfa_rule.reg] + cfa_rule.offset
return_address_rule = row[entry.cie.header.return_address_register] # type: RegisterRule
assert return_address_rule.type == RegisterRule.OFFSET, "OFFSET以外未対応"
return_address = cfa + return_address_rule.arg
prev_regs = regs[:]
for key, reg in row.items():
if isinstance(reg, RegisterRule):
assert reg.type == RegisterRule.OFFSET, "OFFSET以外未対応"
prev_regs[key] = read_mem(cfa + reg.arg)
# スタック巻き戻し
unwind_stack(prev_regs, cfa)
return read_mem(return_address), prev_regs
return None, None
def main() -> None:
"""
バックトレースを表示する
"""
with open(get_elf_file_name(), 'rb') as f:
elf_file = ELFFile(f)
if not elf_file.has_dwarf_info():
print('file has no DWARF info')
return
dwarf_info = elf_file.get_dwarf_info()
# 現在の停止位置情報から初めて、
pc = get_pc()
regs = get_registers()
i = 0
while True:
# 停止位置の関数情報を取得
fi = get_func_info(dwarf_info, pc)
if fi is None:
break
print("#{:<3}0x{:016x} in {}() at {}:{}".format(i,
fi["addr"],
fi["func_name"],
fi["path"],
fi["line"]))
i += 1
# スタックフレームを見て、呼び出し元をたどる
pc, regs = get_prev_frame(fi["cu"], pc, regs, read_uintptr_t)
if pc is None:
break
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment