<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

文章分类

导航

订阅

约有 43 项符合查询结果,费时 0.025 毫秒

使用内核调试调试Windows安装过程
今天的Windows的安装程序其实也是基于NT内核的,因此也是可以使用WinDBG对其做内核调试的,大致的步骤是准备一个USB安装盘,然后使用如下命令启用内核调试,就可以调试安装过程了。
bcdedit –store [usb key drive]:\boot\bcd /dbgsettings net hostip:[IP of WinDbg machine] port:50000
[待补充]

发布于 2018年4月25日 21:38Raymond0 篇评论

Ubuntu内核调试要点(下)——常见问题

  前两讲我们分别介绍了安装调试符号和源文件,以及设置目标机和开始调试会话,这一讲我们介绍一下大家可能遇到的两个常见问题:架构不匹配和内核地址错位。

   架构不匹配

   如果目标机是32位的,那么一般没有问题,如果是64位的,那么主机端的GDB可能误以为对方是32位的,表现出的症状就是找不到函数的符号(因为错把64位的地址当作32位,只取一半),如果观察寄存器,那么也都不对,比如:

(gdb) info registers     
    eax            0x1b 27
    ecx            0x0 0
    edx            0x67 103
    ebx            0x0 0
    esp            0x6 0x6 <irq_stack_union+6>
    ebp            0x0 0x0 <irq_stack_union>
    esi            0x0 0
    edi            0x0 0
    eip            0x246 0x246 <irq_stack_union+582>
    eflags         0x0 [ ]
    cs             0x3f40dc80 1061215360
    ss             0xffff96dd -26915
    ds             0x1e5be38 31833656
    es             0xffffaf96 -20586
    fs             0x1e5be38 31833656
    gs             0xffffaf96 -20586

  仔细观察上面的寄存器内容,对于熟悉x86架构的读者,很容易看出很多异常,比如段寄存器都是16位,数值应该很小,可上面显示的却很大,再比如程序指针寄存器(eip)的值也太小。这都是因为GDB把64位的寄存器上下文强行按32位来对待了。

  有人说,GDB真的这么傻么?真的如此。为了了解GDB里的原委,老雷还特意启动第二个GDB,以GDB调试GDB,截图如下:


  上图左侧是做内核调试的GDB,右侧便是调试左侧GDB的GDB。 

   当我们执行target remote命令时,GDB就会初始化一个gdbarch实例,而此时还未与目标机建立连接,不知道对方是32位还是64位,所以此时GDB是始终初始化一个32为的gdbarch,过程如下:    

#0  i386_gdbarch_init (info=..., arches=0x1240c00) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/i386-tdep.c:8255
#1  0x00000000005e218f in gdbarch_find_by_info (info=...) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/gdbarch.c:5100
#2  0x00000000005e2b71 in gdbarch_update_p (info=...) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/arch-utils.c:522
#3  0x00000000006bb2c3 in target_clear_description () at /build/gdb-9un5Xp/gdb-7.11.1/gdb/target-descriptions.c:400
#4  0x0000000000600637 in target_pre_inferior (from_tty=from_tty@entry=1) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/target.c:2161
#5  0x0000000000601aad in target_preopen (from_tty=from_tty@entry=1) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/target.c:2220
#6  0x00000000004c6b73 in remote_open_1 (name=0x1082e7e "/dev/ttyS0", from_tty=1, target=0xc44040 <remote_ops>, extended_p=0)
    at /build/gdb-9un5Xp/gdb-7.11.1/gdb/remote.c:4886
#7  0x00000000005f4538 in open_target (args=0x1082e7e "/dev/ttyS0", from_tty=1, command=<optimized out>)
    at /build/gdb-9un5Xp/gdb-7.11.1/gdb/target.c:356
#8  0x000000000069dbc6 in execute_command (p=<optimized out>, p@entry=0x1082e70 "", from_tty=1) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/top.c:475
#9  0x00000000005d490c in command_handler (command=0x1082e70 "") at /build/gdb-9un5Xp/gdb-7.11.1/gdb/event-top.c:491
#10 0x00000000005d4fef in command_line_handler (rl=<optimized out>) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/event-top.c:690
#11 0x00007f9a096586f5 in rl_callback_read_char () from /lib/x86_64-linux-gnu/libreadline.so.6
#12 0x00000000005d4969 in rl_callback_read_char_wrapper (client_data=<optimized out>) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/event-top.c:171
#13 0x00000000005d49b3 in stdin_event_handler (error=<optimized out>, client_data=0x0) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/event-top.c:430
#14 0x00000000005d3795 in gdb_wait_for_event (block=block@entry=1) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/event-loop.c:834
#15 0x00000000005d3939 in gdb_do_one_event () at /build/gdb-9un5Xp/gdb-7.11.1/gdb/event-loop.c:323
#16 0x00000000005d3a7e in start_event_loop () at /build/gdb-9un5Xp/gdb-7.11.1/gdb/event-loop.c:347
#17 0x00000000005cd443 in captured_command_loop (data=data@entry=0x0) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/main.c:318
#18 0x00000000005ca25d in catch_errors (func=func@entry=0x5cd430 <captured_command_loop>, func_args=func_args@entry=0x0, 
    errstring=errstring@entry=0x7abfeb "", mask=mask@entry=RETURN_MASK_ALL) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/exceptions.c:240
#19 0x00000000005ce036 in captured_main (data=data@entry=0x7ffec93a15d0) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/main.c:1157
#20 0x00000000005ca25d in catch_errors (func=func@entry=0x5cd990 <captured_main>, func_args=func_args@entry=0x7ffec93a15d0, 
    errstring=errstring@entry=0x7abfeb "", mask=mask@entry=RETURN_MASK_ALL) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/exceptions.c:240
#21 0x00000000005ce90b in gdb_main (args=args@entry=0x7ffec93a15d0) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/main.c:1165
#22 0x000000000045ecd5 in main (argc=<optimized out>, argv=<optimized out>) at /build/gdb-9un5Xp/gdb-7.11.1/gdb/gdb.c:32

  GDB虽然代码量不小,但是设计逻辑还是很清楚的,一个current_inferior_全局变量用来记录当前的被调试对象。在GDB中,调试对象被统称为inferior,是下属晚辈的意思,老雷将其翻译为“下程”(下属程序之意)。

  在current_inferior_结构体中,有一个gdbarch成员,就是用来记录当前调试目标的架构的(32/64位之类),即:

(gdb) p *current_inferior_
$20 = {next = 0x0, num = 1, pid = 42000, fake_pid_p = 1,
  highest_thread_num = 384, control = {stop_soon = STOP_QUIETLY_REMOTE},
  removable = 0, aspace = 0x22d3fc0, pspace = 0x11992b0, args = 0x0,
  argc = 0, argv = 0x0, terminal = 0x0, environment = 0x11c9780,
  attach_flag = 0, vfork_parent = 0x0, vfork_child = 0x0,
  pending_detach = 0, waiting_for_vfork_done = 0, detaching = 0,
  continuations = 0x0, needs_setup = 0, priv = 0x0, has_exit_code = 0,
  exit_code = 0, symfile_flags = 0, tdesc_info = 0x11f2e30,
  gdbarch = 0x1242b80, registry_data = {data = 0x123d160, num_data = 7}}

  观察gdbarch,可以看到它内部的函数指向的都是32位版本(以i386开头)。

  可以用watch命令设置硬件断点监视gdbarch字段的变化,但是当GDB与目标建立连接是,这个断点并没有命中。至少在老雷调试的GDB中,它不会自动检测目标的架构并作切换。

  怎么办呢?答案是需要执行如下命令手工切换:

(gdb) set architecture i386:x86-64
The target architecture is assumed to be i386:x86-64

  这样切换后,再观察gdbarch,就是64位版本的了。

