精华内容
下载资源
问答
  • 本文主要讲了电动机软启动器工作原理,一起来学习一下
  • 启动器工作原理与选用doc,软启动器工作原理与选用:软启动器自动用旁路接触器取代已完成任务的晶闸管,为电动机正常运转提供额定电压,以降低晶闸管的热损耗,延长软启动器的使用寿命。
  • 通过对矿用防爆磁力启动器工作原理、内部电路、维修技巧及故障处理流程图的论述,总结了该启动器常见故障检测和维修的方法,并给出了"只有一个接触器吸合"的故障处理流程图,以方便现场工作人员的理解和操作。
  • 启动器工作原理

    2013-09-29 15:36:02
    好用软启动气的原理,曼好使的,初学者管用
  • 启动器(软起动器)一种集电机软起...使用软启动器启动电动机时,晶闸管的输出电压逐渐增加,电动机逐渐加速,直到晶闸管全导通,电动机工作在额定电压的机械特性上,实现平滑启动,降低启动电流,避免启动过流跳闸。
  • 本文主要讲了一下关于电机软启动器工作原理,一起来学习一下
  • 本文主要讲了时间继电星三角降压启动电路图及工作原理,下面一起来学习一下
  • 主要介绍了SpringBoot启动器Starters使用及原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • 启辉器工作原理

    2015-09-24 14:32:22
    启辉器工作原理是: 当开关接通的时候,电源电压立即通过镇流器和灯管灯丝加到启辉器的两极。220伏的电压立即使启辉器的惰性气体电离,产生辉光放电。 启辉器的双金属片受热膨胀,因为动静触片的膨胀程度不同,U...
    启辉器工作原理是:
    当开关接通的时候,电源电压立即通过镇流器和灯管灯丝加到启辉器的两极。220伏的电压立即使启辉器的惰性气体电离,产生辉光放电。   启辉器的双金属片受热膨胀,因为动静触片的膨胀程度不同,U形动触片膨胀伸长,与静触片接触而接通电路,于是镇流器的两极接触。电流通过镇流器、启辉器触极和两端灯丝构成通路。这时,由于启辉器两极闭合,两极间电压为零,启动器中的氖气停止导电,辉光放电消失,导致管内温度下降,U形动触片冷却收缩,两触片分离,电路自动断开。   在两极断开的瞬间,电路电流突然切断,镇流器产生很大的自感电动势,与电源电压叠加后作用于管两端。灯丝受热时发射出来的大量电子,在灯管两端高电压作用下,以极大的速度由低电势端向高电势端运动。在加速运动的过程中,碰撞管内氩气分子,使之迅速电离点亮。	镇流器在启动时产生瞬时高压,在正常工作时起降压限流作用;启辉器中电容器的作用是避免产生电火花。	点亮后启辉器两端电压低于启辉器的电离电压,所以并联在两端的启辉器也就不再起作用了。   启辉器中还有个电容,与氖泡并联,作用是吸收辉光放电而产生的谐波,以免影响电视,收音机,音响,手机等设备的正常运作.还能使动静触片在分离时不产生火花,以免烧坏触点,没有电容器,启动器也能工作。
    展开全文
  • TRIAC调光(正向相位控制调光)的工作原理。将R1、R2及C1连接一起的RC电路可以令TRIAC调光延迟启动,直至C1的电压上升至触及交流二极管(Diac)的触发点电压。
  •  它的工作原理流程是控制电路控制整个系统的运行,逆变电路完成由直流电转换为交流电的功能,滤波电路用于滤除不需要的信号,逆变的工作过程就是这样子的了。  其中逆变电路的工作还可以细化为:首先,振荡电路...
  • 变频工作原理.pdf

    2019-10-22 07:11:31
    变频工作原理pdf,变频是利用电力半导体器件的通断作用将工频电源变换为另一频率的电能控制装置。变频能实现对交流异步电机的软启动、变频调速、提高运转精度、改变功率因数、过流/过压/过载保护等功能。
  • Linux下调试器工作原理

    千次阅读 2014-01-27 14:19:37
    Linux下调试器工作原理之一—基础篇 介绍关于Linux下的调试器实现的主要组成部分——ptrace系统调用。本文中出现的代码都在32位的Ubuntu系统上开发。请注意,这里出现的代码是同平台紧密相关的,但移植到别的...

    Linux下调试器工作原理之一—基础篇

    介绍关于Linux下的调试器实现的主要组成部分——ptrace系统调用。本文中出现的代码都在32位的Ubuntu系统上开发。请注意,这里出现的代码是同平台紧密相关的,但移植到别的平台上应该不会太难。

    动机

    要想理解我们究竟要做什么,试着想象一下调试器是如何工作的。调试器可以启动某些进程,然后对其进行调试,或者将自己本身关联到一个已存在的进程之上。它可以单步运行代码,设置断点然后运行程序,检查变量的值以及跟踪调用栈。许多调试器已经拥有了一些高级特性,比如执行表达式并在被调试进程的地址空间中调用函数,甚至可以直接修改进程的代码并观察修改后的程序行为。

    尽管现代的调试器都是复杂的大型程序,但令人惊讶的是构建调试器的基础确是如此的简单。调试器只用到了几个由操作系统以及编译器/链接器提供的基础服务,剩下的仅仅就是简单的编程问题了。(可查阅维基百科中关于这个词条的解释,作者是在反讽)

    Linux下的调试——ptrace

    Linux下调试器拥有一个瑞士军刀般的工具,这就是ptrace系统调用。这是一个功能众多且相当复杂的工具,能允许一个进程控制另一个进程的运行,而且可以监视和渗入到进程内部。ptrace本身需要一本中等篇幅的书才能对其进行完整的解释,这就是为什么我只打算通过例子把重点放在它的实际用途上。让我们继续深入探寻。

     

    遍历进程的代码

    我现在要写一个在“跟踪”模式下运行的进程的例子,这里我们要单步遍历这个进程的代码——由CPU所执行的机器码(汇编指令)。我会在这里给出例子代码,解释每个部分,本文结尾处你可以通过链接下载一份完整的C程序文件,可以自行编译执行并研究。从高层设计来说,我们要写一个程序,它产生一个子进程用来执行一个用户指定的命令,而父进程跟踪这个子进程。首先,main函数是这样的:

     

     
    1. int main(intargc,char** argv)  
    2. {  
    3.     pid_t child_pid;  
    4.    
    5.     if(argc < 2) {  
    6.         fprintf(stderr,"Expected a program name as argument\n");  
    7.         return-1;  
    8.     }  
    9.    
    10.     child_pid = fork();  
    11.     if(child_pid == 0)  
    12.         run_target(argv[1]);  
    13.     elseif(child_pid > 0)  
    14.         run_debugger(child_pid);  
    15.     else{  
    16.         perror("fork");  
    17.         return-1;  
    18.     }  
    19.    
    20.     return0;  
    21. }  

     

    代码相当简单,我们通过fork产生一个新的子进程。随后的if语句块处理子进程(这里称为“目标进程”),而else if语句块处理父进程(这里称为“调试器”)。下面是目标进程:

     

     
    1. void run_target(constchar* programname)  
    2. {  
    3.     procmsg("target started. will run '%s'\n", programname);  
    4.    
    5.     /* Allow tracing of this process */  
    6.     if(ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {  
    7.         perror("ptrace");  
    8.         return;  
    9.     }  
    10.    
    11.     /* Replace this process's image with the given program */  
    12.     execl(programname, programname, 0);  
    13. }  

     

    这部分最有意思的地方在ptrace调用。ptrace的原型是(在sys/ptrace.h):

     
    1. long ptrace(enum__ptrace_request request,  pid_t pid, void*addr, void*data);  

    第一个参数是request,可以是预定义的以PTRACE_打头的常量值。第二个参数指定了进程id,第三以及第四个参数是地址和指向数据的指针,用来对内存做操作。上面代码段中的ptrace调用使用了PTRACE_TRACEME请求,这表示这个子进程要求操作系统内核允许它的父进程对其跟踪。这个请求在man手册中解释的非常清楚:

    “表明这个进程由它的父进程来跟踪。任何发给这个进程的信号(除了SIGKILL)将导致该进程停止运行,而它的父进程会通过wait()获得通知。另外,该进程之后所有对exec()的调用都将使操作系统产生一个SIGTRAP信号发送给它,这让父进程有机会在新程序开始执行之前获得对子进程的控制权。如果不希望由父进程来跟踪的话,那就不应该使用这个请求。(pid、addr、data被忽略)”

    我已经把这个例子中我们感兴趣的地方高亮显示了。注意,run_target在ptrace调用之后紧接着做的是通过execl来调用我们指定的程序。这里就会像我们高亮显示的部分所解释的那样,操作系统内核会在子进程开始执行execl中指定的程序之前停止该进程,并发送一个信号给父进程。

    因此,是时候看看父进程需要做些什么了:

     
    1. void run_debugger(pid_t child_pid)  
    2. {  
    3.     intwait_status;  
    4.     unsigned icounter = 0;  
    5.     procmsg("debugger started\n");  
    6.    
    7.     /* Wait for child to stop on its first instruction */  
    8.     wait(&wait_status);  
    9.    
    10.     while(WIFSTOPPED(wait_status)) {  
    11.         icounter++;  
    12.         /* Make the child execute another instruction */  
    13.         if(ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {  
    14.             perror("ptrace");  
    15.             return;  
    16.         }  
    17.    
    18.         /* Wait for child to stop on its next instruction */  
    19.         wait(&wait_status);  
    20.     }  
    21.    
    22.     procmsg("the child executed %u instructions\n", icounter);  
    23. }  

    通过上面的代码我们可以回顾一下,一旦子进程开始执行exec调用,它就会停止然后接收到一个SIGTRAP信号。父进程通过第一个wait调用正在等待这个事件发生。一旦子进程停止(如果子进程由于发送的信号而停止运行,WIFSTOPPED就返回true),父进程就去检查这个事件。

    父进程接下来要做的是本文中最有意思的地方。父进程通过PTRACE_SINGLESTEP以及子进程的id号来调用ptrace。这么做是告诉操作系统——请重新启动子进程,但当子进程执行了下一条指令后再将其停止。然后父进程再次等待子进程的停止,整个循环继续得以执行。当从wait中得到的不是关于子进程停止的信号时,循环结束。在正常运行这个跟踪程序时,会得到子进程正常退出(WIFEXITED会返回true)的信号。

    icounter会统计子进程执行的指令数量。因此我们这个简单的例子实际上还是做了点有用的事情——通过在命令行上指定一个程序名,我们的例子会执行这个指定的程序,然后统计出从开始到结束该程序执行过的CPU指令总数。让我们看看实际运行的情况。

     

    实际测试

    我编译了下面这个简单的程序,然后在我们的跟踪程序下执行:

     
    1. #include <stdio.h>   
    2. int main()  
    3. {  
    4.     printf(“Hello, world!\n”);  
    5.     return0;  
    6. }  

    令我惊讶的是,我们的跟踪程序运行了很长的时间然后报告显示一共有超过100000条指令得到了执行。仅仅只是一个简单的printf调用,为什么会这样?答案非常有意思。默认情况下,Linux中的gcc编译器会动态链接到C运行时库。这意味着任何程序在运行时首先要做的事情是加载动态库。这需要很多代码实现——记住,我们这个简单的跟踪程序会针对每一条被执行的指令计数,不仅仅是main函数,而是整个进程。

    因此,当我采用-static标志静态链接这个测试程序时(注意到可执行文件因此增加了500KB的大小,因为它静态链接了C运行时库),我们的跟踪程序报告显示只有7000条左右的指令被执行了。这还是非常多,但如果你了解到libc的初始化工作仍然先于main的执行,而清理工作会在main之后执行,那么这就完全说得通了。而且,printf也是一个复杂的函数。

    我们还是不满足于此,我希望能看到一些可检测的东西,例如我可以从整体上看到每一条需要被执行的指令是什么。这一点我们可以通过汇编代码来得到。因此我把这个“Hello,world”程序汇编(gcc -S)为如下的汇编码:

     

    1. section    .text  
    2.     ; The _start symbol must be declared forthe linker (ld)  
    3.     global _start  
    4.    
    5. _start:  
    6.    
    7.     ; Prepare arguments forthe sys_write systemcall:  
    8.     ;   - eax: systemcall number (sys_write)  
    9.     ;   - ebx: file descriptor (stdout)  
    10.     ;   - ecx: pointer to string  
    11.     ;   - edx: string length  
    12.     mov    edx, len  
    13.     mov    ecx, msg  
    14.     mov    ebx, 1  
    15.     mov    eax, 4  
    16.    
    17.     ; Execute the sys_write systemcall  
    18.     int   0x80  
    19.    
    20.     ; Execute sys_exit  
    21.     mov    eax, 1  
    22.     int   0x80  
    23.    
    24. section   .data  
    25. msg db    'Hello, world!', 0xa  
    26. len equ    $ - msg  

     

    这就足够了。现在跟踪程序会报告有7条指令得到了执行,我可以很容易地从汇编代码来验证这一点。

     

    深入指令流

    汇编码程序得以让我为大家介绍ptrace的另一个强大的功能——详细检查被跟踪进程的状态。下面是run_debugger函数的另一个版本:

    1. void run_debugger(pid_t child_pid)  
    2. {  
    3.     intwait_status;  
    4.     unsigned icounter = 0;  
    5.     procmsg("debugger started\n");  
    6.    
    7.     /* Wait for child to stop on its first instruction */  
    8.     wait(&wait_status);  
    9.    
    10.     while(WIFSTOPPED(wait_status)) {  
    11.         icounter++;  
    12.         structuser_regs_struct regs;  
    13.         ptrace(PTRACE_GETREGS, child_pid, 0, ®s);  
    14.         unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.eip, 0);  
    15.    
    16.         procmsg("icounter = %u.  EIP = 0x%08x.  instr = 0x%08x\n",  
    17.                     icounter, regs.eip, instr);  
    18.    
    19.         /* Make the child execute another instruction */  
    20.         if(ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {  
    21.             perror("ptrace");  
    22.             return;  
    23.         }  
    24.    
    25.         /* Wait for child to stop on its next instruction */  
    26.         wait(&wait_status);  
    27.     }  
    28.    
    29.     procmsg("the child executed %u instructions\n", icounter);  
    30. }  

    同前个版本相比,唯一的不同之处在于while循环的开始几行。这里有两个新的ptrace调用。第一个读取进程的寄存器值到一个结构体中。结构体user_regs_struct定义在sys/user.h中。这儿有个有趣的地方——如果你打开这个头文件看看,靠近文件顶端的地方有一条这样的注释:

    1
    /* 本文件的唯一目的是为GDB,且只为GDB所用。对于这个文件,不要看的太多。除了GDB以外不要用于任何其他目的,除非你知道你正在做什么。*/

    现在,我不知道你是怎么想的,但我感觉我们正处于正确的跑道上。无论如何,回到我们的例子上来。一旦我们将所有的寄存器值获取到regs中,我们就可以通过PTRACE_PEEKTEXT标志以及将regs.eip(x86架构上的扩展指令指针)做参数传入ptrace来调用。我们所得到的就是指令。让我们在汇编代码上运行这个新版的跟踪程序。

     

    1. $ simple_tracer traced_helloworld  
    2. [5700] debugger started  
    3. [5701] target started. will run 'traced_helloworld'  
    4. [5700] icounter = 1.  EIP = 0x08048080.  instr = 0x00000eba  
    5. [5700] icounter = 2.  EIP = 0x08048085.  instr = 0x0490a0b9  
    6. [5700] icounter = 3.  EIP = 0x0804808a.  instr = 0x000001bb  
    7. [5700] icounter = 4.  EIP = 0x0804808f.  instr = 0x000004b8  
    8. [5700] icounter = 5.  EIP = 0x08048094.  instr = 0x01b880cd  
    9. Hello, world!  
    10. [5700] icounter = 6.  EIP = 0x08048096.  instr = 0x000001b8  
    11. [5700] icounter = 7.  EIP = 0x0804809b.  instr = 0x000080cd  
    12. [5700] the child executed 7 instructions  

     

    OK,所以现在除了icounter以外,我们还能看到指令指针以及每一步的指令。如何验证这是否正确呢?可以通过在可执行文件上执行objdump –d来实现:

    用这份输出对比我们的跟踪程序输出,应该很容易观察到相同的地方。

     

    1. $ objdump -d traced_helloworld  
    2.    
    3. traced_helloworld:     file format elf32-i386  
    4.    
    5. Disassembly of section .text:  
    6.    
    7. 08048080 <.text>:  
    8.  8048080:     ba 0e 00 00 00          mov    $0xe,%edx  
    9.  8048085:     b9 a0 90 04 08          mov    $0x80490a0,%ecx  
    10.  804808a:     bb 01 00 00 00          mov    $0x1,%ebx  
    11.  804808f:     b8 04 00 00 00          mov    $0x4,%eax  
    12.  8048094:     cd 80                   int   $0x80  
    13.  8048096:     b8 01 00 00 00          mov    $0x1,%eax  
    14.  804809b:     cd 80                   int   $0x80  

     

    关联到运行中的进程上

    你已经知道了调试器也可以关联到已经处于运行状态的进程上。看到这里,你应该不会感到惊讶,这也是通过ptrace来实现的。这需要通过PTRACE_ATTACH请求。这里我不会给出一段样例代码,因为通过我们已经看到的代码,这应该很容易实现。基于教学的目的,这里采用的方法更为便捷(因为我们可以在子进程刚启动时立刻将它停止)。

     

    代码

    本文给出的这个简单的跟踪程序的完整代码(更高级一点,可以将具体指令打印出来)可以在这里找到。程序通过-Wall –pedantic –std=c99编译选项在4.4版的gcc上编译。

     

    结论及下一步要做的

    诚然,本文并没有涵盖太多的内容——我们离一个真正可用的调试器还差的很远。但是,我希望这篇文章至少已经揭开了调试过程的神秘面纱。ptrace是一个拥有许多功能的系统调用,目前我们只展示了其中少数几种功能。

    能够单步执行代码是很有用处的,但作用有限。以“Hello, world”为例,要到达main函数,需要先遍历好几千条初始化C运行时库的指令。这就不太方便了。我们所希望的理想方案是可以在main函数入口处设置一个断点,从断点处开始单步执行。下一篇文章中我将向您展示该如何实现断点机制。

    Linux下调试器工作原理之二—实现断点

    说明调试器中的断点机制是如何实现的。断点机制是调试器的两大主要支柱之一 ——另一个是在被调试进程的内存空间中查看变量的值。我们已经在第一篇文章中稍微涉及到了一些监视被调试进程的知识,但断点机制仍然还是个迷。

    软中断

    要在x86体系结构上实现断点我们要用到软中断(也称为“陷阱”trap)。在我们深入细节之前,我想先大致解释一下中断和陷阱的概念。

    CPU有一个单独的执行序列,会一条指令一条指令的顺序执行。要处理类似IO或者硬件时钟这样的异步事件时CPU就要用到中断。硬件中断通常是一个专门的电信号,连接到一个特殊的“响应电路”上。这个电路会感知中断的到来,然后会使CPU停止当前的执行流,保存当前的状态,然后跳转到一个预定义的地址处去执行,这个地址上会有一个中断处理例程。当中断处理例程完成它的工作后,CPU就从之前停止的地方恢复执行。

    软中断的原理类似,但实际上有一点不同。CPU支持特殊的指令允许通过软件来模拟一个中断。当执行到这个指令时,CPU将其当做一个中断——停止当前正常的执行流,保存状态然后跳转到一个处理例程中执行。这种“陷阱”让许多现代的操作系统得以有效完成很多复杂任务(任务调度、虚拟内存、内存保护、调试等)。

    一些编程错误(比如除0操作)也被CPU当做一个“陷阱”,通常被认为是“异常”。这里软中断同硬件中断之间的界限就变得模糊了,因为这里很难说这种异常到底是硬件中断还是软中断引起的。我有些偏离主题了,让我们回到关于断点的讨论上来。

    关于int 3指令

    看过前一节后,现在我可以简单地说断点就是通过CPU的特殊指令——int 3来实现的。int就是x86体系结构中的“陷阱指令”——对预定义的中断处理例程的调用。x86支持int指令带有一个8位的操作数,用来指定所发生的中断号。因此,理论上可以支持256种“陷阱”。前32个由CPU自己保留,这里第3号就是我们感兴趣的——称为“trap to debugger”。

    不多说了,我这里就引用“圣经”中的原话吧(这里的圣经就是Intel’s Architecture software developer’s manual, volume2A):

    “INT 3指令产生一个特殊的单字节操作码(CC),这是用来调用调试异常处理例程的。(这个单字节形式非常有价值,因为这样可以通过一个断点来替换掉任何指令的第一个字节,包括其它的单字节指令也是一样,而不会覆盖到其它的操作码)。”

    上面这段话非常重要,但现在解释它还是太早,我们稍后再来看。

    使用int 3指令

    是的,懂得事物背后的原理是很棒的,但是这到底意味着什么?我们该如何使用int 3来实现断点机制?套用常见的编程问答中出现的对话——请用代码说话!

    实际上这真的非常简单。一旦你的进程执行到int 3指令时,操作系统就将它暂停。在Linux上(本文关注的是Linux平台),这会给该进程发送一个SIGTRAP信号。

    这就是全部——真的!现在回顾一下本系列文章的第一篇,跟踪(调试器)进程可以获得所有其子进程(或者被关联到的进程)所得到信号的通知,现在你知道我们该做什么了吧?

    就是这样,再没有什么计算机体系结构方面的东东了,该写代码了。

    手动设定断点

    现在我要展示如何在程序中设定断点。用于这个示例的目标程序如下:

      

    1. section    .text  
    2.     ; The _start symbol must be declared forthe linker (ld)  
    3.     global _start  
    4.    
    5. _start:  
    6.    
    7.     ; Prepare arguments forthe sys_write systemcall:  
    8.     ;   - eax: systemcall number (sys_write)  
    9.     ;   - ebx: file descriptor (stdout)  
    10.     ;   - ecx: pointer to string  
    11.     ;   - edx: string length  
    12.     mov     edx, len1  
    13.     mov     ecx, msg1  
    14.     mov     ebx, 1  
    15.     mov     eax, 4  
    16.    
    17.     ; Execute the sys_write systemcall  
    18.     int    0x80  
    19.    
    20.     ; Now print the other message  
    21.     mov     edx, len2  
    22.     mov     ecx, msg2  
    23.     mov     ebx, 1  
    24.     mov     eax, 4  
    25.     int    0x80  
    26.    
    27.     ; Execute sys_exit  
    28.     mov     eax, 1  
    29.     int    0x80  
    30.    
    31. section    .data  
    32.    
    33. msg1    db      'Hello,', 0xa  
    34. len1    equ     $ - msg1  
    35. msg2    db      'world!', 0xa  
    36. len2    equ     $ - msg2  

     

    我现在使用的是汇编语言,这是为了避免当使用C语言时涉及到的编译和符号的问题。上面列出的程序功能就是在一行中打印“Hello,”,然后在下一行中打印“world!”。这个例子与上一篇文章中用到的例子很相似。

    我希望设定的断点位置应该在第一条打印之后,但恰好在第二条打印之前。我们就让断点打在第一个int 0×80指令之后吧,也就是mov edx, len2。首先,我需要知道这条指令对应的地址是什么。运行objdump –d:

     

    1. traced_printer2:     file format elf32-i386  
    2. Sections:  
    3. Idx Name          Size      VMA       LMA       File off  Algn  
    4.   0 .text         00000033  08048080  08048080  00000080  2**4  
    5.                   CONTENTS, ALLOC, LOAD, READONLY, CODE  
    6.   1 .data         0000000e  080490b4  080490b4  000000b4  2**2  
    7.                   CONTENTS, ALLOC, LOAD, DATA  
    8. Disassembly of section .text:  
    9. 08048080 <.text>:  
    10.  8048080:     ba 07 00 00 00          mov    $0x7,%edx  
    11.  8048085:     b9 b4 90 04 08          mov    $0x80490b4,%ecx  
    12.  804808a:     bb 01 00 00 00          mov    $0x1,%ebx  
    13.  804808f:     b8 04 00 00 00          mov    $0x4,%eax  
    14.  8048094:     cd 80                   int   $0x80  
    15.  8048096:     ba 07 00 00 00          mov    $0x7,%edx  
    16.  804809b:     b9 bb 90 04 08          mov    $0x80490bb,%ecx  
    17.  80480a0:     bb 01 00 00 00          mov    $0x1,%ebx  
    18.  80480a5:     b8 04 00 00 00          mov    $0x4,%eax  
    19.  80480aa:     cd 80                   int   $0x80  
    20.  80480ac:     b8 01 00 00 00          mov    $0x1,%eax  
    21.  80480b1:     cd 80                   int   $0x80  

     

    通过上面的输出,我们知道要设定的断点地址是0×8048096。等等,真正的调试器不是像这样工作的,对吧?真正的调试器可以根据代码行数或者函数名称来设定断点,而不是基于什么内存地址吧?非常正确。但是我们离那个标准还差的远——如果要像真正的调试器那样设定断点,我们还需要涵盖符号表以及调试信息方面的知识,这需要用另一篇文章来说明。至于现在,我们还必须得通过内存地址来设定断点。

    看到这里我真的很想再扯一点题外话,所以你有两个选择。如果你真的对于为什么地址是0×8048096,以及这代表什么意思非常感兴趣的话,接着看下一节。如果你对此毫无兴趣,只是想看看怎么设定断点,可以略过这一部分。

    题外话——进程地址空间以及入口点

    坦白的说,0×8048096本身并没有太大意义,这只不过是相对可执行镜像的代码段(text section)开始处的一个偏移量。如果你仔细看看前面objdump出来的结果,你会发现代码段的起始位置是0×08048080。这告诉了操作系统要将代码段映射到进程虚拟地址空间的这个位置上。在Linux上,这些地址可以是绝对地址(比如,有的可执行镜像加载到内存中时是不可重定位的),因为在虚拟内存系统中,每个进程都有自己独立的内存空间,并把整个32位的地址空间都看做是属于自己的(称为线性地址)。

    如果我们通过readelf工具来检查可执行文件的ELF头,我们将得到如下输出:

     
    1. $ readelf -h traced_printer2  
    2. ELF Header:  
    3.   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00  
    4.   Class:                                 ELF32  
    5.   Data:                                  2's complement, little endian  
    6.   Version:                               1 (current)  
    7.   OS/ABI:                                UNIX - System V  
    8.   ABI Version:                           0  
    9.   Type:                                  EXEC (Executable file)  
    10.   Machine:                               Intel 80386  
    11.   Version:                               0x1  
    12.   Entry point address:                   0x8048080  
    13.   Start of program headers:              52 (bytes into file)  
    14.   Start of section headers:              220 (bytes into file)  
    15.   Flags:                                 0x0  
    16.   Size of this header:                   52 (bytes)  
    17.   Size of program headers:               32 (bytes)  
    18.   Number of program headers:             2  
    19.   Size of section headers:               40 (bytes)  
    20.   Number of section headers:             4  
    21.   Section header string table index:     3  

    注意,ELF头的“entry point address”同样指向的是0×8048080。因此,如果我们把ELF文件中的这个部分解释给操作系统的话,就表示:

    1.  将代码段映射到地址0×8048080处

    2.  从入口点处开始执行——地址0×8048080

    但是,为什么是0×8048080呢?它的出现是由于历史原因引起的。每个进程的地址空间的前128MB被保留给栈空间了(注:这一部分原因可参考Linkers and Loaders)。128MB刚好是0×80000000,可执行镜像中的其他段可以从这里开始。0×8048080是Linux下的链接器ld所使用的默认入口点。这个入口点可以通过传递参数-Ttext给ld来进行修改。

    因此,得到的结论是这个地址并没有什么特别的,我们可以自由地修改它。只要ELF可执行文件的结构正确且在ELF头中的入口点地址同程序代码段(text section)的实际起始地址相吻合就OK了。

    通过int 3指令在调试器中设定断点

    要在被调试进程中的某个目标地址上设定一个断点,调试器需要做下面两件事情:

    1.  保存目标地址上的数据

    2.  将目标地址上的第一个字节替换为int 3指令

    然后,当调试器向操作系统请求开始运行进程时(通过前一篇文章中提到的PTRACE_CONT),进程最终一定会碰到int 3指令。此时进程停止,操作系统将发送一个信号。这时就是调试器再次出马的时候了,接收到一个其子进程(或被跟踪进程)停止的信号,然后调试器要做下面几件事:

    1.  在目标地址上用原来的指令替换掉int 3

    2.  将被跟踪进程中的指令指针向后递减1。这么做是必须的,因为现在指令指针指向的是已经执行过的int 3之后的下一条指令。

    3.  由于进程此时仍然是停止的,用户可以同被调试进程进行某种形式的交互。这里调试器可以让你查看变量的值,检查调用栈等等。

    4.  当用户希望进程继续运行时,调试器负责将断点再次加到目标地址上(由于在第一步中断点已经被移除了),除非用户希望取消断点。

    让我们看看这些步骤如何转化为实际的代码。我们将沿用第一篇文章中展示过的调试器“模版”(fork一个子进程,然后对其跟踪)。无论如何,本文结尾处会给出完整源码的链接。

     
    1. /* Obtain and show child's instruction pointer */  
    2. ptrace(PTRACE_GETREGS, child_pid, 0, ®s);  
    3. procmsg("Child started. EIP = 0x%08x\n", regs.eip);  
    4.    
    5. /* Look at the word at the address we're interested in */  
    6. unsigned addr = 0x8048096;  
    7. unsigned data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);  
    8. procmsg("Original data at 0x%08x: 0x%08x\n", addr, data);  
    这里调试器从被跟踪进程中获取到指令指针,然后检查当前位于地址0×8048096处的字长内容。运行本文前面列出的汇编码程序,将打印出:
    1. [13028] Child started. EIP = 0x08048080  
    2. [13028] Original data at 0x08048096: 0x000007ba  

    目前为止一切顺利,下一步:

    1. /* Write the trap instruction 'int 3' into the address */  
    2. unsigned data_with_trap = (data & 0xFFFFFF00) | 0xCC;  
    3. ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data_with_trap);  
    4.    
    5. /* See what's there again... */  
    6. unsigned readback_data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);  
    7. procmsg("After trap, data at 0x%08x: 0x%08x\n", addr, readback_data);  

    注意看我们是如何将int 3指令插入到目标地址上的。这部分代码将打印出:

    1. [13028] After trap, data at 0x08048096: 0x000007cc  

    再一次如同预计的那样——0xba被0xcc取代了。调试器现在运行子进程然后等待子进程在断点处停止住。

    1. /* Let the child run to the breakpoint and wait for it to 
    2. ** reach it 
    3. */  
    4. ptrace(PTRACE_CONT, child_pid, 0, 0);  
    5. wait(&wait_status);  
    6. if(WIFSTOPPED(wait_status)) {  
    7.     procmsg("Child got a signal: %s\n", strsignal(WSTOPSIG(wait_status)));  
    8. else {  
    9.     perror("wait");  
    10.     return;  
    11. }  
    12. /* See where the child is now */  
    13. ptrace(PTRACE_GETREGS, child_pid, 0, ®s);  
    14. procmsg("Child stopped at EIP = 0x%08x\n", regs.eip);  

    这段代码打印出:

    1. Hello,  
    2. [13028] Child got a signal: Trace/breakpoint trap  
    3. [13028] Child stopped at EIP = 0x08048097  

    注意,“Hello,”在断点之前打印出来了——同我们计划的一样。同时我们发现子进程已经停止运行了——就在这个单字节的陷阱指令执行之后。

    1. /* Remove the breakpoint by restoring the previous data 
    2. ** at the target address, and unwind the EIP back by 1 to 
    3. ** let the CPU execute the original instruction that was 
    4. ** there. 
    5. */  
    6. ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data);  
    7. regs.eip -= 1;  
    8. ptrace(PTRACE_SETREGS, child_pid, 0, ®s);  
    9.    
    10. /* The child can continue running now */  
    11. ptrace(PTRACE_CONT, child_pid, 0, 0);  

    这会使子进程打印出“world!”然后退出,同之前计划的一样。

    注意,我们这里并没有重新加载断点。这可以在单步模式下执行,然后将陷阱指令加回去,再做PTRACE_CONT就可以了。本文稍后介绍的debug库实现了这个功能。

    更多关于int 3指令

    现在是回过头来说说int 3指令的好机会,以及解释一下Intel手册中对这条指令的奇怪说明。

    “这个单字节形式非常有价值,因为这样可以通过一个断点来替换掉任何指令的第一个字节,包括其它的单字节指令也是一样,而不会覆盖到其它的操作码。”

    x86架构上的int指令占用2个字节——0xcd加上中断号。int 3的二进制形式可以被编码为cd 03,但这里有一个特殊的单字节指令0xcc以同样的作用而被保留。为什么要这样做呢?因为这允许我们在插入一个断点时覆盖到的指令不会多于一条。这很重要,考虑下面的示例代码:

    1. .. some code ..  
    2.     jz    foo  
    3.     dec   eax  
    4. foo:  
    5.     call  bar  
    6.     .. some code ..  

    假设我们要在dec eax上设定断点。这恰好是条单字节指令(操作码是0×48)。如果替换为断点的指令长度超过1字节,我们就被迫改写了接下来的下一条指令(call),这可能会产生一些完全非法的行为。考虑一下条件分支jz foo,这时进程可能不会在dec eax处停止下来(我们在此设定的断点,改写了原来的指令),而是直接执行了后面的非法指令。

    通过对int 3指令采用一个特殊的单字节编码就能解决这个问题。因为x86架构上指令最短的长度就是1字节,这样我们可以保证只有我们希望停止的那条指令被修改。

    封装细节

    前面几节中的示例代码展示了许多底层的细节,这些可以很容易地通过API进行封装。我已经做了一些封装,使其成为一个小型的调试库——debuglib。代码在本文末尾处可以下载。这里我只想介绍下它的用法,我们要开始调试C程序了。

    跟踪C程序

    目前为止为了简单起见我把重点放在对汇编程序的跟踪上了。现在升一级来看看我们该如何跟踪一个C程序。

    其实事情并没有很大的不同——只是现在有点难以找到放置断点的位置。考虑如下这个简单的C程序:

    1. #include <stdio.h>   
    2.    
    3. void do_stuff()  
    4. {  
    5.     printf("Hello, ");  
    6. }  
    7.    
    8. int main()  
    9. {  
    10.     for(inti = 0; i < 4; ++i)  
    11.         do_stuff();  
    12.     printf("world!\n");  
    13.     return0;  
    14. }  

    假设我想在do_stuff的入口处设置一个断点。我将请出我们的老朋友objdump来反汇编可执行文件,但得到的输出太多。其实,查看text段不太管用,因为这里面包含了大量的初始化C运行时库的代码,我目前对此并不感兴趣。所以,我们只需要在dump出来的结果里看do_stuff部分就好了。

    1. 080483e4 <do_stuff>:  
    2. 80483e4:     55                      push   %ebp  
    3. 80483e5:     89 e5                   mov    %esp,%ebp  
    4. 80483e7:     83 ec 18                sub    $0x18,%esp  
    5. 80483ea:     c7 04 24 f0 84 04 08    movl   $0x80484f0,(%esp)  
    6. 80483f1:     e8 22 ff ff ff          call   8048318 <puts@plt>  
    7. 80483f6:     c9                      leave  
    8. 80483f7:     c3                      ret  

    好的,所以我们应该把断点设定在0x080483e4上,这是do_stuff的第一条指令。另外,由于这个函数是在循环体中调用的,我们希望在循环全部结束前保留断点,让程序可以在每一轮循环中都在断点处停下。我将使用debuglib来简化代码编写。这里是完整的调试器函数:

    1. void run_debugger(pid_t child_pid)  
    2. {  
    3.     procmsg("debugger started\n");  
    4.    
    5.     /* Wait for child to stop on its first instruction */  
    6.     wait(0);  
    7.     procmsg("child now at EIP = 0x%08x\n", get_child_eip(child_pid));  
    8.    
    9.     /* Create breakpoint and run to it*/  
    10.     debug_breakpoint* bp = create_breakpoint(child_pid, (void*)0x080483e4);  
    11.     procmsg("breakpoint created\n");  
    12.     ptrace(PTRACE_CONT, child_pid, 0, 0);  
    13.     wait(0);  
    14.    
    15.     /* Loop as long as the child didn't exit */  
    16.     while(1) {  
    17.         /* The child is stopped at a breakpoint here. Resume its 
    18.         ** execution until it either exits or hits the 
    19.         ** breakpoint again. 
    20.         */  
    21.         procmsg("child stopped at breakpoint. EIP = 0x%08X\n", get_child_eip(child_pid));  
    22.         procmsg("resuming\n");  
    23.         intrc = resume_from_breakpoint(child_pid, bp);  
    24.         if(rc == 0) {  
    25.             procmsg("child exited\n");  
    26.             break;  
    27.         } elseif (rc == 1) {  
    28.             continue;  
    29.         } else {  
    30.             procmsg("unexpected: %d\n", rc);  
    31.             break;  
    32.         }  
    33.     }  
    34.     cleanup_breakpoint(bp);  
    35. }  

    我们不用手动修改EIP指针以及目标进程的内存空间,我们只需要通过create_breakpoint, resume_from_breakpoint以及cleanup_breakpoint来操作就可以了。我们来看看当跟踪这个简单的C程序后的打印输出:

    1. $ bp_use_lib traced_c_loop  
    2. [13363] debugger started  
    3. [13364] target started. will run 'traced_c_loop'  
    4. [13363] child now at EIP = 0x00a37850  
    5. [13363] breakpoint created  
    6. [13363] child stopped at breakpoint. EIP = 0x080483E5  
    7. [13363] resuming  
    8. Hello,  
    9. [13363] child stopped at breakpoint. EIP = 0x080483E5  
    10. [13363] resuming  
    11. Hello,  
    12. [13363] child stopped at breakpoint. EIP = 0x080483E5  
    13. [13363] resuming  
    14. Hello,  
    15. [13363] child stopped at breakpoint. EIP = 0x080483E5  
    16. [13363] resuming  
    17. Hello,  
    18. world!  
    19. [13363] child exited  

    跟预计的情况一模一样!

    代码

    这里是完整的源码。在文件夹中你会发现:

    debuglib.h以及debuglib.c——封装了调试器的一些内部工作。

    bp_manual.c —— 本文一开始介绍的“手动”式设定断点。用到了debuglib库中的一些样板代码。

    bp_use_lib.c—— 大部分代码用到了debuglib,这就是本文中用于说明跟踪一个C程序中的循环的示例代码。

    结论及下一步要做的

    我们已经涵盖了如何在调试器中实现断点机制。尽管实现细节根据操作系统的不同而有所区别,但只要你使用的是x86架构的处理器,那么一切变化都基于相同的主题——在我们希望停止的指令上将其替换为int 3。

    Linux下调试器工作原理之三—调试信息

    将向大家解释关于调试器是如何在机器码中寻找C函数以及变量的,以及调试器使用了何种数据能够在C源代码的行号和机器码中来回映射。

    调试信息

    现代的编译器在转换高级语言程序代码上做得十分出色,能够将源代码中漂亮的缩进、嵌套的控制结构以及任意类型的变量全都转化为一长串的比特流——这就是机器码。这么做的唯一目的就是希望程序能在目标CPU上尽可能快的运行。大多数的C代码都被转化为一些机器码指令。变量散落在各处——在栈空间里、在寄存器里,甚至完全被编译器优化掉。结构体和对象甚至在生成的目标代码中根本不存在——它们只不过是对内存缓冲区中偏移量的抽象化表示。

    那么当你在某些函数的入口处设置断点时,调试器如何知道该在哪里停止目标进程的运行呢?当你希望查看一个变量的值时,调试器又是如何找到它并展示给你呢?答案就是——调试信息。

    调试信息是在编译器生成机器码的时候一起产生的。它代表着可执行程序和源代码之间的关系。这个信息以预定义的格式进行编码,并同机器码一起存储。许多年以来,针对不同的平台和可执行文件,人们发明了许多这样的编码格式。由于本文的主要目的不是介绍这些格式的历史渊源,而是为您展示它们的工作原理,所以我们只介绍一种最重要的格式,这就是DWARF。作为Linux以及其他类Unix平台上的ELF可执行文件的调试信息格式,如今的DWARF可以说是无处不在。

    ELF文件中的DWARF格式

    根据维基百科上的词条解释,DWARF是同ELF可执行文件格式一同设计出来的,尽管在理论上DWARF也能够嵌入到其它的对象文件格式中。

    DWARF是一种复杂的格式,在多种体系结构和操作系统上经过多年的探索之后,人们才在之前的格式基础上创建了DWARF。它肯定是很复杂的,因为它解决了一个非常棘手的问题——为任意类型的高级语言和调试器之间提供调试信息,支持任意一种平台和应用程序二进制接口(ABI)。要完全解释清楚这个主题,本文就显得太微不足道了。说实话,我也不理解其中的所有角落。本文我将采取更加实践的方法,只介绍足量的DWARF相关知识,能够阐明实际工作中调试信息是如何发挥其作用的就可以了。

    ELF文件中的调试段

    首先,让我们看看DWARF格式信息处在ELF文件中的什么位置上。ELF可以为每个目标文件定义任意多个段(section)。而Section header表中则定义了实际存在有哪些段,以及它们的名称。不同的工具以各自特殊的方式来处理这些不同的段,比如链接器只寻找它关注的段信息,而调试器则只关注其他的段。

    我们通过下面的C代码构建一个名为traceprog2的可执行文件来做下实验。

         #include <stdio.h>   
    1.    
    2. void do_stuff(intmy_arg)  
    3. {  
    4.     intmy_local = my_arg + 2;  
    5.     inti;  
    6.    
    7.     for(i = 0; i < my_local; ++i)  
    8.         printf("i = %d\n", i);  
    9. }  
    10.    
    11. intmain()  
    12. {  
    13.     do_stuff(2);  
    14.     return0;  
    15. }  

    通过objdump –h导出ELF可执行文件中的段头信息,我们注意到其中有几个段的名字是以.debug_打头的,这些就是DWARF格式的调试段:

        26 .debug_aranges 00000020  00000000  00000000  00001037  
    1.                  CONTENTS, READONLY, DEBUGGING  
    2. 27 .debug_pubnames 00000028  00000000  00000000  00001057  
    3.                  CONTENTS, READONLY, DEBUGGING  
    4. 28 .debug_info   000000cc  00000000  00000000  0000107f  
    5.                  CONTENTS, READONLY, DEBUGGING  
    6. 29 .debug_abbrev 0000008a  00000000  00000000  0000114b  
    7.                  CONTENTS, READONLY, DEBUGGING  
    8. 30 .debug_line   0000006b  00000000  00000000  000011d5  
    9.                  CONTENTS, READONLY, DEBUGGING  
    10. 31 .debug_frame  00000044  00000000  00000000  00001240  
    11.                  CONTENTS, READONLY, DEBUGGING  
    12. 32 .debug_str    000000ae  00000000  00000000  00001284  
    13.                  CONTENTS, READONLY, DEBUGGING  
    14. 33 .debug_loc    00000058  00000000  00000000  00001332  
    15.                  CONTENTS, READONLY, DEBUGGING  

    每行的第一个数字表示每个段的大小,而最后一个数字表示距离ELF文件开始处的偏移量。调试器就是利用这个信息来从可执行文件中读取相关的段信息。现在,让我们通过一些实际的例子来看看如何在DWARF中找寻有用的调试信息。

    定位函数

    当我们在调试程序时,一个最为基本的操作就是在某些函数中设置断点,期望调试器能在函数入口处将程序断下。要完成这个功能,调试器必须具有某种能够从源代码中的函数名称到机器码中该函数的起始指令间相映射的能力。

    这个信息可以通过从DWARF中的.debug_info段获取到。在我们继续之前,先说点背景知识。DWARF的基本描述实体被称为调试信息表项(Debugging Information Entry —— DIE),每个DIE有一个标签——包含它的类型,以及一组属性。各个DIE之间通过兄弟和孩子结点互相链接,属性值可以指向其他的DIE。

    我们运行

    1
    objdump –dwarf=info traceprog2

    得到的输出非常长,对于这个例子,我们只用关注这几行就可以了:

    1. <1><71>: Abbrev Number: 5 (DW_TAG_subprogram)  
    2.     <72>   DW_AT_external    : 1  
    3.     <73>   DW_AT_name        : (...): do_stuff  
    4.     <77>   DW_AT_decl_file   : 1  
    5.     <78>   DW_AT_decl_line   : 4  
    6.     <79>   DW_AT_prototyped  : 1  
    7.     <7a>   DW_AT_low_pc      : 0x8048604  
    8.     <7e>   DW_AT_high_pc     : 0x804863e  
    9.     <82>   DW_AT_frame_base  : 0x0      (location list)  
    10.     <86>   DW_AT_sibling     : <0xb3>  
    11.    
    12. <1><b3>: Abbrev Number: 9 (DW_TAG_subprogram)  
    13.     <b4>   DW_AT_external    : 1  
    14.     <b5>   DW_AT_name        : (...): main  
    15.     <b9>   DW_AT_decl_file   : 1  
    16.     <ba>   DW_AT_decl_line   : 14  
    17.     <bb>   DW_AT_type        : <0x4b>  
    18.     <bf>   DW_AT_low_pc      : 0x804863e  
    19.     <c3>   DW_AT_high_pc     : 0x804865a  
    20. <c7>   DW_AT_frame_base  : 0x2c     (location list)  

    这里有两个被标记为DW_TAG_subprogram的DIE,从DWARF的角度看这就是函数。注意,这里do_stuff和main都各有一个表项。这里有许多有趣的属性,但我们感兴趣的是DW_AT_low_pc。这就是函数起始处的程序计数器的值(x86下的EIP)。注意,对于do_stuff来说,这个值是0×8048604。现在让我们看看,通过objdump –d做反汇编后这个地址是什么:

    1. 08048604 <do_stuff>:  
    2.  8048604:       55           push   ebp  
    3.  8048605:       89 e5        mov    ebp,esp  
    4.  8048607:       83 ec 28     sub    esp,0x28  
    5.  804860a:       8b 45 08     mov    eax,DWORDPTR [ebp+0x8]  
    6.  804860d:       83 c0 02     add    eax,0x2  
    7.  8048610:       89 45 f4     mov    DWORDPTR [ebp-0xc],eax  
    8.  8048613:       c7 45 (...)  mov    DWORDPTR [ebp-0x10],0x0  
    9.  804861a:       eb 18        jmp    8048634 <do_stuff+0x30>  
    10.  804861c:       b8 20 (...)  mov    eax,0x8048720  
    11.  8048621:       8b 55 f0     mov    edx,DWORDPTR [ebp-0x10]  
    12.  8048624:       89 54 24 04  mov    DWORDPTR [esp+0x4],edx  
    13.  8048628:       89 04 24     mov    DWORDPTR [esp],eax  
    14.  804862b:       e8 04 (...)  call   8048534 <printf@plt>  
    15.  8048630:       83 45 f0 01  add    DWORDPTR [ebp-0x10],0x1  
    16.  8048634:       8b 45 f0     mov    eax,DWORDPTR [ebp-0x10]  
    17.  8048637:       3b 45 f4     cmp    eax,DWORDPTR [ebp-0xc]  
    18.  804863a:       7c e0        jl     804861c <do_stuff+0x18>  
    19.  804863c:       c9           leave  
    20.  804863d:       c3           ret  

    没错,从反汇编结果来看0×8048604确实就是函数do_stuff的起始地址。因此,这里调试器就同函数和它们在可执行文件中的位置确立了映射关系。

    定位变量

    假设我们确实在do_stuff中的断点处停了下来。我们希望调试器能够告诉我们my_local变量的值,调试器怎么知道去哪里找到相关的信息呢?这可比定位函数要难多了,因为变量可以在全局数据区,可以在栈上,甚至是在寄存器中。另外,具有相同名称的变量在不同的词法作用域中可能有不同的值。调试信息必须能够反映出所有这些变化,而DWARF确实能做到这些。

    我不会涵盖所有的可能情况,作为例子,我将只展示调试器如何在do_stuff函数中定位到变量my_local。我们从.debug_info段开始,再次看看do_stuff这一项,这一次我们也看看其他的子项:

    1. <1><71>: Abbrev Number: 5 (DW_TAG_subprogram)  
    2.     <72>   DW_AT_external    : 1  
    3.     <73>   DW_AT_name        : (...): do_stuff  
    4.     <77>   DW_AT_decl_file   : 1  
    5.     <78>   DW_AT_decl_line   : 4  
    6.     <79>   DW_AT_prototyped  : 1  
    7.     <7a>   DW_AT_low_pc      : 0x8048604  
    8.     <7e>   DW_AT_high_pc     : 0x804863e  
    9.     <82>   DW_AT_frame_base  : 0x0      (location list)  
    10.     <86>   DW_AT_sibling     : <0xb3>  
    11.  <2><8a>: Abbrev Number: 6 (DW_TAG_formal_parameter)  
    12.     <8b>   DW_AT_name        : (...): my_arg  
    13.     <8f>   DW_AT_decl_file   : 1  
    14.     <90>   DW_AT_decl_line   : 4  
    15.     <91>   DW_AT_type        : <0x4b>  
    16.     <95>   DW_AT_location    : (...)       (DW_OP_fbreg: 0)  
    17.  <2><98>: Abbrev Number: 7 (DW_TAG_variable)  
    18.     <99>   DW_AT_name        : (...): my_local  
    19.     <9d>   DW_AT_decl_file   : 1  
    20.     <9e>   DW_AT_decl_line   : 6  
    21.     <9f>   DW_AT_type        : <0x4b>  
    22.     <a3>   DW_AT_location    : (...)      (DW_OP_fbreg: -20)  
    23. <2><a6>: Abbrev Number: 8 (DW_TAG_variable)  
    24.     <a7>   DW_AT_name        : i  
    25.     <a9>   DW_AT_decl_file   : 1  
    26.     <aa>   DW_AT_decl_line   : 7  
    27.     <ab>   DW_AT_type        : <0x4b>  
    28. <af>   DW_AT_location    : (...)      (DW_OP_fbreg: -24)  

    注意每一个表项中第一个尖括号里的数字,这表示嵌套层次——在这个例子中带有<2>的表项都是表项<1>的子项。因此我们知道变量my_local(以DW_TAG_variable作为标签)是函数do_stuff的一个子项。调试器同样还对变量的类型感兴趣,这样才能正确的显示变量的值。这里my_local的类型根据DW_AT_type标签可知为<0x4b>。如果查看objdump的输出,我们会发现这是一个有符号4字节整数。

    要在执行进程的内存映像中实际定位到变量,调试器需要检查DW_AT_location属性。对于my_local来说,这个属性为DW_OP_fberg: -20。这表示变量存储在从所包含它的函数的DW_AT_frame_base属性开始偏移-20处,而DW_AT_frame_base正代表了该函数的栈帧起始点。

    函数do_stuff的DW_AT_frame_base属性的值是0×0(location list),这表示该值必须要在location list段去查询。我们看看objdump的输出:

    1. $ objdump --dwarf=loc tracedprog2  
    2.    
    3. tracedprog2:    fileformatelf32-i386  
    4.    
    5. Contents of the .debug_loc section:  
    6.    
    7.     Offset   Begin    End      Expression  
    8.     00000000 08048604 08048605 (DW_OP_breg4: 4 )  
    9.     00000000 08048605 08048607 (DW_OP_breg4: 8 )  
    10.     00000000 08048607 0804863e (DW_OP_breg5: 8 )  
    11.     00000000 <End of list>  
    12.     0000002c 0804863e 0804863f (DW_OP_breg4: 4 )  
    13.     0000002c 0804863f 08048641 (DW_OP_breg4: 8 )  
    14.     0000002c 08048641 0804865a (DW_OP_breg5: 8 )  
    15. 0000002c <End of list>  

    关于位置信息,我们这里感兴趣的就是第一个。对于调试器可能定位到的每一个地址,它都会指定当前栈帧到变量间的偏移量,而这个偏移就是通过寄存器来计算的。对于x86体系结构,bpreg4代表esp寄存器,而bpreg5代表ebp寄存器。

    让我们再看看do_stuff的开头几条指令:

    1. 08048604 <do_stuff>:  
    2.  8048604:       55          push   ebp  
    3.  8048605:       89 e5       mov    ebp,esp  
    4.  8048607:       83 ec 28    sub    esp,0x28  
    5.  804860a:       8b 45 08    mov    eax,DWORDPTR [ebp+0x8]  
    6.  804860d:       83 c0 02    add    eax,0x2  
    7.  8048610:       89 45 f4    mov    DWORDPTR [ebp-0xc],eax  

    注意,ebp只有在第二条指令执行后才与我们建立起关联,对于前两个地址,基地址由前面列出的位置信息中的esp计算得出。一旦得到了ebp的有效值,就可以很方便的计算出与它之间的偏移量。因为之后ebp保持不变,而esp会随着数据压栈和出栈不断移动。

    那么这到底为我们定位变量my_local留下了什么线索?我们感兴趣的只是在地址0×8048610上的指令执行过后my_local的值(这里my_local的值会通过eax寄存器计算,而后放入内存)。因此调试器需要用到DW_OP_breg5: 8 基址来定位。现在回顾一下my_local的DW_AT_location属性:DW_OP_fbreg: -20。做下算数:从基址开始偏移-20,那就是ebp – 20,再偏移+8,我们得到ebp – 12。现在再看看反汇编输出,注意到数据确实是从eax寄存器中得到的,而ebp – 12就是my_local存储的位置。

    定位到行号

    当我说到在调试信息中寻找函数时,我撒了个小小的谎。当我们调试C源代码并在函数中放置了一个断点时,我们通常并不会对第一条机器码指令感兴趣。我们真正感兴趣的是函数中的第一行C代码。

    这就是为什么DWARF在可执行文件中对C源码到机器码地址做了全部映射。这部分信息包含在.debug_line段中,可以按照可读的形式进行解读:

     

     objdump --dwarf=decodedline tracedprog2  
    1.    
    2. tracedprog2:    fileformatelf32-i386  
    3.    
    4. Decoded dump of debug contents of section .debug_line:  
    5.    
    6. CU:/home/eliben/eli/eliben-code/debugger/tracedprog2.c:  
    7. File name           Line number    Starting address  
    8. tracedprog2.c                5           0x8048604  
    9. tracedprog2.c                6           0x804860a  
    10. tracedprog2.c                9           0x8048613  
    11. tracedprog2.c               10           0x804861c  
    12. tracedprog2.c                9           0x8048630  
    13. tracedprog2.c               11           0x804863c  
    14. tracedprog2.c               15           0x804863e  
    15. tracedprog2.c               16           0x8048647  
    16. tracedprog2.c               17           0x8048653  
    17. tracedprog2.c               18           0x8048658  

     

    不难看出C源码同反汇编输出之间的关系。第5行源码指向函数do_stuff的入口点——地址0×8040604。接下第6行源码,当在do_stuff上设置断点时,这里就是调试器实际应该停下的地方,它指向地址0x804860a——刚过do_stuff的开场白。这个行信息能够方便的在C源码的行号同指令地址间建立双向的映射关系。

    1.  当在某一行上设定断点时,调试器将利用行信息找到实际应该陷入的地址(还记得前一篇中的int 3指令吗?)

    2.  当某个指令引起段错误时,调试器会利用行信息反过来找出源代码中的行号,并告诉用户。

    libdwarf —— 在程序中访问DWARF

    通过命令行工具来访问DWARF信息这虽然有用但还不能完全令我们满意。作为程序员,我们希望知道应该如何写出实际的代码来解析DWARF格式并从中读取我们需要的信息。

    自然的,一种方法就是拿起DWARF规范开始钻研。还记得每个人都告诉你永远不要自己手动解析HTML,而应该使用函数库来做吗?没错,如果你要手动解析DWARF的话情况会更糟糕,DWARF比HTML要复杂的多。本文展示的只是冰山一角而已。更困难的是,在实际的目标文件中,这些信息大部分都以非常紧凑和压缩的方式进行编码处理。

    因此我们要走另一条路,使用一个函数库来同DWARF打交道。我知道的这类函数库主要有两个:

    1.    BFD(libbfd),GNU binutils就是使用的它,包括本文中多次使用到的工具objdump,ld(GNU链接器),以及as(GNU汇编器)。

    2.    libdwarf —— 同它的老大哥libelf一样,为Solaris以及FreeBSD系统上的工具服务。

    我这里选择了libdwarf,因为对我来说它看起来没那么神秘,而且license更加自由(LGPL,BFD是GPL)。

    由于libdwarf自身非常复杂,需要很多代码来操作。我这里不打算把所有代码贴出来,但你可以下载,然后自己编译运行。要编译这个文件,你需要安装libelf以及libdwarf,并在编译时为链接器提供-lelf以及-ldwarf标志。

    这个演示程序接收一个可执行文件,并打印出程序中的函数名称同函数入口点地址。下面是本文用以演示的C程序产生的输出:

    1. $ dwarf_get_func_addr tracedprog2  
    2. DW_TAG_subprogram:'do_stuff'  
    3. low pc  : 0x08048604  
    4. high pc : 0x0804863e  
    5. DW_TAG_subprogram:'main'  
    6. low pc  : 0x0804863e  
    7. high pc : 0x0804865a  

    libdwarf的文档非常好(见本文的参考文献部分),花点时间看看,对于本文中提到的DWARF段信息你处理起来就应该没什么问题了。

    结论及下一步

    调试信息只是一个简单的概念,具体实现细节可能相当复杂。但最终我们知道了调试器是如何从可执行文件中找出同源代码之间的关系。有了调试信息在手,调试器为用户所能识别的源代码和数据结构同可执行文件之间架起了一座桥。

    本文加上之前的两篇文章总结了调试器内部的工作原理。通过这一系列文章,再加上一点编程工作就应该可以在Linux下创建一个具有基本功能的调试器。


    展开全文
  • 中间继电就是个继电,它的工作原理和交流接触一样,都是由固定铁芯、动铁芯、弹簧、动触点、静触点、线圈、接线端子和外壳组成。线圈通电,动铁芯在电磁力作用下动作吸合,带动动触点动作,使常闭触点分开,常...
  • 天然气就是可燃气体,常见的燃气包括液化石油气、 人工煤气、天然气。随着燃气的广泛应用,由于...天然气泄漏报警器工作原理: 天然燃气泄漏报警器的主要核心部件主要是传感器,燃气传感器一般运用催化燃烧式原理,

    天然气就是可燃气体,常见的燃气包括液化石油气、 人工煤气、天然气。随着燃气的广泛应用,由于燃气泄漏所引发的爆炸、中毒和火灾事故也时有发生。天然气泄漏报警器就是安装在可能发生燃气泄漏的环境中,当现场环境中燃气浓度超过设定值,报警器就会发出声光报警,如果连接报警主机和接警中心则可联网报警,同时可以自动启动排风设备、关闭燃气管道阀门,防止燃气泄露,达到彻底预警放爆炸的危险等,保障生命和财产的安全。

    天然气泄漏报警器工作原理:

    天然燃气泄漏报警器的主要核心部件主要是传感器,燃气传感器一般运用催化燃烧式原理,催化燃烧式传感器原理广泛使用在检测可燃气体的浓度上,具有输出信号线形好、指数可靠、价格便宜、不会受到其他非可燃性气体的干扰等特点。

    其次催化燃烧式传感器采用惠斯通电桥原理,感应电阻与环境中的可燃气体发生无焰燃烧,使温度使感应电阻的阻值发生变化,打破电桥平衡,使之输出稳定的电流信号,再经过后期电路的放大、稳定和数据算法处理后显示燃气相关数据。

    注意事项:

    由于传感器本身的技术特点决定了随着时间的推移和环境的改变,传感器的灵敏度会降低,其设定的感应点也会发生变化。所以,在使用一段时间以后,报警器必须进行重新检定校验。如果传感器功能正常可以继续使用,如果传感器已经失灵,则需要及时更换。

    展开全文
  • busybox lxc工作原理分析 0202 用Xlinx ARTIX7芯片分析PCIe通信 0201 拦截数据到fifo,理解抓包软件内核数据获取原理(内置接口) 0200 网卡插拔检测原理,便于理解netd进度处理原理 0199 使用模板可以避免一些...
  • 变频是把工频电源(50Hz或60Hz)变换成各种频率的交流电源,以实现电机的变速运行的设备,其中控制... 2、软启动节能,功率因数补偿节能。 变频的间接作用: 1、节能(节电)。风机、泵类等设备传统的调速方法
  • 电感镇流由于结构简单,寿命长,作为...使得启辉器内的跳泡内的气体弧光放电,使得双金属片加热变形,两个电极靠在一起,形成通路给灯丝加热),当启动器的两个电极靠在一起,由于没有弧光放电,双金属片冷却,两极分开
  • 简介笼型异步电机启动方式和特点 针对企业电机启动存在问题,研发出智能软启动器,介绍软启动器工作原理,电气系统构成,以及工作流程
  • 本文介绍了各种降压稳压的设计,阐释它们的工作原理,并讨论实现这些设计需要考虑的实际因素。 采用降压稳压IC的降压转换 瑞萨电子ISL8541x系列降压稳压IC具有集成的上管和下管FET、内部启动二极管和内部...
  • SysLogger系统日志收集1.1 SysLogger启动入口1.2 1. SysLogger系统日志收集      在 【PostgreSQL教程】· PostgreSQL配置管理日志(一) 一文中,详细介绍了如何在PostgreSQL中开启日志收集,以及配置...

    1. SysLogger系统日志收集器

         在 【PostgreSQL教程】· PostgreSQL配置管理日志(一) 一文中,详细介绍了如何在PostgreSQL中开启日志收集器,以及配置log文件存储目录和大小,同时还介绍了许多与log文件相关联的配置参数。此外还说明了log文件在PostgreSQL10.0之前与之后的一些细微差异化变动。本节内容主要用于分析SysLogger日志收集器的内部原理,在学习 了本文之后,将对Logger的工作方式有着更加清晰的认识。

    1.1 SysLogger启动入口

         PostgreSQL是一个客户端/服务器模式(C/S)架构,整个服务的初始化代码入口是main.c(/src/backend/main)文件中的main函数。在main函数中会根据启动参数选项来进行判断,并走不同的分支。然后进行postmaster守护进程初始化操作,这一初始化过程主要在postmaster.c文件中实现(位于/src/backend/postmaster/目录)。守护进程postmaster负责整个系统的启动和关闭,它监听并接受来自客户端的连接请求,并未其每一个请求分配一个postgres服务。之后该客户端连接上面的所有请求操作都直接与postgres进程进行交互,而不再经由postmaster守护进程参与

         SysLogger日志收集器的初始化入口是SysLogger_Start函数(/src/backend/postmaster/syslogger.c)。在初始化日志收集器前,会对GNC全局变量参数Logging_collector进行判断(对应postgresql.conf配置文件中的logging_collector,更多细节阅读 【PostgreSQL教程】· PostgreSQL配置管理日志(一) ),若该参数值为true,则表示开启日志收集器进程logger,反之则退出,不开启logger进程。

    1.1.1 SysLogger日志收集器进程名

         SysLogger日志收集器的守护进程名是logger,在初始化SysLogger进程的时候,会对全局变量 MyBackendType (BackendType MyBackendType;)进程初始化为:B_LOGGER 的操作(MyBackendType = B_LOGGER;),后面在创建守护进程时候,根据GetBackendTypeDesc()函数获取对应的守护进程名。在PostgreSQL 13.2版本中,共支持以下几种类型的后台守护进程,如下所示枚举值(更多关于枚举类型的知识请阅读 C/C++ 枚举类型)。

    typedef enum BackendType
    {
    	B_INVALID = 0,
    	B_AUTOVAC_LAUNCHER,
    	B_AUTOVAC_WORKER,
    	B_BACKEND,
    	B_BG_WORKER,
    	B_BG_WRITER,
    	B_CHECKPOINTER,
    	B_STARTUP,
    	B_WAL_RECEIVER,
    	B_WAL_SENDER,
    	B_WAL_WRITER,
    	B_ARCHIVER,
    	B_STATS_COLLECTOR,
    	B_LOGGER,
    } BackendType;
    

         以下是各守护进程(枚举值)对应的进程名字:

    在这里插入图片描述

    1.1.2 PostgreSQL默认开启的守护进程

         并非所有的守护进程都会默认开启,有些是需要在postgresql.conf配置文件中进手动配置启动,比如日志收集器,就需要置参数logging_collector为on。 默认情况下,PostgreSQL仅开启了 checkpointer、background write、walwriter、autovacuum launcher、stats collector、logical replication launcher 这几个后台守护进程,如下图所示:

    在这里插入图片描述

    1.1.3 SysLogger日志收集器启动流程

    1.1.3.1 SysLogger系统日志整体初始化流程图

         SysLogger日志收集器的整体初始化过程如下流程图所示:

    在这里插入图片描述
         在初始化日志收集器时候,先根据postgres.conf配置文件中的参数(log_directory)来创建对应的log日志目录,默认log日志目录权限为文件拥有者具有读、写和执行权限。如下:

    #ifndef S_IRWXU
    #define S_IRWXU (S_IRUSR | S_IWUSR | S_IXUSR)
    #endif
    
    int
    MakePGDirectory(const char *directoryName)
    {
    	return mkdir(directoryName, pg_dir_create_mode);
    }
    
    

         目录创建好之后,再获取当前系统时间,然后将时间按照postgresql.conf配置文件中的log_filename参数的值进行对应的格式化,生成一个新log文件。然后以文件访问模式“+a”的方式打开log文件,若不存在则新建,并且对该log文件的属性进行调整,同时设置该log文件的文件流缓冲区为行缓存(_IOLBF)形式。在log文件创建成功之后,并调用函数fork_process()创建logger子进程(fork_process函数是fork函数的封装,包括返回值都匹配fork.),并在子进程中对内存相关的参数(OOM)进行一些内部设置。之后该函数返回进程的PID。

         根据PID的值进行对应的其他工作处理,若PID为0,则表示为子进程中,在子进程中会初始化一些与子进程状态相关的全局变量、注册父进程状态信号、关闭读管道、关闭postmaster父进程中的监听套接字和与父进程postmaster相关的内存数据等。然后进入SysLoggerMain()函数真正开始logger日志收集器进程的相关处理操作。 而在父进程中,则会先刷新stout、stderr等文件描述符的缓冲区数据,然后在将stdout、stderr文件描述符重定向到管道syslogPipe[1]d 写端,接着关闭管道写端和日志文件描述符句柄syslogFile。 因为postmaster父进程将永远不会向该log文件中写数据。

         之后父进程postmaster中将返回logger日志收集器的子进程PID。 该子进程PID将用于postmaster父进程的ServerLoop()函数中。ServerLoop()函数是守护进程postmaster(父进程)的主要空循环处理函数。该函数为一个死循环函数(for( ; ; )), 该函数内部主要负责对 checkpointer(检查点进程)、background write(后台写进程)、walwriter(预写式日志写进程)、autovacuum launcher(系统自动清理进程)、stats collector(统计数据搜集进程)、logger(系统日志进程)、archiver(预写式日志归档进程)等 辅助守护进程的状态管理维护,若发现其中某个进程PID丢失,则立刻重新创建一个新的对应守护进程。

         比如对于下图中的几个辅助进程logger、background writer、walwriter、autovacuum launcher、stats collector、logical replication launcher,若其中一个被手动人为kill掉,则postmaster守护进程将会检查到对应辅助子进程被kill掉的状态和对应信号(若开启了最高等级(debug5)日志,则会打印出对应信号值)。然后立刻重新fork()一个对应的子进程。备注:我结合代码逻辑亲自测试过,实际情况与逻辑是相吻合、匹配的。

    在这里插入图片描述
         此外,ServerLoop()函数还负责监听用户的连接请求,对于用户下发的每个请求,postmaster都会fork一个子进程(postgres)来进行处理,之后的该用户的所有请求操作,包括数据库、表、索引等的增删改查等操作都交由该postgres进程处理响应。因此PostgreSQL是一个多进程的客户端/服务器模型。

    if (selres > 0)
    		{
    			int			i;
    
    			for (i = 0; i < MAXLISTEN; i++)
    			{
    				if (ListenSocket[i] == PGINVALID_SOCKET)
    					break;
    				if (FD_ISSET(ListenSocket[i], &rmask))
    				{
    					Port	   *port;
    
    					port = ConnCreate(ListenSocket[i]);
    					if (port)
    					{
    						BackendStartup(port);
    
    						/*
    						 * We no longer need the open socket or port structure
    						 * in this process
    						 */
    						StreamClose(port->sock);
    						ConnFree(port);
    					}
    				}
    			}
    		}
    
    

         在接收到用户的连接请求后,ServerLoop()函数将首先创建一个与该请求对应的本地连接ConnCreate()。之后的fork子进程等工作则交给BackendStartup()函数中去处理。当fork进程成功后,父进程中将会把本次创建的子进程的PID放入到后端活动的进程PID链表中,该工作由dlist_push_head()函数负责完成。

    3. 总结

         到这里为止,较为详细地对PostgreSQL 8.0引入的logger系统日志收集器的初始化流程与工作原理做了梳理和总结,通过本文的阅读学习,将提升你对logger辅助进程的理解。同时,这也对PostgreSQL数据库服务工作的日志排查有着辅助性的帮助。下一节将继续对其他辅助进程的工作原理进行分析。

    展开全文
  • 从能量观点及利用RLC串联电路接通直流电源瞬态特性,阐述电子镇流工作原理,推导启动瞬间及正常工作时频率计算公式。
  • 调试器工作原理系列三篇

    千次阅读 2012-12-20 15:57:43
    基础篇 关于本文 我打算在这篇文章中介绍关于Linux下的调试实现的主要组成部分——ptrace系统调用。本文中出现的代码都在32位...调试可以启动某些进程,然后对其进行调试,或者将自己本身关联到一个已存在
  • 要了解 ROM、RAM 启动,首先需要对 链接 Linker 如何分配内存有一定的了解。 通常,对于栈生长方向向下的单片机,其内存一般模型是: 最低内存地址 转载于:...
  • 它可用来分配电能,不频繁地启动异步电动机,对电源线路及电动机等实行保护,当它们发生严重的过载或者短路及欠压等故障时能自动切断电路,其功能相当于熔断式开关与过欠热继电等组合。而且在分断故障电流后一般...
  • 三端集成稳压工作原理现以具有正电压输出的78L12系列为例介绍它的工作原理。 注 图中R11由输出电流档次决定,R12由输出电压档次决定图1电路如图1所示,三端式稳压启动电路、基准电压电路、取样比较放大电路...
  • 山武温控是根据工作环境的温度变化,在开关内部发生物理形变,从而产生某些特殊效应,产生导通或者断开动作的一系列自动控制元件,也叫温控开关、温度保护、温度控制,简称温控。 山武温控是通过温度...
  • 升压型DC/DC转换结构功能框图和工作原理从传统升压型DC/DC转换的结构和工作原理可以看出,其 问题是驱动电路对开关晶体管M的控制,本文提出了一种升压型PFM控制DC/DC转换,采用内置的MOSFET作为开关管,...
  • 电感镇流由于结构简单,寿命长,作为第一...使得启辉器内的跳泡内的气体弧光放电,使得双金属片加热变形,两个电极靠在一起,形成通路给灯丝加热),当启动器的两个电极靠在一起,由于没有弧光放电,双金属片冷却,两极
  • 此种防跳继电有有两个线圈,一个是供启动用的电流线圈,接在跳闸回路中;另一个是自保持用的电压线圈,通过本身的常开触点(TBJ1)接入合闸回路。  当合闸过程中,如正遇永久性故障,因而保护出口继电触点BCJ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,029
精华内容 811
关键字:

启动器工作原理