char * strcpy(char * dest, const char * src) // 实现src到dest的复制 { if ((src == NULL) || (dest == NULL)) //判断参数src和dest的有效性 { return NULL; } char *strdest = dest; //保存目标字符串的首地址 while ((*strDest++ = *strSrc++)!='\0'); //把src字符串的内容复制到dest下 return strdest; }
memcpy的原型是:void *memcpy( void *dest, const void *src, size_t count );html
void *memcpy(void *memTo, const void *memFrom, size_t size) { if((memTo == NULL) || (memFrom == NULL)) //memTo和memFrom必须有效 return NULL; char *tempFrom = (char *)memFrom; //保存memFrom首地址 char *tempTo = (char *)memTo; //保存memTo首地址 while(size -- > 0) //循环size次,复制memFrom的值到memTo中 *tempTo++ = *tempFrom++ ; return memTo; }
strcpy和memcpy主要有如下3方面的区别。
一、复制的内容不一样。strcpy只能复制字符串,而memcpy能够复制任意内容,例如字符数组、整型、结构体、类等。
二、复制的方法不一样。strcpy不须要指定长度,它遇到被复制字符的串结束符"\0"才结束,因此容易溢出。memcpy则是根据其第3个参数决定复制的长度。
三、用途不一样。一般在复制字符串时用strcpy,而须要复制其余类型数据时则通常用memcpylinux
extern char *strtok( char *s, const char *delim );
功能:分解字符串为一组标记串。s为要分解的字符串,delim为分隔符字符串。c++
说明:strtok()用来将字符串分割成一个个片断。当strtok()在参数s的字符串中发现到参数delim的分割字符时则会将该字符改成 \0 字符。在第一次调用时,strtok()必需给予参数s字符串,日后的调用则将参数s设置成NULL。每次调用成功则返回被分割出片断的指针。当没有被分割的串时则返回NULL。全部delim中包含的字符都会被滤掉,并将被滤掉的地方设为一处分割的节点。git
char * strstr( const char * str1, const char * str2 );
功能:从字符串 str1 中寻找 str2 第一次出现的位置(不比较结束符NULL),若是没找到则返回NULL。github
char * strchr ( const char *str, int ch );
功能:查找字符串 str 中首次出现字符 ch 的位置
说明:返回首次出现 ch 的位置的指针,若是 str 中不存在 ch 则返回NULL。web
char * strcpy( char * dest, const char * src );
功能:把 src 所指由NULL结束的字符串复制到 dest 所指的数组中。
说明:src 和 dest 所指内存区域不能够重叠且 dest 必须有足够的空间来容纳 src 的字符串。返回指向 dest 结尾处字符(NULL)的指针。面试
相似的:算法
strncpy编程
char * strncpy( char * dest, const char * src, size_t num );
char * strcat ( char * dest, const char * src );
功能:把 src 所指字符串添加到 dest 结尾处(覆盖dest结尾处的'\0')并添加'\0'。
说明:src 和 dest 所指内存区域不能够重叠且 dest 必须有足够的空间来容纳 src 的字符串。
返回指向 dest 的指针。数组
相似的 strncat
char * strncat ( char * dest, const char * src, size_t num );
int strcmp ( const char * str1, const char * str2 );
功能:比较字符串 str1 和 str2。
说明:
当s1<s2时,返回值<0
当s1=s2时,返回值=0
当s1>s2时,返回值>0
相似的:
strncmp
int strncmp ( const char * str1, const char * str2, size_t num );
size_t strlen ( const char * str );
功能:计算字符串 str 的长度
说明:返回 str 的长度,不包括结束符NULL。(注意与 sizeof 的区别)
相似的 strnlen:它从内存的某个位置(能够是字符串开头,中间某个位置,甚至是某个不肯定的内存区域)开始扫描,直到碰到第一个字符串结束符'\0'或计数器到达如下的maxlen为止,而后返回计数器值。
size_t strnlen(const char *str, size_t maxlen);
2、mem 系列
1.memset
void * memset ( void * ptr, int value, size_t num );
功能:把 ptr 所指内存区域的前 num 个字节设置成字符 value。
说明:返回指向 ptr 的指针。可用于变量初始化等操做
举例:
#include <stdio.h> #include <string.h> int main () { char str[] = "almost erery programer should know memset!"; memset(str, '-', sizeof(str)); printf("the str is: %s now\n", str); return 0; }
2.memmove
void * memmove ( void * dest, const void * src, size_t num );
功能:由 src 所指内存区域复制 num 个字节到 dest 所指内存区域。
说明:src 和 dest 所指内存区域能够重叠,但复制后 src 内容会被更改。函数返回指向dest的指针。
举例:
#include <stdio.h> #include <string.h> int main () { char str[] = "memmove can be very useful......"; memmove(str + 20, str + 15, 11); printf("the str is: %s\n", str); return 0; }
the str is: memmove can be very very useful.
3.memcpy
void * memcpy ( void * destination, const void * source, size_t num );
相似 strncpy。区别:拷贝指定大小的内存数据,而无论内容(不限于字符串)。
memcpy和memmove做用是同样的,惟一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。(memcpy更快)
但当源内存和目标内存存在重叠时,memcpy会出现错误,而memmove能正确地实施拷贝,但这也增长了一点点开销。
memmove的处理措施:
(1)当源内存的首地址等于目标内存的首地址时,不进行任何拷贝
(2)当源内存的首地址大于目标内存的首地址时,实行正向拷贝
(3)当源内存的首地址小于目标内存的首地址时,实行反向拷贝
4.memcmp
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
相似 strncmp
5.memchr
void * memchr ( const void *buf, int ch, size_t count);
功能:从 buf 所指内存区域的前 count 个字节查找字符 ch。
说明:当第一次遇到字符 ch 时中止查找。若是成功,返回指向字符 ch 的指针;不然返回NULL。
类的析构函数为何设计成虚函数?
析构函数的做用与构造函数正好相反,是在对象的生命期结束时,释放系统为对象所分配的空间,即要撤消一个对象。
用对象指针来调用一个函数,有如下两种状况:
若是是虚函数,会调用派生类中的版本。(在有派生类的状况下)
若是是非虚函数,会调用指针所指类型的实现版本。
析构函数也会遵循以上两种状况,由于析构函数也是函数嘛,不要把它看得太特殊。 当对象出了做用域或是咱们删除对象指针,析构函数就会被调用。
当派生类对象出了做用域,派生类的析构函数会先调用,而后再调用它父类的析构函数, 这样能保证分配给对象的内存获得正确释放。
可是,若是咱们删除一个指向派生类对象的基类指针,而基类析构函数又是非虚的话, 那么就会先调用基类的析构函数(上面第2种状况),派生类的析构函数得不到调用。
补充构造函数为何不能是虚函数:
1. 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这你们都知道,但是这个指向vtable的指针实际上是存储在对象的内存空间的。问题出来了,若是构造函数是虚的,就须要经过 vtable来调用,但是对象尚未实例化,也就是内存空间尚未,怎么找vtable呢?因此构造函数不能是虚函数。 2. 从使用角度,虚函数主要用于在信息不全的状况下,能使重载的函数获得对应的调用。构造函数自己就是要初始化实例,那使用虚函数也没有实际意义呀。因此构造函数没有必要是虚函数。虚函数的做用在于经过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在建立对象时自动调用的,不可能经过父类的指针或者引用去调用,所以也就规定构造函数不能是虚函数。 3. 构造函数不须要是虚函数,也不容许是虚函数,由于建立一个对象时咱们老是要明确指定对象的类型,尽管咱们可能经过实验室的基类的指针或引用去访问它但析构却不必定,咱们每每经过基类的指针来销毁对象。这时候若是析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。 4. 从实现上看,vbtl在构造函数调用后才创建,于是构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能肯定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的做用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。 5. 当一个构造函数被调用时,它作的首要的事情之一是初始化它的VPTR。所以,它只能知道它是“当前”类的,而彻底忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。因此它使用的VPTR必须是对于这个类的VTABLE。并且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但若是接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数肯定的。这就是为何构造函数调用是从基类到更加派生类顺序的另外一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向它本身的VTABLE。若是函数调用使用虚机制,它将只产生经过它本身的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。
说两种进程间通讯的方式。
1. 管道pipe:管道是一种半双工的通讯方式,数据只能单向流动,并且只能在具备亲缘关系的进程间使用。进程的亲缘关系一般是指父子进程关系。 2. 命名管道FIFO:有名管道也是半双工的通讯方式,可是它容许无亲缘关系进程间的通讯。 3. 内存映射MemoryMapping 4. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 5. 共享存储SharedMemory:共享内存就是映射一段能被其余进程所访问的内存,这段共享内存由一个进程建立,但多个进程均可以访问。共享内存是最快的 IPC 方式,它是针对其余进程间通讯方式运行效率低而专门设计的。它每每与其余通讯机制,如信号两,配合使用,来实现进程间的同步和通讯。 6. 信号量Semaphore:信号量是一个计数器,能够用来控制多个进程对共享资源的访问。它常做为一种锁机制,防止某进程正在访问共享资源时,其余进程也访问该资源。所以,主要做为进程间以及同一进程内不一样线程之间的同步手段。 7. 套接字Socket:套解口也是一种进程间通讯机制,与其余通讯机制不一样的是,它可用于不一样及其间的进程通讯。
8. 信号 ( sinal ) : 信号是一种比较复杂的通讯方式,用于通知接收进程某个事件已经发生。
问了一些调试的问题,VS为何能进行断点单步调式,原理是啥?(腾讯实习生面试也问过这个问题,软中断)
要在被调试进程中的某个目标地址上设定一个断点,调试器须要作下面两件事情:
1. 保存目标地址上的数据
2. 将目标地址上的第一个字节替换为int 3指令
而后,当调试器向操做系统请求开始运行进程时(经过前一篇文章中提到的PTRACE_CONT),进程最终必定会碰到int 3指令。此时进程中止,操做系统将发送一个信号。这时就是调试器再次出马的时候了,接收到一个其子进程(或被跟踪进程)中止的信号,而后调试器要作下面几 件事:
1. 在目标地址上用原来的指令替换掉int 3
2. 将被跟踪进程中的指令指针向后递减1。这么作是必须的,由于如今指令指针指向的是已经执行过的int 3以后的下一条指令。
3. 因为进程此时仍然是中止的,用户能够同被调试进程进行某种形式的交互。这里调试器可让你查看变量的值,检查调用栈等等。
4. 当用户但愿进程继续运行时,调试器负责将断点再次加到目标地址上(因为在第一步中断点已经被移除了),除非用户但愿取消断点。
template <class T> class auto_ptr { T* ptr; public: explicit auto_ptr(T* p = 0) : ptr(p) {} ~auto_ptr() {delete ptr;} T& operator*() {return *ptr;} T* operator->() {return ptr;} // ... };
从上面auto_ptr能够看出来,智能指针将基本类型指针封装为类对象指针(这个类确定是个模板,以适应不一样基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
templet<class T>
class auto_ptr { explicit auto_ptr(X* p = 0) ; ... };
所以不能自动将指针转换为智能指针对象,必须显式调用:
shared_ptr<double> pd; double *p_reg = new double; pd = p_reg; // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg); // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg); // allowed (explicit conversion)
string vacation("I wandered lonely as a cloud."); shared_ptr<string> pvac(&vacation); // No
pvac过时时,程序将把delete运算符用于非堆内存,这是错误的。
四种智能指针:
STL一共给咱们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暂不讨论)。
模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年:同时,若是您的编译器不支持其余两种解决力案,auto_ptr将是惟一的选择。
先来看下面的赋值语句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; vocaticn = ps;
上述赋值语句将完成什么工做呢?若是ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,由于程序将试图删除同一个对象两次——一次是ps过时时,另外一次是vocation过时时。要避免这种问题,方法有多种:
四种智能指针:
列1 | 列2 |
auto_ptr | 内部使用一个成员变量,指向一块内存资源(构造函数), 并在析构函数中释放内存资源。(未实现深复制,所以拷贝一个auto_ptr将会有删除两次一个内存的潜在问题) |
unique_ptr | 独享全部权的智能指针: 一、拥有它指向的对象 二、没法进行复制构造,没法进行复制赋值操做。即没法使两个unique_ptr指向同一个对象。可是能够进行移动构造和移动赋值操做(全部权转让) 三、保存指向某个对象的指针,当它自己被删除释放的时候,会使用给定的删除器释放它指向的对象 |
shared_ptr | 使用计数机制来代表资源被几个指针共享。能够经过成员函数use_count()来查看资源的全部者个数。 拷贝构造时候,计数器会加一。当咱们调用release()时,当前指针会释放资源全部权,计数减一。当计数等于0时,资源会被释放 会有死锁问题,引入weak_ptr:,若是说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能降低为0,资源永远不会释放。 |
weak_ptr | 构造和析构不会引发引用记数的增长或减小。协助shared_ptr,没有重载*和->但可使用lock得到一个可用的shared_ptr对象 |
Visual C++内置内存泄露检测工具,可是功能十分有限。VLD就至关强大,能够定位文件、行号,能够很是准确地找到内存泄漏的位置,并且还免费、开源!
在使用的时候只要将VLD的头文件和lib文件放在工程文件中便可。
也能够一次设置,新工程就不用从新设置了。只介绍在Visual Studio 2003/2005中的设置方法,VC++ 6.0相似:
#include “vld.h”
顺序无所谓,可是必定不能在一些预编译的文件前(如stdafx.h)。我是加在stdafx.h文件最后。
---------- Block 2715024 at 0x04D8A368: 512 bytes ---------- Call Stack: d:\kangzj\documents\visual studio 2005\projects\rsip.root\readtiff\readtiff\segmentflag.cpp (56): CSegmentFlag::GetFlagFromArray d:\kangzj\documents\visual studio 2005\projects\rsip.root\readtiff\readtiff\wholeclassdlg.cpp (495): segmentThreadProc f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\thrdcore.cpp (109): _AfxThreadEntry f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c (348): _callthreadstartex f:\dd\vctools\crt_bld\self_x86\crt\src\threadex.c (331): _threadstartex 0x7C80B729 (File and line number not available): GetModuleFileNameA
template <class RandomAccessIterator> inline void sort(RandomAccessIterator first, RandomAccessIterator last) { if (first != last) { __introsort_loop(first, last, value_type(first), __lg(last - first) * 2); __final_insertion_sort(first, last); } }
它是一个模板函数,只接受随机访问迭代器。if语句先判断区间有效性,接着调用__introsort_loop,它就是STL的Introspective Sort实现。在该函数结束以后,最后调用插入排序。咱们来揭开该算法的面纱:
template <class RandomAccessIterator, class T, class Size> void __introsort_loop(RandomAccessIterator first, RandomAccessIterator last, T*, Size depth_limit) { while (last - first > __stl_threshold) { if (depth_limit == 0) { partial_sort(first, last, last); return; } --depth_limit; RandomAccessIterator cut = __unguarded_partition (first, last, T(__median(*first, *(first + (last - first)/2), *(last - 1)))); __introsort_loop(cut, last, value_type(first), depth_limit); last = cut; } }
咱们来比较一下二者的区别,试想,若是一个序列只须要递归两次即可结束,即它能够分红四个子序列。原始的方式须要两个递归函数调用,接着二者各自调 用一次,也就是说进行了7次函数调用,以下图左边所示。可是STL这种写法每次划分子序列以后仅对右子序列进行函数调用,左边子序列进行正常的循环调用, 以下图右边所示。
二者区别就在于STL节省了接近一半的函数调用,因为每次的函数调用有必定的开销,所以对于数据量很是庞大时,这一半的函数调用可能可以省下至关可观的时 间。真是为了效率无所不用其极,使人惊叹!更关键是这并无带来太多的可读性的下降,稍稍一经分析便可以读懂。这种稍稍以牺牲可读性来换取效率的作法在 STL的实现中比比皆是,本文后面还会有例子。(more)
本文是关于调试器工做原理探究系列的第二篇。在开始阅读本文前,请先确保你已经读过本系列的第一篇(基础篇)。
本文的主要内容
这里我将说明调试器中的断点机制是如何实现的。断点机制是调试器的两大主要支柱之一 ——另外一个是在被调试进程的内存空间中查看变量的值。咱们已经在第一篇文章中稍微涉及到了一些监视被调试进程的知识,但断点机制仍然仍是个迷。阅读完本文以后,这将再也不是什么秘密了。
软中断
要在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
section
.
text
;
The
_start
symbol
must
be
declared
for
the
linker
(
ld
)
global
_start
_start
:
;
Prepare
arguments
for
the
sys_write
system
call
:
;
-
eax
:
system
call
number
(
sys_write
)
;
-
ebx
:
file
descriptor
(
stdout
)
;
-
ecx
:
pointer
to
string
;
-
edx
:
string
length
mov
edx
,
len1
mov
ecx
,
msg1
mov
ebx
,
1
mov
eax
,
4
;
Execute
the
sys_write
system
call
int
0x80
;
Now
print
the
other
message
mov
edx
,
len2
mov
ecx
,
msg2
mov
ebx
,
1
mov
eax
,
4
int
0x80
;
Execute
sys_exit
mov
eax
,
1
int
0x80
section
.
data
msg1
db
'Hello,'
,
0xa
len1
equ
$
-
msg1
msg2
db
'world!'
,
0xa
len2
equ
$
-
msg2
|
我如今使用的是汇编语言,这是为了不当使用C语言时涉及到的编译和符号的问题。上面列出的程序功能就是在一行中打印“Hello,”,而后在下一行中打印“world!”。这个例子与上一篇文章中用到的例子很类似。
我但愿设定的断点位置应该在第一条打印以后,但刚好在第二条打印以前。咱们就让断点打在第一个int 0x80指令以后吧,也就是mov edx, len2。首先,我须要知道这条指令对应的地址是什么。运行objdump –d:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
traced_printer2
:
file
format
elf32
-
i386
Sections
:
Idx
Name
Size
VMA
LMA
File
off
Algn
0
.
text
00000033
08048080
08048080
00000080
2
*
*
4
CONTENTS
,
ALLOC
,
LOAD
,
READONLY
,
CODE
1
.
data
0000000e
080490b4
080490b4
000000b4
2
*
*
2
CONTENTS
,
ALLOC
,
LOAD
,
DATA
Disassembly
of
section
.
text
:
08048080
<
.
text
>
:
8048080
:
ba
07
00
00
00
mov
$
0x7
,
%
edx
8048085
:
b9
b4
90
04
08
mov
$
0x80490b4
,
%
ecx
804808a
:
bb
01
00
00
00
mov
$
0x1
,
%
ebx
804808f
:
b8
04
00
00
00
mov
$
0x4
,
%
eax
8048094
:
cd
80
int
$
0x80
8048096
:
ba
07
00
00
00
mov
$
0x7
,
%
edx
804809b
:
b9
bb
90
04
08
mov
$
0x80490bb
,
%
ecx
80480a0
:
bb
01
00
00
00
mov
$
0x1
,
%
ebx
80480a5
:
b8
04
00
00
00
mov
$
0x4
,
%
eax
80480aa
:
cd
80
int
$
0x80
80480ac
:
b8
01
00
00
00
mov
$
0x1
,
%
eax
80480b1
:
cd
80
int
$
0x80
|
经过上面的输出,咱们知道要设定的断点地址是0x8048096。等等,真正的调试器不是像这样工做的,对吧?真正的调试器能够根据代码行数或者函 数名称来设定断点,而不是基于什么内存地址吧?很是正确。可是咱们离那个标准还差的远——若是要像真正的调试器那样设定断点,咱们还须要涵盖符号表以及调 试信息方面的知识,这须要用另外一篇文章来讲明。至于如今,咱们还必须得经过内存地址来设定断点。
看到这里我真的很想再扯一点题外话,因此你有两个选择。若是你真的对于为何地址是0x8048096,以及这表明什么意思很是感兴趣的话,接着看下一节。若是你对此毫无兴趣,只是想看看怎么设定断点,能够略过这一部分。
题外话——进程地址空间以及入口点
坦白的说,0x8048096自己并无太大意义,这只不过是相对可执行镜像的代码段(text section)开始处的一个偏移量。若是你仔细看看前面objdump出来的结果,你会发现代码段的起始位置是0x08048080。这告诉了操做系统 要将代码段映射到进程虚拟地址空间的这个位置上。在Linux上,这些地址能够是绝对地址(好比,有的可执行镜像加载到内存中时是不可重定位的),由于在 虚拟内存系统中,每一个进程都有本身独立的内存空间,并把整个32位的地址空间都看作是属于本身的(称为线性地址)。
若是咱们经过readelf工具来检查可执行文件的ELF头,咱们将获得以下输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
$
readelf
-
h
traced_printer2
ELF
Header
:
Magic
:
7f
45
4c
46
01
01
01
00
00
00
00
00
00
00
00
00
Class
:
ELF32
Data
:
2'
s
complement
,
little
endian
Version
:
1
(
current
)
OS
/
ABI
:
UNIX
-
System
V
ABI
Version
:
0
Type
:
EXEC
(
Executable
file
)
Machine
:
Intel
80386
Version
:
0x1
Entry
point
address
:
0x8048080
Start
of
program
headers
:
52
(
bytes
into
file
)
Start
of
section
headers
:
220
(
bytes
into
file
)
Flags
:
0x0
Size
of
this
header
:
52
(
bytes
)
Size
of
program
headers
:
32
(
bytes
)
Number
of
program
headers
:
2
Size
of
section
headers
:
40
(
bytes
)
Number
of
section
headers
:
4
Section
header
string
table
index
:
3
|
注意,ELF头的“entry point address”一样指向的是0x8048080。所以,若是咱们把ELF文件中的这个部分解释给操做系统的话,就表示:
1. 将代码段映射到地址0x8048080处
2. 从入口点处开始执行——地址0x8048080
可是,为何是0x8048080呢?它的出现是因为历史缘由引发的。每一个进程的地址空间的前128MB被保留给栈空间了(注:这一部分缘由可参考 Linkers and Loaders)。128MB恰好是0x80000000,可执行镜像中的其余段能够从这里开始。0x8048080是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. 当用户但愿进程继续运行时,调试器负责将断点再次加到目标地址上(因为在第一步中断点已经被移除了),除非用户但愿取消断点。