(gdb) p *gdbarch
$42 = {initialized_p = 1, obstack = 0x12e40b0, bfd_arch_info = 0x952dc0 <bfd_x86_64_arch>, byte_order = BFD_ENDIAN_LITTLE, 
  byte_order_for_code = BFD_ENDIAN_LITTLE, osabi = GDB_OSABI_LINUX, target_desc = 0x0, tdep = 0x12e3f10, dump_tdep = 0x0, nr_data = 26, 
  data = 0x12f9990, bits_big_endian = 0, short_bit = 16, int_bit = 32, long_bit = 64, long_long_bit = 64, long_long_align_bit = 32, half_bit = 16, 
  half_format = 0xc37970 <floatformats_ieee_half>, float_bit = 32, float_format = 0xc37960 <floatformats_ieee_single>, double_bit = 64, 
  double_format = 0xc37950 <floatformats_ieee_double>, long_double_bit = 128, long_double_format = 0xc37930 <floatformats_i387_ext>, ptr_bit = 64, 
  addr_bit = 64, dwarf2_addr_size = 8, char_signed = 1, read_pc = 0x0, write_pc = 0x46bf90 <amd64_linux_write_pc>, 
  virtual_frame_pointer = 0x5e29f0 <legacy_virtual_frame_pointer>, pseudo_register_read = 0x0, 
  pseudo_register_read_value = 0x45fc30 <amd64_pseudo_register_read_value>, pseudo_register_write = 0x45fb20 <amd64_pseudo_register_write>, 
  num_regs = 152, num_pseudo_regs = 52, ax_pseudo_register_collect = 0x0, ax_pseudo_register_push_stack = 0x0, sp_regnum = 7, pc_regnum = 16, 
  ps_regnum = 17, fp0_regnum = 24, stab_reg_to_regnum = 0x45f9c0 <amd64_dwarf_reg_to_regnum>, ecoff_reg_to_regnum = 0x5e2990 <no_op_reg_to_regnum>, 
  sdb_reg_to_regnum = 0x47ef30 <i386_dbx_reg_to_regnum>, dwarf2_reg_to_regnum = 0x45f9c0 <amd64_dwarf_reg_to_regnum>, 
  register_name = 0x6bb050 <tdesc_register_name>, register_type = 0x6bc1a0 <tdesc_register_type>, dummy_id = 0x45f950 <amd64_dummy_id>, 
  deprecated_fp_regnum = -1, push_dummy_call = 0x467330 <amd64_push_dummy_call>, call_dummy_location = 1, 
  push_dummy_code = 0x477b80 <i386_push_dummy_code>, print_registers_info = 0x5aef10 <default_print_registers_info>, 
  print_float_info = 0x488170 <i387_print_float_info>, print_vector_info = 0x0, register_sim_regno = 0x5e2890 <legacy_register_sim_regno>, 
  cannot_fetch_register = 0x5e29e0 <cannot_register_not>, cannot_store_register = 0x5e29e0 <cannot_register_not>, 
  get_longjmp_target = 0x45f400 <amd64_get_longjmp_target>, believe_pcc_promotion = 0, convert_register_p = 0x488c50 <i387_convert_register_p>, 
  register_to_value = 0x488c80 <i387_register_to_value>, value_to_register = 0x488dc0 <i387_value_to_register>, 
  value_from_register = 0x561a10 <default_value_from_register>, pointer_to_address = 0x561790 <unsigned_pointer_to_address>, 
  address_to_pointer = 0x5617f0 <unsigned_address_to_pointer>, integer_to_address = 0x0, return_value = 0x465e20 <amd64_return_value>, 
  return_in_first_hidden_param_p = 0x5e3880 <default_return_in_first_hidden_param_p>, skip_prologue = 0x4674a0 <amd64_skip_prologue>, 
  skip_main_prologue = 0x0, skip_entrypoint = 0x0, inner_than = 0x5e2950 <core_addr_lessthan>, 
  breakpoint_from_pc = 0x477b10 <i386_breakpoint_from_pc>, remote_breakpoint_from_pc = 0x5e3850 <default_remote_breakpoint_from_pc>, 
  adjust_breakpoint_address = 0x0, memory_insert_breakpoint = 0x5f1ba0 <default_memory_insert_breakpoint>, 
  memory_remove_breakpoint = 0x5f1c70 <default_memory_remove_breakpoint>, decr_pc_after_break = 1, deprecated_function_start_offset = 0, 
  remote_register_number = 0x6bb020 <tdesc_remote_register_number>, fetch_tls_load_module_address = 0x493be0 <svr4_fetch_objfile_link_map>, 
  frame_args_skip = 8, unwind_pc = 0x47a250 <i386_unwind_pc>, unwind_sp = 0x0, frame_num_args = 0x0, frame_align = 0x45efd0 <amd64_frame_align>, 
  stabs_argument_has_addr = 0x5e2ac0 <default_stabs_argument_has_addr>, frame_red_zone_size = 128, 
  convert_from_func_ptr_addr = 0x5e2980 <convert_from_func_ptr_addr_identity>, addr_bits_remove = 0x5e2970 <core_addr_identity>, 
  software_single_step = 0x0, single_step_through_delay = 0x0, print_insn = 0x47e170 <i386_print_insn>, 
  skip_trampoline_code = 0x613c30 <find_solib_trampoline_target>, skip_solib_resolver = 0x490380 <glibc_skip_solib_resolver>, 
  in_solib_return_trampoline = 0x5e2930 <generic_in_solib_return_trampoline>, stack_frame_destroyed_p = 0x5e2940 <generic_stack_frame_destroyed_p>, 
  elf_make_msymbol_special = 0x0, coff_make_msymbol_special = 0x5e29a0 <default_coff_make_msymbol_special>, 
  make_symbol_special = 0x5e29b0 <default_make_symbol_special>, adjust_dwarf2_addr = 0x5e29c0 <default_adjust_dwarf2_addr>, 
  adjust_dwarf2_line = 0x5e29d0 <default_adjust_dwarf2_line>, cannot_step_breakpoint = 0, have_nonsteppable_watchpoint = 0, 
  address_class_type_flags = 0x0, address_class_type_flags_to_name = 0x0, address_class_name_to_type_flags = 0x0, 
  register_reggroup_p = 0x474950 <amd64_linux_register_reggroup_p>, fetch_pointer_argument = 0x4796f0 <i386_fetch_pointer_argument>, 
  iterate_over_regset_sections = 0x46b610 <amd64_linux_iterate_over_regset_sections>, make_corefile_notes = 0x496c60 <linux_make_corefile_notes>, 
  elfcore_write_linux_prpsinfo = 0x0, find_memory_regions = 0x4976a0 <linux_find_memory_regions>, core_xfer_shared_libraries = 0x0, 
  core_xfer_shared_libraries_aix = 0x0, core_pid_to_str = 0x495e30 <linux_core_pid_to_str>, core_thread_name = 0x0, gcore_bfd_target = 0x0, 
  vtable_function_descriptors = 0, vbit_in_delta = 0, skip_permanent_breakpoint = 0x5e38c0 <default_skip_permanent_breakpoint>, max_insn_length = 16, 
  displaced_step_copy_insn = 0x467770 <amd64_displaced_step_copy_insn>, 
  displaced_step_hw_singlestep = 0x5e2810 <default_displaced_step_hw_singlestep>, displaced_step_fixup = 0x467b10 <amd64_displaced_step_fixup>, 
  displaced_step_free_closure = 0x5e2800 <simple_displaced_step_free_closure>, displaced_step_location = 0x4981d0 <linux_displaced_step_location>, 
  relocate_instruction = 0x45f0f0 <amd64_relocate_instruction>, overlay_update = 0x0, 
  core_read_description = 0x4748a0 <amd64_linux_core_read_description>, static_transform_name = 0x0, sofun_address_maybe_missing = 0, 
  process_record = 0x480860 <i386_process_record>, process_record_signal = 0x474800 <amd64_linux_record_signal>, 
  gdb_signal_from_target = 0x494280 <linux_gdb_signal_from_target>, gdb_signal_to_target = 0x4944c0 <linux_gdb_signal_to_target>, 
  get_siginfo_type = 0x48b2d0 <x86_linux_get_siginfo_type>, record_special_symbol = 0x0, 
  get_syscall_number = 0x46bf10 <amd64_linux_get_syscall_number>, xml_syscall_file = 0x79fce7 "syscalls/amd64-linux.xml", syscalls_info = 0x0, 
---Type <return> to continue, or q <return> to quit---
  stap_integer_prefixes = 0x79f3b0 <stap_integer_prefixes>, stap_integer_suffixes = 0x0, stap_register_prefixes = 0x79f3a0 <stap_register_prefixes>, 
  stap_register_suffixes = 0x0, stap_register_indirection_prefixes = 0x79f390 <stap_register_indirection_prefixes>, 
  stap_register_indirection_suffixes = 0x79f380 <stap_register_indirection_suffixes>, stap_gdb_register_prefix = 0x0, stap_gdb_register_suffix = 0x0, 
  stap_is_single_operand = 0x478270 <i386_stap_is_single_operand>, stap_parse_special_token = 0x478b80 <i386_stap_parse_special_token>, 
  dtrace_parse_probe_argument = 0x474990 <amd64_dtrace_parse_probe_argument>, dtrace_probe_is_enabled = 0x46c8e0 <amd64_dtrace_probe_is_enabled>, 
  dtrace_enable_probe = 0x46c8c0 <amd64_dtrace_enable_probe>, dtrace_disable_probe = 0x46c8a0 <amd64_dtrace_disable_probe>, has_global_solist = 0, 
  has_global_breakpoints = 0, has_shared_address_space = 0x4981c0 <linux_has_shared_address_space>, 
  fast_tracepoint_valid_at = 0x4795d0 <i386_fast_tracepoint_valid_at>, auto_charset = 0x566ae0 <default_auto_charset>, 
  auto_wide_charset = 0x566af0 <default_auto_wide_charset>, solib_symbols_extension = 0x0, has_dos_based_file_system = 0, 
  gen_return_address = 0x45f090 <amd64_gen_return_address>, info_proc = 0x494f70 <linux_info_proc>, core_info_proc = 0x4976f0 <linux_core_info_proc>, 
  iterate_over_objfiles_in_search_order = 0x60fa60 <default_iterate_over_objfiles_in_search_order>, ravenscar_ops = 0x0, 
  insn_is_call = 0x45f080 <amd64_insn_is_call>, insn_is_ret = 0x45f070 <amd64_insn_is_ret>, insn_is_jump = 0x45f060 <amd64_insn_is_jump>, 
  auxv_parse = 0x0, vsyscall_range = 0x494990 <linux_vsyscall_range>, infcall_mmap = 0x494800 <linux_infcall_mmap>, 
  infcall_munmap = 0x494710 <linux_infcall_munmap>, gcc_target_options = 0x5e3970 <default_gcc_target_options>, 
  gnu_triplet_regexp = 0x477ba0 <i386_gnu_triplet_regexp>, addressable_memory_unit_size = 0x5e39d0 <default_addressable_memory_unit_size>}

  接下来,再观察寄存器,就对了,比如:

(gdb) i r
rax            0x2f    47
rbx            0xffffffff81efadc0    -2114998848
rcx            0xffffffff81e5f568    -2115635864
rdx            0x0    0
rsi            0x246    582
rdi            0x246    582
rbp            0xffffc90000643da8    0xffffc90000643da8
rsp            0xffffc90000643da8    0xffffc90000643da8
r8             0xb57e8    743400
r9             0x207    519
r10            0xd2a81d91    3534232977
r11            0xffffffff822487ed    -2111535123
r12            0xffff880111a2be40    -131936804487616
r13            0x0    0
r14            0x55a9bbc50cf0    94187488087280
r15            0x55a9bbc503c0    94187488084928
rip            0xffffffff81141344    0xffffffff81141344 <kgdb_breakpoint+20>
eflags         0x202    [ IF ]
cs             0x10    16
ss             0x0    0
ds             0x0    0
es             0x0    0
fs             0x0    0
gs             0xb    11

 

 内核地址错位

 第二个常见的问题是,GDB里找不到函数的名字,长话短说是因为在GDB看来,内核的编译地址和运行时的地址是一样的,所以就直接用符号文件中查找到的地址来访问目标机器。但其实,今天的较高版本内核(3.14+)可能启用了KASLR(内核空间地址随机化),把内核也做了重定位。

  不妨做个小实验来理解KASLR,先观察/proc/kallsyms中的vfs_read函数的地址(这是实际运行时的地址),再观察/boot目录下编译时的地址,会发现二者明显不同。

gedu@gedu-VirtualBox:~$ sudo cat /proc/kallsyms | grep " vfs_read"
ffffffff86c31960 T vfs_readf
ffffffff86c32ce0 T vfs_read
ffffffff86c33210 T vfs_readv
gedu@gedu-VirtualBox:~$ sudo cat /boot/System.map-4.8.0-36-generic | grep " vfs_read"
ffffffff81231960 T vfs_readf
ffffffff81232ce0 T vfs_read
ffffffff81233210 T vfs_readv

KASLR是一项安全措施,与黑客捉迷藏。但这样躲躲闪闪,让GDB也蒙了。

  那么如何让告诉GDB内核搬家了呢?今天用的方法大多都是禁止KASLR,也就是在内核的命令行中加入nokaslr,然后重启问题就消除了。 

  读到这里,大家是不觉得陷阱很多啊。是的,新问题,新挑战总是有的。GNU领袖RMS在GDB教程的封面如此写道:“Don't worry if it doesn't work right. If everything did, you'd be out of job.”翻译一下:“别担心它工作的不好。如果一切都工作的好,那么你就没工作了。” 其实,我并不很赞同这句话,因为不够积极。不过RMS如此说,也只是开个玩笑而已,看看他的相册,可以看到他走到哪里就在哪里掏出本子改BUG啊。 :-) 


发布于 2018年3月10日 14:21admin0 篇评论

Ubuntu内核调试要点(中)——配置目标机和建立调试会话

  虽然如上一部分所讲,原生的Ubuntu内核在编译时已经包含了内核调试支持(KGDB),但是它默认是不工作,如果是使用,那么需要先启用。这与Windows的内核调试是一样的。

  开启内核调试的方法不难,只要在内核命令行增加一些参数。基本步骤如下:

  1,切换到/etc/grub.d目录下,以sudo方式打开40-custom文件,即:

   sudo gedit /ect/grub.d/40-custom

    然后从/boot/grub/grub.cfg中复制一个菜单项(menuentry)过来,再把菜单名中增加调试信息,然后在内核命令行中增加KGDB选项,即下面这样:

    menuentry 'Ubuntu, with Linux 3.12.2 Kernel Debug Serial' --class ubuntu --class gnu-linux --class gnu --class os {    
    recordfail
    gfxmode $linux_gfx_mode
    insmod gzio
    insmod part_msdos
    insmod ext2
    set root='(hd0,msdos1)'
    search --no-floppy --fs-uuid --set=root eb5d8aee-6a0f-4257-b5b8-8d6731ba6764
    echo 'Loading Linux 3.12.2 with KGDB built by GEDU lab...'
    linux/boot/vmlinuz-3.12.2 root=UUID=eb5d8aee-6a0f-4257-b5b8-8d6731ba6764 ro quiet splash nomodeset $vt_handoff print-fatal-signals=1 kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon nokaslr
    echo 'Loading initial ramdisk ...'
    initrd/boot/initrd.img-3.12.2
    }

    新增部分:kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon nokaslr

    其中,kgdboc=ttyS0,115200 kgdbcon是关键。其中,kgdboc是KGDB over console的缩写,后面的ttyS0代表通过1号串口进行通信,波特率为115200。后面的nokaslr是禁止内核空间的地址随机化,我们将在第三部分详细解释。

   如果调试目标是虚拟机,那么模拟串口很方便,像上面这样设置就可以了。如果调试目标是物理机器,那么今天很多机器都不带串口,这时可以考虑使用PCIe的扩展串口卡(对于台式机)。实在没有串口,可以考虑使用网络连接方式,即所谓的kgdboe(KGDB over ethernet),我们以后再介绍。

  修改grub的配置后,需要执行sudo update-grub来更新。更新后目标机器就准备好了。

  对于主机端,如果使用虚拟机,那么可以把目标机克隆一份。如果是真实机器,那么最好选择相同版本的Ubuntu。

  对于串口方式,因为默认用户没有串口设备的使用权限,所以应该先使用如下命令来增加权限:

!sudo usermod -a -G dialout gychang

  注意dialout后面是用户名,应该替换成你使用的用户名。如果不调整权限,那么也可以使用sudo方式来启动gdb,但是如果忘记使用sudo了,就要退出重来。     

  注意,上诉权限修改需要logout再登录才生效。

  接下来应该通过stty命令来设置串口的通信速率。这里需要说明一下,在某些版本的GDB中,可以通过set remotebaud 115200这样的命令在GDB中设置波特率,但是在最近的一些版本中,这条命令不存在了,这时便需要使用Linux的命令来设置了,即: stty -F /dev/ttyS0 115200

    gychang@gychang-HP-Pro-3380-MT:~/dbg$ stty -F /dev/ttyS0    
    speed 9600 baud; line = 0;
    min = 0; time = 10;
    -brkint -icrnl -imaxbel
    -opost -onlcr
    -isig -icanon -iexten -echo -echoe -echok -echoctl -echoke
    gychang@gychang-HP-Pro-3380-MT:~/dbg$ stty -F /dev/ttyS0 115200
    gychang@gychang-HP-Pro-3380-MT:~/dbg$ stty -F /dev/ttyS0
    speed 115200 baud; line = 0;
    min = 0; time = 10;
    -brkint -icrnl -imaxbel
    -opost -onlcr
    -isig -icanon -iexten -echo -echoe -echok -echoctl -echoke

  上面先是显示旧的设置,再设置,最后再观察确认。        

   主机和目标机都准备好后,现在主机端启动以如下命令启动gdb:

   gdb -s /usr/lib/debug/boot/vmlinux-4.10.0-28-generic

   GDB启动后,会加载内核文件中的符号,加载完毕后,输入以下命令让GDB开始等待远程目标:      

   target remote /dev/ttyS0

  

  让主机端的GDB进入等待后,重启目标机器,启动时如果grub的菜单没有自动弹出,那么请按住shift键。然后在菜单中选择我们前面配置的调试项。

  如果一切顺利的话,目标机会主动中断到主机端的GDB,因为我们在命令行参数中的kgdbwait命令的含义就是在启动时等待kgdb和中断。这个启动早期的中断一般是通过一条断点指令来实现的,被称为初始断点,下面因初始断点中断后的场景:


  但是有时可能不顺利,比如GDB报告收到意外的包,忽略之类,这时可以按Ctrl + C让GDB退出等待状态,然后再执行target remote /dev/ttyS0让其重新与目标机握手和对话。

  初始断点命中后,就可以执行各种GDB命令了,最常用的就是bt,观察栈回溯,如下图所示:

  

  上图中,kgdb_breakpoint函数就是触发断点指令(int 3)的函数,它所在的源文件名为debug_core.c,是KDB和KGDB共享的核心调试逻辑,其作用相当于NT内核中的KD(调试引擎)。  

  接下来可以使用dir命令来设置源文件位置,设置好后,就可以使用l命令来观察源代码了,比如:

  接下来,可以执行n和s命令单步,在内核空间里逛一逛,也可以在感兴趣的位置埋个断点(命令b),让内核跑起来(c),遇到断点再停下来。这种自由控制内核的感觉是其它方式无法达到的。

  怎么样,是不是很简单呢?不过实际调试时,还会有一些实际的问题,比如如何处理64位和KASLR(内核空间随机化)等干扰。我们将在下一讲继续介绍。





发布于 2018年3月10日 13:02admin0 篇评论

Ubuntu内核调试要点(上)——准备符号和源文件

  内核调试是解决复杂软件问题和探索内核世界的最佳手段。因为多方面原因,Linux系统的内核调试设施在很长一段时间里都比较薄弱,特别是与Windows相比,落后较多。不过这种情况正在改变。本文以Ubuntu为例,详细介绍Linux内核调试的现状,着重介绍存在误解和难点的地方。

  首先,如果对于从Ubuntu官网下载安装包而安装的系统,不需要重新编译内核。因为,Canonical(开发Ubuntu的公司)在编译时,已经开启了内核调试支持。如果你想确认一下,那么可以查看/boot目录下的编译选项文件,比如对于目前使用较多的16.04 LTS版本:

        gedu@gedu-VirtualBox:/boot$ cat config-4.8.0-36-generic | grep -i "GDB"
        # CONFIG_CFG80211_INTERNAL_REGDB is not set
        CONFIG_SERIAL_KGDB_NMI=y
        CONFIG_GDB_SCRIPTS=y
        CONFIG_HAVE_ARCH_KGDB=y
        CONFIG_KGDB=y
        CONFIG_KGDB_SERIAL_CONSOLE=y
        # CONFIG_KGDB_TESTS is not set
        CONFIG_KGDB_LOW_LEVEL_TRAP=y
        CONFIG_KGDB_KDB=y

  可以看到,关于KDB和KGDB的几个选项都是开启的(yes)。

  第二个问题是符号文件。

  虽然编译选项已经开启了,但是没有符号文件,那么会缺少很多信息,也无法做源代码调试,岂不是还要重新编译?非也。早在2007年,Ubuntu就公开了符号服务器,其网址为http://ddebs.ubuntu.com。其角色和价值与微软的符号服务器是一样的。

  那么如何从Ubuntu的符号服务器下载符号呢?基本步骤如下:

  1,执行如下命令,产生“符号源列表”文件ddebs.list:

        codename=$(lsb_release -c | awk  '{print $2}')
        sudo tee /etc/apt/sources.list.d/ddebs.list << EOF
        deb http://ddebs.ubuntu.com/ ${codename}      main restricted universe multiverse
        deb http://ddebs.ubuntu.com/ ${codename}-security main restricted universe multiverse
        deb http://ddebs.ubuntu.com/ ${codename}-updates  main restricted universe multiverse
        deb http://ddebs.ubuntu.com/ ${codename}-proposed main restricted universe multiverse
        EOF

    lsb_release -c命令用来获取当前Ubuntu的开发代号,用于在符号服务器中寻找所需符号包。产生好的ddebs.list应该像下面这样:

        ge@gewubox:/etc/apt/sources.list.d$ cat ddebs.list 
        deb http://ddebs.ubuntu.com/ precise      main restricted universe multiverse
        deb http://ddebs.ubuntu.com/ precise-security main restricted universe multiverse
        deb http://ddebs.ubuntu.com/ precise-updates  main restricted universe multiverse
        deb http://ddebs.ubuntu.com/ precise-proposed main restricted universe multiverse

  其实,对于16.04之前的版本,只需要第一行,为了统一,可以都多加几行。

  2,添加访问符号服务器的密钥文件:

        wget -O - http://ddebs.ubuntu.com/dbgsym-release-key.asc | sudo apt-key add -

  以下是执行过程: 

            --2018-03-09 22:29:51--  http://ddebs.ubuntu.com/dbgsym-release-key.asc
            Resolving ddebs.ubuntu.com (ddebs.ubuntu.com)... 91.189.90.217
            Connecting to ddebs.ubuntu.com (ddebs.ubuntu.com)|91.189.90.217|:80... connected.
            HTTP request sent, awaiting response... 200 OK
            Length: 2471 (2.4K) [text/plain]
            Saving to: `STDOUT'
            100%[======================================================>] 
            2,471       --.-K/s           in 0s      
            2018-03-09 22:30:00 (147 MB/s) - written to stdout [2471/2471]
            OK

   3,执行sudo apt-get update更新

   4,执行如下命令开始下载符号包:

   sudo apt-get install linux-image-`uname -r`-dbgsym

  对于64位的16.04 LTS,安装过程需要占用大约4GB的磁盘空间,也需要下载一会。

  安装过程如果顺利结束,那么符号文件会被安装到/usr/lib/debug/目录下,最重要的kernel文件在boot子目录下。

  可以使用gdb加载内核文件(相当于把内核当作可执行程序来调试),通过读取其中的变量来确认内核文件的版本,如下图所示。

  从vesion字段可以拷打详细的版本信息。从machine字段可以看出内核是64位的。

因为内核调试需要两台机器,所以实际调试时应该在安装有相同版本Ubuntu的调试机器上执行上诉步骤。如果版本不同,那么应该把变量部分(开发代号和版本号)替换成目标机的信息。比如直接执行:

    sudo apt-get install linux-image-4.13.0-36-generic-dbgsym

  第三个问题是源文件,长话短说,也可以从Ubuntu下载安装,需要3个步骤:

1,打开/etc/apt/sources.list文件,将原本注释掉的deb-src行启用回来,也就是把代表注释的#号去掉。

deb-src http://cn.archive.ubuntu.com/ubuntu/ precise main restricted

2,执行sudo apt-get update。

3,安装源文件:

    apt-get source linux-source

  准备工作差不多好了,接下来是开始建立会话调试了,web编辑器很不给力,编辑的好费劲,休息一下,我们将在下一篇文章中继续介绍。

[2018/4/3补充]

  今日在Ubuntu 16.04(64位)上使用上诉方法下载内核源代码时,出现如下提示:

  使用提示中的git命令下载只下载很少的文件后就结束了,后来使用如下命令直接从kernel.ubuntu.com下载可以了

   $ git clone git://kernel.ubuntu.com/ubuntu/ubuntu-trusty.git

 




发布于 2018年3月9日 13:19admin0 篇评论

通过互联网做内核调试
Win8开始支持通过有线网络做内核调试,这个功能一般被简称为KDNET。最近有人在OSR的windbg讨论组里询问这种调试方式是否可以跨互联网上工作?

这是提问者提供的部分信息:

“Microsoft (R) Windows Debugger Version 6.3.9600.17029 AMD64 Copyright (c) Microsoft Corporation. All rights reserved.

 

Using NET for debugging

Opened WinSock 2.0

Waiting to reconnect...

Connected to target x.x.x.x on port 50000 on local IP y.y.y.y.

Connected to Windows 8 9600 x64 target at (...)), ptr64 TRUE Kernel Debugger connection established.”

看起来已经成功建立连接,而且Host端已经拿到了目标机的一些信息。

但是也看到错误信息:

Received an out of order encrypted packet.  Packet dropped.

Bad packet sent from x.x.x.x.

问题发出后,很快得到了非常权威的回复。回复者是微软的Joe Ballantyne。Joe是KDNET的开发者,我也曾就KDNET的问题和他通过很多次邮件。

Joe的回答是肯定的,可以跨互联网调试。

We have successfully used KDNET to debug machines on the other side of the world just fine.

 

”It was part of what we tested when KDNET was first developed.

 

“Now granted, that was on Microsoft's internal corporate network, but we debugged machines in India from Redmond, without issue.”

据此可以肯定的说,在某些情况下,KDNET是可以跨互联网工作的。


发布于 2015年1月27日 20:52Raymond0 篇评论

戏说IRQL(2)

做软件是多么好的一个工作啊,但有时,做软件又是多么差的一个工作啊!哈哈,脑海中突然冒出这么两句,权作这个IRQL系列的第二集的开篇吧!

书接上回,继续讲上次的试验。请出WinDBG,附加到目标系统,开始内核调试,kn:

kd> knL
 # ChildEBP RetAddr 
00 8af1fa50 83093bfb nt!RtlpBreakWithStatusInstruction
01 8af1fa58 83093bcd nt!KdCheckForDebugBreak+0x22
02 8af1fa88 83093a5b nt!KeUpdateRunTime+0x164
03 8af1fae0 830983e3 nt!KeUpdateSystemTime+0x613
04 8af1fae0 9574b43c nt!KeUpdateSystemTimeAssist+0x13
05 8af1fb6c 9574baf0 RealBug!RoamAtIRQL+0x1c
06 8af1fb90 9574bcf0 RealBug!RealBugDeviceControl+0xc0
07 8af1fbdc 8335e6c3 RealBug!RealBugDispatch+0x90
08 8af1fc00 83069efb nt!IovCallDriver+0x258
09 8af1fc14 8323f2e7 nt!IofCallDriver+0x1b
0a 8af1fc34 832416da nt!IopSynchronousServiceTail+0x1f8
0b 8af1fcd0 83248727 nt!IopXxxControlFile+0x6aa
0c 8af1fd04 8307079a nt!NtDeviceIoControlFile+0x2a
0d 8af1fd04 775c64f4 nt!KiFastCallEntry+0x12a

栈帧4和5之间明显有中断的痕迹,其实就是著名的时钟中断,#5就是我们上期提到的RoamAtIRQLRoamAtIRQL函数,在中断发生前,CPU在那里转圈,但尽管是在26这样的高IRQL转圈,因为时钟中断具有更高的IRQL(28),所以还是将RoamAtIRQLRoamAtIRQL函数打断了,令CPU跳出循环去执行时钟中断。

执行!pcr命令观察CPU的控制区:

kd> !pcr
KPCR for Processor 0 at 83140c00:
    Major 1 Minor 1
 NtTib.ExceptionList: 8aec130c
     NtTib.StackBase: 00000000
    NtTib.StackLimit: 00000000
  NtTib.SubSystemTib: 801c8000
       NtTib.Version: 0001ca27
   NtTib.UserPointer: 00000001
       NtTib.SelfTib: 7ffdf000

             SelfPcr: 83140c00
                Prcb: 83140d20
                Irql: 0000001f
                 IRR: 00000004
                 IDR: ffff2070
       InterruptMode: 00000000
                 IDT: 80b95400
                 GDT: 80b95000
                 TSS: 801c8000

       CurrentThread: 910a3030
          NextThread: 00000000
          IdleThread: 8314a240

           DpcQueue:

注意其中的IRQL字段,这就是记录在CPU控制区中的IRQL值,也是常常把IRQL说成是CPU属性的一个原因。

                Irql: 0000001f       

但是细心的读者可能立刻生出一个疑问,为什么是1f(31),而不是26呢?

原因是,现在已经停在调试器了,在中断到调试器时,内核调试引擎会提升IRQL到HIGH_LEVEL(X86上也就是31),最高级别。因为这个原因,在软件调试器中观察时,CPU控制区中记录的IRQL永远是31。为了解决这个问题,可以观察调试引擎提升IRQL之前的本来IRQL,从Server 2003开始,会故意将老的IRQL值保存在PCR的DebuggerSavedIRQL字段中,并可以通过!irql这个扩展命令将其读出来:

执行!irql命令:

kd> !irql
Debugger saved IRQL for processor 0x0 -- 26

直接观察,也可以看到:

kd> dd 83140d20+4c4
831411e4  0000001a

那么为什么CPU在IRQL 26兜圈时,系统就表现出挂死的症状呢?原因是鼠标键盘中断对应的IRQL都是属于设备IRQL范围,都是低于26的,这意味着鼠标键盘中断都被屏蔽了。另一个重要的原因是普通线程的IRQL是0,因此绘制窗口这样的代码根本没机会执行。

IRQL 0有很多个别名,其中之一叫PASSIVE_LEVEL,被动级别,何谓被动级别,意思是它只能被动等待被执行,从来没有机会去主动抢夺CPU的执行权。IRQL 0的另一个常见别名叫LOW_LEVEL,与最高级别的HIGH_LEVEL相对应。

是时候把所有的IRQL定义请出来了,在WDK的头文件中就可以找到它们:

// wdm.h

#define PASSIVE_LEVEL 0             // Passive release level
#define LOW_LEVEL 0                 // Lowest interrupt level
#define APC_LEVEL 1                 // APC interrupt level
#define DISPATCH_LEVEL 2            // Dispatcher level
#define CMCI_LEVEL 5                // CMCI handler level

#define PROFILE_LEVEL 27            // timer used for profiling.
#define CLOCK1_LEVEL 28             // Interval clock 1 level - Not used on x86
#define CLOCK2_LEVEL 28             // Interval clock 2 level
#define IPI_LEVEL 29                // Interprocessor interrupt level
#define POWER_LEVEL 30              // Power failure level
#define HIGH_LEVEL 31               // Highest interrupt level

#define CLOCK_LEVEL                 (CLOCK2_LEVEL)

看这个列表,通过前面的实验,我们对HIGH_LEVEL、CLOCK_LEVEL和一般分给硬件设备中断的IRQL 26已经有所认识了。 对于前几个可能还有一些疑问,尤其是2和1这两个级别,它们被统称为软件中断级别,我们下一次继续讲。

 

发布于 2013年9月14日 21:34Raymond0 篇评论

使用USB3.0调试Windows 8

Windows 8,成也,败也?众说纷纭。但无论如何,我很喜欢它所作出的如下改变:

- 开发语言回归C++

- 旧的Win32与新的WinRT两套API的“双头”模式

- 重视内核调试,引入两种新的连接方式,并将内核调试支持纳入徽标测试

当然,也有不喜欢的地方,首当其冲的就是新的启动选项界面——居然是一个应用程序,需要内核先起来才能运行,如果内核起不来,那么这个启动选项根本没办法出来,想靠它抢救起不来的系统,基本没指望,真是十足“脑残”的设计。

真的不愿意看Windows走下坡路,还是回过来说它的优点吧,今天就聊一下新的USB3调试。

主机端要求:

H.1 支持USB 3的端口;

H.2新版本的WinDBG6.2.9200或者更高;

H.3 Windows 8系统。

 

前两项是必须的,没得商量,第3项按理说Windows 7也可能成功,但是官方的说法要求Windows 8。笔者曾经在Windows 7上试验过,有USB 3硬件,USB 3的驱动和USB2DBG驱动也都安装成功了,但是还是没能建立起调试连接,原因应该是和总线驱动有关,内建的USB 3驱动是从Windows 8才开始的。

 

目标机的要求:

T.1 Windows 8系统;

T.2 有可用的USB 3端口,下面详细描述。

T.3 USB 3的控制器是支持调试的,与此前的USB 2调试类似,调试痛信是一种特殊的简单通信,要求USB 3控制器特别支持才行,但与此前USB 2的控制器只有0号口支持调试不同,USB 3的控制器的所有3.0口都是支持调试的,因此这个要求一般都满足,可以使用USBView工具检查是否支持Debug,如图1所示;

T.4 启用内核调试。

图1 UsbView

USB 3的端口有很多种,PC上常见的有AB两种,A与以前的USB 1.0/2.0端口看起来很像,物理属性是兼容的,每部的信号线有不同,识别是否是USB 3端口的简单方法就是看是否是有SS标记,SS代表Super Speed,如果标记是SS,那么就是USB 3端口。

USB 3B端口是长相很特别,一道沟槽/凸起将端口分为不对称的两个部分。图2照片中的线是把B口转成A口的,通过这个图,大家就可以知道AB两种口的长相了。

图2 USB 3的B-A转接电缆(和黑布林在一起,压缩算法把bitrate似乎都分在布林身上了,线不大清楚)

 

接下来该说电缆了,与USB 2调试所需的中间带有设备的特别电缆不同,USB 3调试需要的是一条真正的线。哪里能买到这种线呢?国外有公司卖。经过一番调查和尝试,其实也可以从国内买,然后略微加工一下就可以了。这种方法是由一位聪明的同事试验成功的,我亲自尝试了一下,确实简单有效。

从淘宝买一根USB 3AA连线,有时也称公对公连线,很便宜。这根线需要加工一下才可以支持调试,加工的方法是选取线的某个位置,剥开外皮,然后把其中的红绿白三根线剪断,然后包上就可以了。USB 3电缆的线是有固定颜色的,如图3所示,

图3 花花绿绿的USB 3线缆

 

其中SDPShielded Differential Pair的缩写,即屏蔽起来的差分信号线,是USB 3.0的数据线,UTPUnshielded Twisted Pair的缩写,即未屏蔽的双绞线,是USB1/2使用的数据线,所谓的D+D-。要做的加工其实就是把2.0的三根弦剪断。剥开后,很容易找到红绿白三根,胆大心细,下剪子吧:-)

图4,看准下剪子 (照片不大清楚,剪错了责任自负哦)

 

线做好后,用它来接主机和目标机。

然后需要在目标机上启用调试。打开一个有管理员权限的控制台窗口,执行如下命令:

bcdedit /dbgsettings usb targetname:<名字>

bcdedit /debug on

设置好后需要重启

最后再说一下主机端,安装好新版本的WinDBG后,以管理员身份启动运行,File  > Kernel Debugging,选择USB ,然后指定名称(上面bcdedit里设置的名字)。第一次使用的话,WinDBG会自动安装驱动,这也是要以管理员启动的原因。需要说明的是,在主机端,USB 2.0调试和3.0调试使用的是一个驱动Usb2DBG,不必怀疑。

 

好了,主机端就绪后,按Ctrl + Break,目标机应声断下,设置符号,开始开Windows 8的代码或者找Bug吧!

发布于 2013年8月21日 21:13Raymond0 篇评论

re: 是谁破坏了内核调试
突然发现,调试真的可以改变命运

发布于 2013年7月10日 13:12cgc20xin2 篇评论

re: 是谁破坏了内核调试
等着张老师的另文记述啊,呵呵

发布于 2013年5月17日 10:17s56894122 篇评论

是谁破坏了内核调试

这是前些日发生的一个故事,星期五一早到单位,开机收邮件,发现一封急件,要求一定要在xxx日之前打好某个补丁。对于这样的邮件,不得不重视,于是就打吧,过了一阵,要求重启系统,那就重启吧。没想到,重启之后系统一直停留在一个画面,显示如下两行字:

“Preparing to configure Windows"

"Do not turn off your computer”

当然除了这两行字,还有一个代表忙碌的旋转图标,转啊转啊,转啊转啊,5分钟过去了,还转啊转啊,十分钟过去了,还转啊转啊,转啊转啊...

实在看不下去了,强制蓝屏后重启,结果还是这个画面,转啊转啊...

尝试进安全模式,但也遇到同样的问题,转啊转啊...

尝试"Last known good...",也是转啊转啊...

尝试WRE,顺利进入到修复环境,但是因为主系统的Windows 7使用了全硬盘加密,所以无法休息,晕倒。

怎么办,上调试器,在WRE中启用内核调试,选择串口方式,将本子放在dock上,找到另一台有串口的机器,开始调试。

起初很顺利,目标机启动后,WinDBG便收到了握手信息,可以顺利断下来。因为问题出在登录阶段,所以g,让目标系统继续跑,过了一会,转啊转啊的场景又出现了,心里想,这下可以看看到底是为什么停在这里了,激动的按下Ctrl + Break,然后等待目标系统“应声落马”,乖乖的中断到调试器中,但实际却没有反应,目标系统根本没有中断下来,还是在那里转啊转啊,转啊转啊,真是气煞我也!

眼看着时间一分一秒的过去,上午本来要做的事情还没有做,但是电脑进不去,啥也干不成啊。是可忍,孰不可忍。脱下外套,倒一杯茶,准备好打一场硬仗。

第一步要搞清楚的是为什么出现“转啊转啊”的时候内核调试不工作了。启动初期工作的好好的,后来为何不工作了呢?

根据经验,一定是被哪个坏蛋破坏掉了。怎么抓住破坏内核调试的黑手呢?一种方法就是监视禁止内核调试的KD函数(KdDisableDebuggerWithLock和KdDisableDebugger),当坏蛋调用这个函数时将其当场抓住。

于是bp KdDisableDebuggerWithLock;bp KdDisableDebugger将两个函数都设置上断点,然后再g恢复目标运行。

过了一会,登录桌面出现,就要进入“转啊转啊”阶段了,这时断点命中了,看来坏蛋正是在这个时候对内核调试下狠手了。k命令追查调用者:

2: kd> k
Child-SP          RetAddr           Call Site
fffff880`035b6828 fffff880`0838840a nt!KdDisableDebuggerWithLock
fffff880`035b6830 fffff800`03c66047 PECKP_x64+0x540a
fffff880`035b6860 fffff800`03c66445 nt!IopLoadDriver+0xa07
fffff880`035b6b30 fffff800`03882ca9 nt!IopLoadUnloadDriver+0x55
fffff880`035b6b70 fffff800`03b1a34a nt!ExpWorkerThread+0x111
fffff880`035b6c00 fffff800`0386a946 nt!PspSystemThreadStartup+0x5a
fffff880`035b6c40 00000000`00000000 nt!KxStartSystemThread+0x162

狐狸露出尾巴了,名字叫PECKP_x64。还挺隐晦的名字。lm观察详情:

3: kd> lm vm pec*
start             end                 module name
fffff880`086f3000 fffff880`08708000   PECKP_x64   (deferred)            
    Image path: \??\C:\Windows\system32\drivers\PECKP_x64.SYS
    Image name: PECKP_x64.SYS
    Timestamp:        Thu Jul 12 23:06:36 2012 (4FFEE7FC)
    CheckSum:         0000B31B
    ImageSize:        00015000
    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4

没有描述,只好搜索了,Google加baidu,查到了,原来是某银行网银控件的一部分,所谓的键盘保护驱动,防止用户输入密码时被嗅探。出发点是好的,但为什么要破坏内核调试啊?怕被跟踪么?哎,那也不能这样干啊,亲爱的同行。

u看了下汇编,没有两秒钟,就想出了一个化解的方法:

eb nt!KdBlockEnable 1

然后跟踪几下,KdDisableDebuggerWithLock就直接返回了。再g继续,转啊转啊的又出现了,再次Ctrl + Break,目标系统应声停下,转啊转啊的图标也定格在那里了。

又经过一番分析,发现问题与TrustedInstaller.exe有关,一通追查(另文记述),问题排除,可以进入系统了。


发布于 2013年5月4日 21:39Raymond2 篇评论

2012年的收获

对我来说,2012年的最大收获要算是又完成了一本书。不算翻译的书,这是我的第二本书。书名叫《格蠹汇编——软件调试案例集锦》。

因为篇幅限制,《软件调试》中没有包括较长的案例,关于堆的一个较长案例也放入“补编”中了。《软件调试》出版后,就想着写案例性的书。曾经想过按问题分册来写,但是这样每一册的覆盖面就会比较窄。

如今,《软件调试》出版将近五年时间了,终于完成了第一层案例分析,概况如下:

- 全书36章,篇幅在480页左右,是《软件调试》一半篇幅还略少,目的是可以在一年内轻松读完。

- 36章内容分为4篇,每篇9章。前两篇重在实例解析,第3篇以案例形式介绍调试工具和调试方案,第4篇介绍重要的调试“对象”和“过程”。

- 书中设计了10个动手实验,以便大家亲自“操练”。

- 除了正文还,还有5个附录,分别是实验材料安装方法,内核调试环境设置方法,面向问题的索引,术语索引,WinDBG命令索引和常用的汇编指令。

 

对于后两个索引可以在这里预览:http://www.advdbg.org/books/dbgwars/DebugWars_appendix_E&F.pdf  ,更多的预览内容将陆续公布出来。

 

顺祝大家2013新年快乐!

 

 

发布于 2013年1月1日 23:43Raymond6 篇评论

老雷看Win7(4)——发疯内幕

【本文曾发表在《程序员》杂志的调试之剑专栏中】

现代人生活在重重压力中,有时压力太大无法忍受时,便以疯狂的方式来释放压力,有的大喊大叫,有的打人骂人,也有动刀动枪的……其实软件世界也有类似的问题,随着软件被应用到越来越多的重要岗位,人们对软件的期望也越来越高:要长的漂亮,有华丽的外表;要跑的快,反应神速;要功能强大,啥都能干;要灵活通用,能在所有带CPU的盒子上跑;要坚韧鲁棒,能适应千差万别的使用环境;要开放,能以有线无线、局域网、互联网等各种方式通信;而且还要安全,铜墙铁壁,百毒不侵……看看人们对软件的这些要求,您说软件的压力大不大?在如此重负下,软件也难免会有“发疯”的时候。

 

疯狂重启

一位朋友说,他的“Win7疯特了”,没完没了的重启,我答应帮忙看一下。几个小时后,出故障的笔记本电脑摆在了我面前。的确,Win7进入桌面后便会弹出一个如图1所示的对话框,然后大约5秒钟后,系统中的所有程序都会被杀掉,系统开始重启。重启后,可以看到启动的画面,闪烁的Win7徽标,然后看到桌面,但是很快就又出现了要强制重启的“通牒”,这时如果速度足够快,有可能把任务管理器打开,或者做一些其他动作,但是可以动作的时间只有几秒钟,几秒钟过后,所有东西都会被杀掉,然后显示登出(log off)和关机画面,系统复位重启。重启后的现象还是一样,如此启动、关机,再启动,再关机,周而复始,持续不断,Win7看起来真的像“疯”了一样!

1 强制重启通告

 

安全模式无济于事

抱着试试看的心理,在启动前按F8选择,调出高级启动选项菜单(图2),然后选择以安全模式(Safe Mode)启动。

2 尝试安全模式

但是问题没这么简单,故障依然存在,那个已经熟悉的对话框仍然态度坚决的蹦出来(图3),然后很准时的开始杀掉所有程序,开始重启,与普通模式没什么变化。

3 安全模式下的强制重启通告

细心的读者可能注意到图1和图3中的错误信息略有不同,图1中提到的是电源服务(Power service),图3中提到的是PnP服务(Plug and Play service)。但是事实上,在正常模式下,两种提示也都出现过。

也尝试过在恢复到已知的良好配置(Last Known Good Configuration),但也于事无补,问题照旧。

 

是谁杀了关键服务?

简单的方法没能奏效后,我开始思考如何对付这个问题。从图1和图3中的提示信息来看,系统重启的直接原因是关键的系统服务意外终止了(service terminated unexpectedly)。因为有些系统服务(service)承担着重要的职责,它们的“健康”关系到整个系统是否能正常运行,所以系统会监视这些服务,如果发现它们意外退出(终止)了,那么便像有国家政要被谋杀了一样,进入紧急状态,强制“戒严”—— 关闭登录会话,退出窗口系统,强制重启系统…….因为有很多个重要服务,比如日志服务、PnP服务、电源服务、DCOM等,都居住(host)在SvcHost进程中,在那里办公,所以一旦这个进程意外终止,那么很多个关键服务都会受影响。从上面描述的现象来看,很可能是SvcHost进程意外终止了,导致运行在这个进程中的系统服务全完了,可谓“城门失火,殃及池鱼”。那么是谁杀了这个进程呢?

 

选择方法

接下来应该选用什么方法来调试呢?我开始评估各种方法。

很多进程意外终止是因为未处理异常导致进程被强行终止,即通常说的应用程序崩溃(Application Crash)。对付应用程序崩溃的常用方法是JIT调试(《软件调试》12.5节),也就是当程序在被终止前,自动启动JIT调试器。但对于本例,出问题的进程运行在不可见的Session 0中,因此,当JIT调试器被启动后,我们也看不见它,只能将通过另外一个调试器间接的方式来控制它,要么通过内核调试会话来控制,要么通过命名管道等方式来远程调试,前者需要两台机器;后者理论上可以在同一台机器实现,例如,在Session 1中运行一个WinDBG,然后连接到Session 0中的NTSD,但因为本例中很多关键的系统服务都受到影响,所以这很可能导致两个调试器无法建立连接,所以使用远程调试,也应该使用串行口这种依赖系统服务较少的硬件方式,这意味着也需要两台机器。

第二种方法是使用双机内核调试,也就是通过串行口、1394或者USB 2.0电缆来调试出问题的系统。尽管本例中问题发生在用户态,但是仍可以通过内核方式设置断点,或者等待发生未处理异常时中断到内核调试器,然后进行调试。

第三种方式是使用转储文件(dump file),也就是在SvcHost进程崩溃时产生转储文件,然后分析这个转储文件。通常系统的WER机制(《软件调试》第14章)会自动产生转储文件,因此只需要找到并复制出来。

比较以上三种方法,第三种相对来说简单一些,但是只能看到崩溃时的“瞬间快照”,前两种方法都需要两台机器,相对较麻烦,但是可以进行交互式调试。

不妨先尝试第三种方法,不行再用其它两种方法。于是面临的问题便是如何找到转储文件并复制出来。眼下系统反复的重启,每次只能使用几秒钟,要在那几秒钟时间内找到转储文件,然后复制出来难度太大了。怎么办呢?Win7的一个新功能刚好可以完美的解决这个问题。

 

WinRE派用场

很多普通用户可能根本不注意,一个典型的Win7系统中,其实有两个Windows,一个是用户通常使用的,另一个是正常系统出故障时用来紧急恢复用的,后者通常被称为WinREWindows Recovery Environment)。简单来说,WRE是个简化了的Windows,它很小,占用大约200MB的磁盘空间。

如何进入WinRE呢?与进入安全模式的方法是类似的,也就是在图2所示的高级启动菜单中选择Repair Your Computer

进入WinRE后,启动一个命令行窗口,然后切换到Win7的系统盘。值得注意的是,WinRE映射的盘符与正常系统中看到盘符很可能是不一样的,C盘一般是所谓的系统保留分区,D盘一般是Win7的系统盘,可以通过文件来确定。在本例中,D盘是Win7的系统盘,于是切换到D盘后,执行dir *.mdmp /s以下命令来寻找WER机制产生的转储文件,如图4所示。

4 寻找转储文件

哦,真的存在,因为WinREUSB磁盘支持的非常好,因此只要插入一个U盘就可以把找到文件复制出来了。

 

分析转储文件

在正常的系统中,启动WinDBG打开复制过来的转储文件。加载过程中,WinDBG显示的如下信息值得注意:

 (280.2a4): Stack buffer overflow - code c0000409 (first/second chance not available)

上面这句话是说在280号进程的2a4号线程中发生了缓冲区溢出,这个缓冲区是分配在栈上的。用~*命令列出所有线程,可以看到当前线程就是这个发生溢出的线程,执行kn命令观察栈回溯,其结果如图5所示。

5 溢出线程的栈回溯

从栈回溯中可以看到几个Wer开头的函数,这说明这个进程在终止前调用了WER设施,这正是我们能得到这个转储文件的原因。#08栈帧中的函数是UnhandledExceptionFilter,这是位于kernel32.dll中的用于处置未处理异常核心函数,它也是系统在终止掉一个进程前做最后处理的地方,应用程序错误对话框和JIT调试都是从这个函数发起的,《软件调试》的第12章曾深入讨论过这个函数,并给出了伪代码。通常这个函数下面就是导致异常的函数了。看一下#09栈帧,函数名叫__report_gsfailure,模块名被我们故意隐掉了,我们不妨就称它为模块U,下一个栈帧的函数也是位于模块U中,我们称那个函数为函数U

再看一眼栈帧#0b,我基本明白了故障的原因,简单来说,是模块U中的函数U发生了缓冲区溢出,当这个函数要返回时,编译在函数中溢出检查代码检测出了溢出,于是调用__report_gsfailure函数报告错误。这种检测溢出的方式通常称为基于Cookie的溢出检查(《软件调试》22.12),简称GS机制。

 

谁动了我的甜饼?

简单来说,GS机制就是在可能发生溢出的函数所使用的栈帧起始处(EBP-4的位置),存放一个称为Cookie的整数,在函数返回时检查这个Cookie是否完好,如果被破坏了,就说明函数中发生了溢出。部署和检查Cookie的代码都是编译器在编译时加入到函数中的。反汇编模块U的函数U,我们可以看到存入Cookie的代码:

751a15fb a190a31b75 mov     eax,dword ptr [XXXX!__security_cookie (751ba390)]

751a1600 33c5       xor     eax,ebp

751a1602 8945fc     mov     dword ptr [ebp-4],eax

和函数返回前检查Cookie的代码:

74ed166f e82efdffff call    XXXX!__security_check_cookie (74ed13a2)

从图5可以知道函数U的栈帧基地址(EBP)是009afb30,于是使用dd命令可以观察目前栈上的Cookie值、父函数EBP值和函数返回地址值:

0:007> dd 009afb30-4 l4

009afb2c  00640064 00640064 006a002e 00670070

从上面的结果看,Cookie值为00640064,父函数的EBP值为00640064,函数返回地址为006a002e,这些值看起来都不大像合适的内存地址,倒都像ASCII代码。看来,因为缓冲区溢出,这些重要的信息都被冲掉了,变成其它数值,也正因为这个原因,图5中,#0b号栈帧的函数名字段只能显示006a002e,因为WinDBG无法找到这个地址所对应的有效符号。

那么,在函数U中到底发生了什么呢?反汇编整个函数,便可以得到它的伪代码:

ULONG FuncU(PVOID p, int nLength)

{

XXX_MSG  msg;

 

memset(&msg, 0, 0x200);

memcpy(&msg, p, nLength);

return FuncV(…);

}

这段代码有问题么?很多程序员都会发现并且大声说有问题,但是在实际编写代码的时候,他们还是会写出这样的代码。上面的代码在大多数时候应该都是可以工作的,但是当第二个参数的取值大于5120x200)时便有问题了,这时会有超出局部变量m长度的数据写向这个缓冲区,超出的部分会把Cookie覆盖掉。因为放在栈上的参数原始值也被覆盖掉了,我们无法直接看到它们的值,但是因为函数中会把参数指定的内存区复制到栈帧中,因为我们可以通过观察整个栈帧来了解传进来的内容:

变量区的长度是0x204,因此我们从EBP-204开始显示,显示的长度为0x210字节。依稀可以看到,从第4行开始一直到结束是一个很长的文件名:c:\users\public\...\...dd.jpg

仔细核对一下,上面我们观察的放Cookie、父函数EBP和返回地址的地方(009afb2c~009afb38)正好是被这个长文件名给覆盖掉了。

 

为了安全

现在可以知道,因为模块U的函数U接受到一个比“预想长度”还长的参数时发生了缓冲区溢出,触发了GS机制,让当前线程开始执行__report_gsfailure函数。那么这个函数中都会做什么呢?在Visual Studio的源文件中可以找到它的源代码,源文件名为gs_report.c,典型的完整路径为:

c:\Program Files\Microsoft Visual Studio 8\VC\crt\src\gs_report.c

看一下这个函数的源代码,并不复杂,它主要做两件事,第一件是模拟一个异常现场,然后调用UnhandledExceptionFilter,这一步的主要目的是支持JIT调试和WER;第二件事便是终止当前进程:

TerminateProcess(GetCurrentProcess(), STATUS_STACK_BUFFER_OVERRUN);

看来SvcHost进程是因为发生缓冲区溢出而“自杀”的。为什么要如此做呢?假设没有GS机制,因为函数U的返回地址已经被破坏了,所以函数U就会返回到一个由参数内容所决定的未知地方,如果参数内容是精心设计的,那么函数U便可能返回到一个精心设计的地方,执行一段精心设计的代码,于是这个进程便成了恶意代码进入系统的“登陆地”,后果可能比“发疯”还糟。

 

恢复正常

原因找到了,要排除故障只要找到那个意外的参数是哪里来的。在WinRE中打开注册表,搜索那个超长的文件名,果真找到了,将其改为一个典型长度的文件名,然后关闭注册表编辑器,重启,系统恢复正常了。为了防止本文描述的问题在正式修复前被人用来做坏事,我们就不介绍可以重现这个问题的注册表表键了,前面故意隐掉有问题的模块名和函数名也是这个原因。

发布于 2010年1月24日 21:32Raymond6 篇评论

re: 在内核调试会话中设置用户态断点
这个其实没有必要在kd里面看,可以在windbg中设断点,然后用另外一个调试器实例,以非入侵的方式查相应地址,即可发现调试器在后台做的手脚

# re: 在内核调试会话中设置用户态断点 @ 2008年11月19日 20:15
对于WinDBG调试器,每次中断给用户前会恢复所有软件断点,所以在WinDBG中看不到它自己社的断点。当让WinDBG恢复目标运行后,断点恢复了,所以是可以用ReadProcessMemory看到。

发布于 2009年12月15日 16:37skyworth7 篇评论

re: 在内核调试会话中设置用户态断点
To diyhack, 本来应该是Copy/Paste时搞错了,已经纠正,多谢!

发布于 2009年9月5日 23:23Raymond7 篇评论

宝贵的串口

串口越来越少了,不单是笔记本很少有串口,甚至不少台式机也没有串口了。就这个问题,我曾经和做主板设计的工程师聊过多次,尽管聊的人不一样,但是对话的过程却都惊人的相似。

“现在谁还用串口?”

“内核调试要用呀。”

“哦,那很少用的吗。”

“是不总用,但是系统出问题的时候要用的,一旦需要往往都是很重大的问题。”

“毕竟不常用的吗。”[笑]

“灭火器也不常用啊,为啥每个楼层都得准备着呢?”

“那是强制的呀!”

......

继续按这个思路说不下去了,是啊,支持调试不是强制的呀!?

“加串口要多少费用?”

“没什么费用,只是引出来就行了。”是啊,其实主板上用来完成Super IO的集成块里几乎都实现了不止一个串口。

“那怎么不加呢?”

“没什么用啊,哈哈。”[笑]

“*&$^/?$#@......”

“但是最终用户不用啊!”

得,没啥继续好说的了。最终用户不用就代表着加这个东西不能增加销量,那加这个东西可不没用?!

但也有他们感觉后悔的时候。那就是当系统出问题无计可施的时候,比如,启动一半蓝屏;系统睡眠后无法唤醒;关机关不掉,就挂在那儿......

“XX操作系统不知道在搞什么?”

[保持沉默]

“硬件都查过了,没发现问题呀...”

[继续沉默]

“你们软件有啥办法吧?”

现在到时候普及一下“可调试性”教育了。“当初*&$^/?$#@...但是*&$^/?$#@...现在*&$^/?$#@...”

 

问题总是要解决的,即使真的没有串口。这时怎么办呢?一种方案是临时增加串口,就像软件工程师可以在编译好的程序中,直接把某几条指令替换掉一样,硬件工程师可以通过
“飞线”来修改电路(rework)。但这可不那么容易做,看看今天的主板就知道,线路密集无比,寸土必争;元器件大多都是适合流水线工艺的可以“粘贴”到板子上的微小元件。想找个飞线的落脚点真好比是要把一架飞机迫降到一座繁华城市的CBD。

这时要看当初设计电路时是否未雨绸缪留用“扩展”余地了。有时布线本来可能已经有所准备,只是没有上某些元件,那么加上去就比较容易。如果系统中事先故意预留了某种总线接口,比如LPC(Low Pin Count)总线,那么也比较好办的,可以把一个带串口的小电路板通过LPC总线“嫁接”过来。这样嫁接成功后,通常还需要修改BIOS,配置这个“扩展卡”,为其分配必要的资源。

说到这,很多人都会问:“没有其它方式来做内核调试么?”

有,比如1394和USB 2.0端口。对于1394,最大的问题就是不稳定,两端的1394芯片有兼容性问题。对于USB 2.0,姑且不说要专用的Host to Host电缆,而且对目标机器端的USB端口号还有要求。简单来说,并不是所有USB 2.0的端口都可以用来调试,以英特尔的芯片组为例,只有每个EHCI的0号端口才支持内核调试。EHCI一般实现在南桥(ICH)中,ICH9开始包含两个EHCI,此前一般只有一个EHCI,这就意味着,ICH9以前的系统只有一个支持调试的USB端口,使用ICH9的较新系统也只有两个USB端口能用来调试。如何知道哪个端口是0号端口呢?可以使用一个名为UsbView的小程序,启动它后,拿一个USB 2.0的设备,比如优盘,依次插到每个USB端口,然后刷新UsbView,看这个设备所在的端口。当这个设备出现在EHCI(Enhanced Host Controler)下面的端口1(UsbView是从1开始编号的)位置时,恭喜您,调试端口找到了!

也有时候,试过所有可以插入USB设备的端口,都没有发现0号端口,这也是可能的,因为ICH一共提供了8个或者12个端口,通常只有一部分连接到外部可见的端口,有些是不引到外部的。哪些引出,哪些不引出,完全看电路设计。

考虑到它对于内核调试的重要性,UsbView这个小工具目前已经被放入到WinDBG软件包中,默认会安装到WinDBG的程序目录中。

找到支持调试的端口了,接下来需要启用内核的USB调试支持,通常是使用BCDEdit来设置。除了将DebugType设置为USB,并约定一个TargetName外,对于ICH9这样有多个EHCI的系统,还需要指定另外一个参数——busparams,告诉系统调试端口所在EHCI的总线位置,即:

 bcdedit -set {current} loadoptions busparams=0.1D.7

上面的0.1D.7便代表EHCI的总线位置,0号总线(Bus),0x1D号设备(Device),7号功能(Function)。如何知道EHCI的总线位置呢?可以在设备管理器中,通过EHCI的设备属性对话框来看。

哇,大家是不是觉得很麻烦呀?是滴。串口简单,但是不总有啊!

在服务器系统中,某些串口外表长的和网口一样,即RJ45形式。这是因为串口接头不像RJ45那么方方正正节约空间。对于这样的串口,只要找一个或者自己做一个转换头就可以了。

 

啥时候大家都觉得支持调试就像准备灭火器那么必不可少,我们就不用担心没有端口支持调试了。

 

发布于 2009年9月5日 7:42Raymond2 篇评论

< 1 2 3

Powered by Community Server Powered by CnForums.Net