Wednesday, May 30, 2007

断点续传 以及 多线程下载 的原理

(一)断点续传的原理
其实断点续传的原理很简单,就是在Http的请求上和一般的下载有所不同而已。
打个比方,浏览器请求服务器上的一个文时,所发出的请求如下:
假设服务器域名为wwww.sjtu.edu.cn,文件名为down.zip。
GET /down.zip HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-
excel, application/msword, application/vnd.ms-powerpoint, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Connection: Keep-Alive


服务器收到请求后,按要求寻找请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下:


200
Content-Length=106786028
Accept-Ranges=bytes
Date=Mon, 30 Apr 2001 12:56:11 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT

 

所谓断点续传,也就是要从文件已经下载的地方开始继续下载。所以在客户端浏览器传给
Web服务器的时候要多加一条信息--从哪里开始。
下面是用自己编的一个"浏览器"来传递请求信息给Web服务器,要求从2000070字节开始。
GET /down.zip HTTP/1.0
User-Agent: NetFox
RANGE: bytes=2000070-
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2


仔细看一下就会发现多了一行RANGE: bytes=2000070-
这一行的意思就是告诉服务器down.zip这个文件从2000070字节开始传,前面的字节不用传了。
服务器收到这个请求以后,返回的信息如下:
206
Content-Length=106786028
Content-Range=bytes 2000070-106786027/106786028
Date=Mon, 30 Apr 2001 12:55:20 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT


和前面服务器返回的信息比较一下,就会发现增加了一行:
Content-Range=bytes 2000070-106786027/106786028
返回的代码也改为206了,而不再是200了。


知道了以上原理,就可以进行断点续传的编程了。
 
 
 
多线程下载原理
转自[http://blog.csdn.net/xsc2001/archive/2006/04/24/675108.aspx]

最近自己写了一个类似网络蚂蚁和FlashGet一样的程序,主要是为了自己能学点东西,不过还好,已经有点样子了,已经实现了类似它们的界面风格和功能,只是里面的下载任务、错误任务、已完成任务、中止任务的调度问题还没有详细去做,通过IE右键菜单的传参数还没有解决,IE右键菜单已经现,已经能够取到鼠标位置的URL,只是不知如何将取得的一个URL传给下载程序,还有待解决。现在将多线程下载同一个文件的原理讲述一下:

1、首先得到要下载的文件的长度,这是通过Http的HEAD命令得到其长度。

2、根据长度和线程数,确定各线程所下载的数据范围。通过HTTP的Get命令,这里需要指定From和To.

3、创建一个文件,这个文件是一个空文件,不需要事先生成与下载文件同样大小的空文件。给每个线程只需要传递其打开的文件句柄即可。

4、创建各个下载线程,每个线程接受到数据后就保存到相应的位置上去。这里不需要对文件加锁,因为操作系统一级本身对文件的I/O有锁的,我们的应用不用管,而且各个线程所写的文件位置是不一样的,每一线程负责下载和写一段数据。每个线程都记录下来了当前已经下载到的位置,这样若有线程出错了,可以从断点处继续下载。

5、等各线程都下载完各自的数据块后,最后中关闭这个文件。

注意:这里的文件打开时一定要以二制打开,否则在调用fseek时将导致位置定位不正确。

FILE *f;

if(( f = fopen(szFileName, "wr") != NULL)

{

...............................;

fclose(f);

}


 

Factory Method模式

Play slideshow | Download the highest quality version of a picture by clicking the + above it

一、模式概述

也许Factory Method模式是设计模式中应用最广泛的模式。在面向对象的设计中,关于对象的管理是其核心所在,而其中对象的创建则是对象管理的第一步。对象的创建非常简单,在C#中,只需要应用new操作符调用对象的构造函数即可,然而创建对象的时机却非常重要。 

首先我们从对象的特征来看,代表抽象关系的类型,如接口和抽象类,是不能创建的,换句话说,我们要创建的对象都是与具体的对象类型有关。因此,对象的创建工作必然涉及到设计中的实现细节,从而导致创建者与具体的被创建者之间耦合度增强。举例来说,如果在一个项目中我们需要创建一些图形对象,例如Circle、Square。这些对象的结构如下:

factory1.GIF

这个结构是非常符合OO思想的,它通过IShape接口将Square和Circle对象抽象出来,根据多态的原理,我们完全可以在程序中用IShape来代替具体的Square和Circle类,从而将具体的对象类型绑定留到运行时。然而,上文说到,接口对象是不能创建的,因此,项目一旦要创建IShape类型的对象,必然要针对具体的对象Square或Circle进行创建操作。例如:
    IShape shape = new Square(); 

如果是开发一个图形工具,诸如Square和Circle之类的对象,其创建工作必然非常频繁。可以设想,在这个项目的各个模块中,将会大量充斥着如上的代码行,导致的结果是各个模块无法与Square对象结耦,这意味着,如果我们改变创建的对象为Circle,就需要修改所有调用new Square()操作的模块。这既加大了工作量,同时也导致了项目的不可扩展性,以及模块的不可重用性。而对于图形对象的抽象IShape来说,也是不必要而失败的。

在面向对象的设计中,我们常常将可能变化的操作进行封装,封装的内容可能仅是某种行为,也可能是一种状态,或者是某些职责。而在当前的案例中,我们需要将对象的创建行为进行封装,这就引入了Factory Method模式。此时的对象就是Factory要生产的产品。既然产品有两种,相对应的工厂也应该是两个,即SquareFactory和CircleFactory。在Factory Method模式中,工厂对象的结构应与产品的结构平行,并与之一一对应,所以,对这两个工厂而言,还需要为其抽象出一个共同的工厂接口IShapeFactory:

factory2.GIF

代码如下:
public interface IShapeFactory
{

    IShape CreateShape();

}
public class SquareFactory:IShapeFactory
{
    public IShape CreateShape()
    {
         return new Square();
    }
}
public class CircleFactory:IShapeFactory
{
    public IShape CreateShape()
    {
      return new Circle();
    }
}

通过Factory Method模式,我们完成了对象创建的封装,将前面诸如IShape shape = new Square()的代码全部移到了各自的工厂对象中,并放到CreateShape()方法中实现。整个结构如下图所示:

factory3.GIF

请注意CreateShape()方法的返回类型是IShape类型,这就有效地避免了工厂对象与具体产品对象的依赖。

也许会有人认为,虽然通过工厂方法,将创建IShape对象的职责转交给工厂对象,然而在工厂类的结构中,仍然存在具体的工厂类对象。如此以来,虽然我们解除了模块与具体Shape对象的依赖,却增加了对具体工厂对象的依赖,这会带来何种益处?

让我们从对象创建的频率来分析。对于一个图形工具而言,IShape对象的创建无疑是频繁的,最大的可能性是在这个项目的各个模块中都可能存在创建IShape对象的需要。而工厂对象则不尽然,我们完全可以集中在一个模块中,初始化这个工厂对象,而在需要IShape对象的时候,直接调用工厂实例的CreateShape()就可以达到目的。

举例来说,假设在图形工具中,有三个模块:ModuleA,ModuleB,ModuleC;这三个模块中都需要创建Square对象,则按照原来的设计方案,这三个模块都包含这样一行代码:
IShape shape = new Square();

此时,与Square对象有依赖关系的就包括了ModuleA,ModuleB,ModuleC三个模块。如果我们需要修改shape对象为Circle类型,则这个变动无疑会影响到上述的三个模块。现在,我们引入Factory Method模式,并增加一个模块名为ModuleFactory,在这个模块中,我们创建一个工厂对象:
IShapeFactory shapeFactory = new SquareFactory();

如此以来,原来的三个模块有关Square对象的创建,就相应地修改为:
IShape shape = shapeFactory.CreateShape();

此时,即使需求发生改变,需要对shape对象进行修改,那么我们只需要修改ModuleFactory模块中的代码:
IShapeFactory shapeFactory = new CircleFactory();

而ModuleA,ModuleB,ModuleC三个模块则根本不需要作任何改变。如此的设计改进,虽然在项目中增加了三个工厂对象,并引入了ModuleFactory,但它却完成了ModuleA,ModuleB,ModuleC与具体的Square对象的解耦,从而将这三个模块与产品对象的依赖性转嫁到ModuleFactory上。如此以来,牵一发而不动其全身,极大地提高了模块的重用性。

从上述的分析可知,引入工厂对象并不是简单地为产品建立相应的工厂,而是要注意划分各个模块的职责,将工厂对象的创建放到合适的地方。最佳方案莫过于将创建工厂对象的职责集中起来,放到一个模块中;而不是在需要创建产品时,才创建工厂对象。错误的例子是在创建产品时将工厂对象的创建于产品对象的创建放在一起,并分布在各个模块中:
IShapeFactory shapeFactory = new SquareFactory();
IShape shape = shapeFactory.CreateShape();

这样的做法,则引入Factory Method模式,无异于画蛇添足了。

Online pictures are available for 30 days. Get Windows Live Mail desktop to create your own photo e-mails.

Tuesday, May 29, 2007

制作自己的man手册

 
 
 
 
制作自己的man手册

转自: http://bbs.chinaunix.net/viewthread.php?tid=834378&extra=page%3D1

一直以来没有发现制作man手册的文章。写了一篇供大家参考补充。
不是很完善,大家可以继续完善它。

关键命令:

groff   groff_man  gzip

其中大部分宏标签可以通过 man groff_man来查看。

主要宏标签

QUOTE:
.\"
在行首的注释



QUOTE:
\"
在行中的注释



QUOTE:
.TH title section [extra1] [extra2] [extra3]

其中section 为1-8之前的数字. extra1 在下中. extra2 在左下  extra3在上中
TH在文件的开始



QUOTE:
.SH 从行首开始,靠左边,宽体



QUOTE:
.SS 和.SS不同之处加了一个"边界" ,宽体



QUOTE:
.TP n
TP标签下的第2行开始缩进n个字符 (在第1行超过n字符的前提下) n默认值为7



QUOTE:
.sp

空行



QUOTE:
.B

宽体,如果本行没有文字,则.B标签的下一行为宽体



QUOTE:
.I

加下划线



QUOTE:
\fB文字\fR

将该文字设置成宽体



QUOTE:
\fI文字\fR

将文字加下划线



QUOTE:
.为\&.

-为\-






顶部
ELM
管理员
Rank: 9Rank: 9Rank: 9


UID 1
精华 1
积分 10
帖子 422
阅读权限 200
注册
状态 离线
发表于 09:22  资料  个人空间  短消息  加为好友 
以下为一个例子 用到上面所说的标签:

新建文件:
touch hello.1

编辑该文件

QUOTE:
.\" author : mq110

.TH Hello 1 "2006-9-26" "GNU" "GNU tools"         \"  hello(1)  GNU tools  hello(1)
                                                  \"  GNU       2006-9-26  hello(1)
.SH Name                                          \"  left
hello \- GNU hello                                 \" - must be \-
.SH SYNOPSIS
.B hello
.RB [\fB\-help\fR]
.RB [\-a]
.sp
.SH DESCRIPTION
this is a test\&.
.TP
\fIitalic\fR
.sp
.sp
.TP 3
123456789
890

groff -Tascii -man hello.1
看到显示的结果

cp hello.1 hello.1.bak
备份一下

gzip hello.1
生成hello.1.gz

mv hello.1.gz /usr/share/man/man1

man hello





顶部
ELM
管理员
Rank: 9Rank: 9Rank: 9


UID 1
精华 1
积分 10
帖子 422
阅读权限 200
注册
状态 离线
发表于 09:24  资料  个人空间  短消息  加为好友 
groff -Tascii -man hello.1

显示的结果

[root@Firewall man]# groff -Tascii -man hello.1

QUOTE:
Hello(1)                           GNU tools                          Hello(1)



Name
       hello - GNU hello

SYNOPSIS
       hello [-help] [-a]


DESCRIPTION
       this is a test.

       italic



       123456789
          890




GNU                                2006-9-26                          Hello(1)

可以看着这个显示做修改。


man hello的效果

QUOTE:
Hello(1)                                                 GNU tools                                                Hello(1)

Name
       hello - GNU hello

SYNOPSIS
       hello [-help] [-a]

DESCRIPTION
       this is a test.

       italic

       123456789
          890

GNU                                                      2006-9-26                                                Hello(1)



 

Monday, May 28, 2007

C++内存对象大会战

C++内存对象大会战
 
 
如果一个人自称为程序高手,却对内存一无所知,那么我可以告诉你,他一定在吹牛。用C或C++写程序,需要更多地关注内存,这不仅仅是因为内存的分配是否合理直接影响着程序的效率和性能,更为主要的是,当我们操作内存的时候一不小心就会出现问题,而且很多时候,这些问题都是不易发觉的,比如内存泄漏,比如悬挂指针。笔者今天在这里并不是要讨论如何避免这些问题,而是想从另外一个角度来认识C++内存对象。

  我们知道,C++将内存划分为三个逻辑区域:堆、栈和静态存储区。既然如此,我称位于它们之中的对象分别为堆对象,栈对象以及静态对象。那么这些不同的内存对象有什么区别了?堆对象和栈对象各有什么优劣了?如何禁止创建堆对象或栈对象了?这些便是今天的主题。

  一.基本概念

  先来看看栈。栈,一般用于存放局部变量或对象,如我们在函数定义中用类似下面语句声明的对象:
Type stack_object ;

  stack_object便是一个栈对象,它的生命期是从定义点开始,当所在函数返回时,生命结束。

  另外,几乎所有的临时对象都是栈对象。比如,下面的函数定义:
Type fun(Type object) ;

  这个函数至少产生两个临时对象,首先,参数是按值传递的,所以会调用拷贝构造函数生成一个临时对象object_copy1 ,在函数内部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一个栈对象,它在函数返回时被释放;还有这个函数是值返回的,在函数返回时,如果我们不考虑返回值优化(NRV),那么也会产生一个临时对象object_copy2,这个临时对象会在函数返回后一段时间内被释放。比如某个函数中有如下代码:
Type tt ,result ; //生成两个栈对象
tt = fun(tt) ; //函数返回时,生成的是一个临时对象object_copy2

  上面的第二个语句的执行情况是这样的,首先函数fun返回时生成一个临时对象object_copy2 ,然后再调用赋值运算符执行
tt = object_copy2 ; //调用赋值运算符

  看到了吗?编译器在我们毫无知觉的情况下,为我们生成了这么多临时对象,而生成这些临时对象的时间和空间的开销可能是很大的,所以,你也许明白了,为什么对于"大"对象最好用const引用传递代替按值进行函数参数传递了。

  接下来,看看堆。堆,又叫自由存储区,它是在程序执行的过程中动态分配的,所以它最大的特性就是动态性。在C++中,所有堆对象的创建和销毁都要由程序员负责,所以,如果处理不好,就会发生内存问题。如果分配了堆对象,却忘记了释放,就会产生内存泄漏;而如果已释放了对象,却没有将相应的指针置为 NULL,该指针就是所谓的"悬挂指针",再度使用此指针时,就会出现非法访问,严重时就导致程序崩溃。

  那么,C++中是怎样分配堆对象的?唯一的方法就是用new(当然,用类malloc指令也可获得C式堆内存),只要使用new,就会在堆中分配一块内存,并且返回指向该堆对象的指针。

  再来看看静态存储区。所有的静态对象、全局对象都于静态存储区分配。关于全局对象,是在main()函数执行前就分配好了的。其实,在main()函数中的显示代码执行之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的的构造及初始化工作。而在main()函数结束之前,会调用由编译器生成的exit函数,来释放所有的全局对象。比如下面的代码:
void main(void)
{
 … …// 显式代码
}

  实际上,被转化成这样:
void main(void)
{
 _main(); //隐式代码,由编译器产生,用以构造所有全局对象
 … … // 显式代码
 … …
 exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}

  所以,知道了这个之后,便可以由此引出一些技巧,如,假设我们要在main()函数执行之前做某些准备工作,那么我们可以将这些准备工作写到一个自定义的全局对象的构造函数中,这样,在main()函数的显式代码执行之前,这个全局对象的构造函数会被调用,执行预期的动作,这样就达到了我们的目的。刚才讲的是静态存储区中的全局对象,那么,局部静态对象了?局部静态对象通常也是在函数中定义的,就像栈对象一样,只不过,其前面多了个static关键字。局部静态对象的生命期是从其所在函数第一次被调用,更确切地说,是当第一次执行到该静态对象的声明代码时,产生该静态局部对象,直到整个程序结束时,才销毁该对象。

  还有一种静态对象,那就是它作为class的静态成员。考虑这种情况时,就牵涉了一些较复杂的问题。

  第一个问题是class的静态成员对象的生命期,class的静态成员对象随着第一个class object的产生而产生,在整个程序结束时消亡。也就是有这样的情况存在,在程序中我们定义了一个class,该类中有一个静态对象作为成员,但是在程序执行过程中,如果我们没有创建任何一个该class object,那么也就不会产生该class所包含的那个静态对象。还有,如果创建了多个class object,那么所有这些object都共享那个静态对象成员。

  第二个问题是,当出现下列情况时:
class Base
{
 public:
  static Type s_object ;
}
class Derived1 : public Base / / 公共继承
{
 … …// other data
}
class Derived2 : public Base / / 公共继承
{
 … …// other data
}

Base example ;
Derivde1 example1 ;
Derivde2 example2 ;
example.s_object = …… ;
example1.s_object = …… ;
example2.s_object = …… ;


  请注意上面标为黑体的三条语句,它们所访问的s_object是同一个对象吗?答案是肯定的,它们的确是指向同一个对象,这听起来不像是真的,是吗?但这是事实,你可以自己写段简单的代码验证一下。我要做的是来解释为什么会这样?我们知道,当一个类比如Derived1,从另一个类比如Base继承时,那么,可以看作一个Derived1对象中含有一个Base型的对象,这就是一个subobject。一个Derived1对象的大致内存布局如下:
  
  让我们想想,当我们将一个Derived1型的对象传给一个接受非引用Base型参数的函数时会发生切割,那么是怎么切割的呢?相信现在你已经知道了,那就是仅仅取出了Derived1型的对象中的 subobject,而忽略了所有Derived1自定义的其它数据成员,然后将这个subobject传递给函数(实际上,函数中使用的是这个 subobject的拷贝)。

  所有继承Base类的派生类的对象都含有一个Base型的subobject(这是能用Base型指针指向一个Derived1对象的关键所在,自然也是多态的关键了),而所有的 subobject和所有Base型的对象都共用同一个s_object对象,自然,从Base类派生的整个继承体系中的类的实例都会共用同一个 s_object对象了。上面提到的example、example1、example2的对象布局如下图所示:

  二.三种内存对象的比较

  栈对象的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般较堆对象快,因为分配堆对象时,会调用 operator new操作,operator new会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈空间容量比较小,一般是1MB~2MB,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。

  堆对象,其产生时刻和销毁时刻都要程序员精确定义,也就是说,程序员对堆对象的生命具有完全的控制权。我们常常需要这样的对象,比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。
接下来看看static对象。

  首先是全局对象。全局对象为类间通信和函数间通信提供了一种最简单的方式,虽然这种方式并不优雅。一般而言,在完全的面向对象语言中,是不存在全局对象的,比如C#,因为全局对象意味着不安全和高耦合,在程序中过多地使用全局对象将大大降低程序的健壮性、稳定性、可维护性和可复用性。C++也完全可以剔除全局对象,但是最终没有,我想原因之一是为了兼容C。

  其次是类的静态成员,上面已经提到,基类及其派生类的所有对象都共享这个静态成员对象,所以当需要在这些class之间或这些class objects之间进行数据共享或通信时,这样的静态成员无疑是很好的选择。

  接着是静态局部对象,主要可用于保存该对象所在函数被屡次调用期间的中间状态,其中一个最显著的例子就是递归函数,我们都知道递归函数是自己调用自己的函数,如果在递归函数中定义一个nonstatic局部对象,那么当递归次数相当大时,所产生的开销也是巨大的。这是因为nonstatic局部对象是栈对象,每递归调用一次,就会产生一个这样的对象,每返回一次,就会释放这个对象,而且,这样的对象只局限于当前调用层,对于更深入的嵌套层和更浅露的外层,都是不可见的。每个层都有自己的局部对象和参数。

  在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

  三.使用栈对象的意外收获

  前面已经介绍到,栈对象是在适当的时候创建,然后在适当的时候自动释放的,也就是栈对象有自动管理功能。那么栈对象会在什么会自动释放了?第一,在其生命期结束的时候;第二,在其所在的函数发生异常的时候。你也许说,这些都很正常啊,没什么大不了的。是的,没什么大不了的。但是只要我们再深入一点点,也许就有意外的收获了。

  栈对象,自动释放时,会调用它自己的析构函数。如果我们在栈对象中封装资源,而且在栈对象的析构函数中执行释放资源的动作,那么就会使资源泄漏的概率大大降低,因为栈对象可以自动的释放资源,即使在所在函数发生异常的时候。实际的过程是这样的:函数抛出异常时,会发生所谓的stack_unwinding(堆栈回滚),即堆栈会展开,由于是栈对象,自然存在于栈中,所以在堆栈回滚的过程中,栈对象的析构函数会被执行,从而释放其所封装的资源。除非,除非在析构函数执行的过程中再次抛出异常�D�D而这种可能性是很小的,所以用栈对象封装资源是比较安全的。基于此认识,我们就可以创建一个自己的句柄或代理来封装资源了。智能指针(auto_ptr)中就使用了这种技术。在有这种需要的时候,我们就希望我们的资源封装类只能在栈中创建,也就是要限制在堆中创建该资源封装类的实例。

  四.禁止产生堆对象

  上面已经提到,你决定禁止产生某种类型的堆对象,这时你可以自己创建一个资源封装类,该类对象只能在栈中产生,这样就能在异常的情况下自动释放封装的资源。

  那么怎样禁止产生堆对象了?我们已经知道,产生堆对象的唯一方法是使用new操作,如果我们禁止使用new不就行了么。再进一步,new操作执行时会调用operator new,而operator new是可以重载的。方法有了,就是使new operator 为private,为了对称,最好将operator delete也重载为private。现在,你也许又有疑问了,难道创建栈对象不需要调用new吗?是的,不需要,因为创建栈对象不需要搜索内存,而是直接调整堆栈指针,将对象压栈,而operator new的主要任务是搜索合适的堆内存,为堆对象分配空间,这在上面已经提到过了。好,让我们看看下面的示例代码:

#include <stdlib.h> //需要用到C式内存分配函数
class Resource ; //代表需要被封装的资源类
class NoHashObject
{
 private:
  Resource* ptr ;//指向被封装的资源
  ... ... //其它数据成员
  void* operator new(size_t size) //非严格实现,仅作示意之用
  {
   return malloc(size) ;
  }
  void operator delete(void* pp) //非严格实现,仅作示意之用
  {
   free(pp) ;
  }
 public:
  NoHashObject()
  {
   //此处可以获得需要封装的资源,并让ptr指针指向该资源
   ptr = new Resource() ;
  }
  ~NoHashObject()
  {
   delete ptr ; //释放封装的资源
  }
};

  NoHashObject现在就是一个禁止堆对象的类了,如果你写下如下代码:
NoHashObject* fp = new NoHashObject() ; //编译期错误!
delete fp ;

  上面代码会产生编译期错误。好了,现在你已经知道了如何设计一个禁止堆对象的类了,你也许和我一样有这样的疑问,难道在类NoHashObject的定义不能改变的情况下,就一定不能产生该类型的堆对象了吗?不,还是有办法的,我称之为"暴力破解法"。C++是如此地强大,强大到你可以用它做你想做的任何事情。这里主要用到的是技巧是指针类型的强制转换。
void main(void)
{
 char* temp = new char[sizeof(NoHashObject)] ;

 //强制类型转换,现在ptr是一个指向NoHashObject对象的指针
 NoHashObject* obj_ptr = (NoHashObject*)temp ;

 temp = NULL ; //防止通过temp指针修改NoHashObject对象

 //再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员
 Resource* rp = (Resource*)obj_ptr ;

 //初始化obj_ptr指向的NoHashObject对象的ptr成员
 rp = new Resource() ;
 //现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了
 ... ...

 delete rp ;//释放资源
 temp = (char*)obj_ptr ;
 obj_ptr = NULL ;//防止悬挂指针产生
 delete [] temp ;//释放NoHashObject对象所占的堆空间。
}

  上面的实现是麻烦的,而且这种实现方式几乎不会在实践中使用,但是我还是写出来路,因为理解它,对于我们理解C++内存对象是有好处的。对于上面的这么多强制类型转换,其最根本的是什么了?我们可以这样理解:

  某块内存中的数据是不变的,而类型就是我们戴上的眼镜,当我们戴上一种眼镜后,我们就会用对应的类型来解释内存中的数据,这样不同的解释就得到了不同的信息。

  所谓强制类型转换实际上就是换上另一副眼镜后再来看同样的那块内存数据。

  另外要提醒的是,不同的编译器对对象的成员数据的布局安排可能是不一样的,比如,大多数编译器将NoHashObject的ptr指针成员安排在对象空间的头4个字节,这样才会保证下面这条语句的转换动作像我们预期的那样执行:
Resource* rp = (Resource*)obj_ptr ;

  但是,并不一定所有的编译器都是如此。

  既然我们可以禁止产生某种类型的堆对象,那么可以设计一个类,使之不能产生栈对象吗?当然可以。

  五.禁止产生栈对象

  前面已经提到了,创建栈对象时会移动栈顶指针以"挪出"适当大小的空间,然后在这个空间上直接调用对应的构造函数以形成一个栈对象,而当函数返回时,会调用其析构函数释放这个对象,然后再调整栈顶指针收回那块栈内存。在这个过程中是不需要operator new/delete操作的,所以将operator new/delete设置为private不能达到目的。当然从上面的叙述中,你也许已经想到了:将构造函数或析构函数设为私有的,这样系统就不能调用构造/析构函数了,当然就不能在栈中生成对象了。

  这样的确可以,而且我也打算采用这种方案。但是在此之前,有一点需要考虑清楚,那就是,如果我们将构造函数设置为私有,那么我们也就不能用new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数啊。所以,我打算只将析构函数设置为private。再进一步,将析构函数设为private除了会限制栈对象生成外,还有其它影响吗?是的,这还会限制继承。

  如果一个类不打算作为基类,通常采用的方案就是将其析构函数声明为private。

  为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美了。如下代码所示:
class NoStackObject
{
 protected:
  ~NoStackObject() { }
 public:
  void destroy()
  {
   delete this ;//调用保护析构函数
  }
};

  接着,可以像这样使用NoStackObject类:
NoStackObject* hash_ptr = new NoStackObject() ;
... ... //对hash_ptr指向的对象进行操作
hash_ptr->destroy() ;

  呵呵,是不是觉得有点怪怪的,我们用new创建一个对象,却不是用delete去删除它,而是要用destroy方法。很显然,用户是不习惯这种怪异的使用方式的。所以,我决定将构造函数也设为private或protected。这又回到了上面曾试图避免的问题,即不用new,那么该用什么方式来生成一个对象了?我们可以用间接的办法完成,即让这个类提供一个static成员函数专门用于产生该类型的堆对象。(设计模式中的singleton模式就可以用这种方式实现。)让我们来看看:
class NoStackObject
{
 protected:
  NoStackObject() { }
  ~NoStackObject() { }
 public:
  static NoStackObject* creatInstance()
  {
   return new NoStackObject() ;//调用保护的构造函数
  }
  void destroy()
  {
   delete this ;//调用保护的析构函数
  }
};

  现在可以这样使用NoStackObject类了:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
... ... //对hash_ptr指向的对象进行操作
hash_ptr->destroy() ;
hash_ptr = NULL ; //防止使用悬挂指针

  现在感觉是不是好多了,生成对象和释放对象的操作一致了。

  ok,讲到这里,已经涉及了较多的东西,如果要把内存对象讲得更深入更全面,那可能需要写成一本书了,而就我自己的功力而言,可能是很难完全把握的。如果上面所写的能使你有所收获或启发,我就满足了。如果你要更进一步去了解内存对象方面的知识,那么我可以推荐你看看《深入探索C++对象模型》这本书。

关于全局、static对象/变量的初始化问题

关于全局、static对象/变量的初始化问题
 
 
 
1. 全局变量、static变量的初始化时机:main()函数执行之前(或者说main中第一个用户语句执行之前)。 
2. 初始化顺序。
1)全局对象、外部static对象
    a)同一编译单元(同一源文件)中,按照对象/变量的定义顺序初始化。
    b)不同编译单元,C++标准未保证初始化先后顺序,只保证都在main()之前初始化完成。
2)函数内部local static变量,在该函数调用过程中第一次遇到该static变量时初始化。
 
     基于以上观点,大师们建议少用全局变量,全局变量之间要消除依赖关系――特别是初始化依赖关系!
     全局变量的使用可以参考 Scott Meyers 在《More Effective C++》中M34所说的,模拟Singleton模式,通过函数内部的local static变量来代替全局变量。
     写个示例程序验证下这些变量的初始化,中间的注释部分就作为上面结论的一些补充吧。

class CA
{
public:
     class CInner
     {
     public:
          CInner()
          {
               cout << "constructor of inner class CInner." << endl << endl;
          }
     };
public:
     CA()
     {
          cout << "Constructor of CA." << endl;
          cout << " m_i1 = "  << m_i1
                  << ", m_i2 = " << m_i2
                  << ", m_i3 = " << m_i3
                  << ", m_i4 = " << m_i4 << endl << endl;
     }

     static void Func1()
     {
          cout << " In function Func1()." << endl;
          static CInner myInner1;
     }

     static void Func2()
     {
          cout << " In function Func2(), m_i1 = " << m_i1 << endl;
          if(m_i1 < 10)
          {
               cout << "m_i1 < 10 and Constructor of CInner won't be called!" << endl << endl;
               return;
          }

          static CInner myInner2;
     }
public:
     static int m_i1;
     static int m_i2;
     static const int m_i3;
     static int m_i4;
};

/* 不同模块的全局、static变量/对象初始化顺序不确定;
 * 同一个编译模块按定义顺序初始化。
 * 但有一点相同,就是它们均在编译期已分配好内存。
 * 对于诸如基本数据类型,编译期能确定其值的,编译器就直接将值写入分配的空间,如"
CA::m_i1=3"。
 * 对于编译期不能确定值的,要等到运行时main函数之前初始化,如theCA、CA::m_i2。
 * 但若static的初始化表达式均为const或字面常量等确定的值,则亦能在编译期确定值,如m_i4。
 */

int CA::m_i1 = 1;
CA theCA;
const int  CA::m_i3 = 3;
int CA::m_i2 = CA::m_i1 + 1;
int CA::m_i4 = CA::m_i3 + 1;

int main(int argc, _TCHAR* argv[])
{
     CA::Func1();
     CA::Func2();

     cout << "After CA::m_i1 increased by 11 :" << endl;
     CA::m_i1 += 11;
     CA::Func2();

     return 0;
}


以上程序运行结果为:
Constructor of CA.
 m_i1 = 1, m_i2 = 0, m_i3 = 3, m_i4 = 4
 In function Func1().
constructor of inner class CInner
 In function Func2(), m_i1 = 1
m_i1 < 10 and Constructor of CInner won't be called!
After CA::m_i1 increased by 11 :
 In function Func2(), m_i1 = 12
constructor of inner class CInner.

Sunday, May 27, 2007

Before main() 分析

Before main() 分析
 
 
 
★ 前言

本文分析了在main()之前的ELF程序流程,试图让您更清楚的把握程序的流程的脉络走向。
从而更深入的了解ELF。不正确之处,还请斧正。


★ 综述

ELF的可执行文件与共享库在结构上非常类似,它们具有一张程序段表,用来描述这些段如何映射到进程空间.
对于可执行文件来说,段的加载位置是固定的,程序段表中如实反映了段的加载地址.对于共享库来说,段的加
载位置是浮动的,位置无关的,程序段表反映的是以0作为基准地址的相对加载地址.尽管共享库的连接是不
充分的,为了便于测试动态链接器,Linux允许直接加载共享库运行.如果应用程序具有动态链接器的描述段,
内核在完成程序段加载后,紧接着加载动态链接器,并且启动动态链接器的入口.如果没有动态链接器的描述段,
就直接交给用户程序入口。
上述这部分请参考:linuxforum论坛上opera写的《分析ELF的加载过程》

在控制权交给动态链接器的入口后,首先调用_dl_start函数获得真实的程序入口(注:该入口地址
不是main的地址,也就是说一般程序的入口不是main),然后循环调用每个共享object的初始化函数,
接着跳转到真实的程序入口,一般为_start(程序中的_start)的一个例程,该例程压入一些参数到堆栈,
就直接调用__libc_start_main函数。在__libc_start_main函数中替动态连接器和自己程序安排
destructor,并运行程序的初始化函数。然后才把控制权交给main()函数。



★ main()之前流程

下面就是动态链接器的入口。
/* Initial entry point code for the dynamic linker.
 ��The C function `_dl_start' is the real entry point;
 ��its return value is the user program's entry point.��*/

#define RTLD_START asm ("\
.text\n\
.globl _start\n\
.globl _dl_start_user\n\
_start:\n\
 �� pushl %esp\n\
 �� call _dl_start\n\/*该函数返回时候,%eax中存放着user entry point address*/
 �� popl %ebx\n\/*%ebx放着是esp的内容*/
_dl_start_user:\n\
 �� # Save the user entry point address in %edi.\n\
 �� movl %eax, %edi\n\/*入口地址放在%edi*/

 �� # Point %ebx at the GOT.
 �� call 0f\n\
0:����popl %ebx\n\
 �� addl $_GLOBAL_OFFSET_TABLE_+[.-0b], %ebx\n\

 �� # Store the highest stack address\n\
 �� movl __libc_stack_end@GOT(%ebx), %eax\n\
 �� movl %esp, (%eax)\n\/*把栈顶%esp放到GOT的__libc_stack_end中*/

 �� # See if we were run as a command with the executable file\n\
 �� # name as an extra leading argument.\n\
 �� movl _dl_skip_args@GOT(%ebx), %eax\n\
 �� movl (%eax), %eax\n\

 �� # Pop the original argument count.\n\
 �� popl %ecx\n\

 �� # Subtract _dl_skip_args from it.\n\
 �� subl %eax, %ecx\n\
 ��
 �� # Adjust the stack pointer to skip _dl_skip_args words.\n\
 �� leal (%esp,%eax,4), %esp\n\
 ��
 �� # Push back the modified argument count.\n\
 �� pushl %ecx\n\
 ��
 �� # Push the searchlist of the main object as argument in\n\
 �� # _dl_init_next call below.\n\
 �� movl _dl_main_searchlist@GOT(%ebx), %eax\n\
 �� movl (%eax), %esi\n\
0:����movl %esi,%eax\n\
 ��
 �� # Call _dl_init_next to return the address of an initializer\n\
 �� # function to run.\n\
 �� call _dl_init_next@PLT\n\/*该函数返回初始化函数的地址,返回地址放在%eax中*/
 ��
 �� # Check for zero return, when out of initializers.\n\
 �� testl %eax, %eax\n\
 �� jz 1f\n\
 ��
 �� # Call the shared object initializer function.\n\
 �� # NOTE: We depend only on the registers (%ebx, %esi and %edi)\n\
 �� # and the return address pushed by this call;\n\
 �� # the initializer is called with the stack just\n\
 �� # as it appears on entry, and it is free to move\n\
 �� # the stack around, as long as it winds up jumping to\n\
 �� # the return address on the top of the stack.\n\
 �� call *%eax\n\/*调用共享object初始化函数*/
 ��
 �� # Loop to call _dl_init_next for the next initializer.\n\
 �� jmp 0b\n\

1:����# Clear the startup flag.\n\
 �� movl _dl_starting_up@GOT(%ebx), %eax\n\
 �� movl $0, (%eax)\n\
 ��
 �� # Pass our finalizer function to the user in %edx, as per ELF ABI.\n\
 �� movl _dl_fini@GOT(%ebx), %edx\n\
 ��
 �� # Jump to the user's entry point.\n\
 �� jmp *%edi\n\
.previous\n\
");


sysdeps\i386\start.s中
user's entry也就是下面的_start例程

/* This is the canonical entry point, usually the first thing in the text
 ��segment.��The SVR4/i386 ABI (pages 3-31, 3-32) says that when the entry
 ��point runs, most registers' values are unspecified, except for:

 ��%edx��������Contains a function pointer to be registered with `atexit'.
 ����������This is how the dynamic linker arranges to have DT_FINI
 ������ functions called for shared libraries that have been loaded
 ������ before this code runs.

 ��%esp��������The stack contains the arguments and environment:
 ����������0(%esp)������������argc
 ������ 4(%esp)������������argv[0]
 ������ ...
 ������ (4*argc)(%esp)��������NULL
 ������ (4*(argc+1))(%esp)����envp[0]
 ������ ...
 ������������������ NULL
*/

 �� .text
 �� .globl _start
_start:
 �� /* Clear the frame pointer.��The ABI suggests this be done, to mark
 ������the outermost frame obviously.��*/
 �� xorl %ebp, %ebp

 �� /* Extract the arguments as encoded on the stack and set up
 ������the arguments for `main': argc, argv.��envp will be determined
 ������later in __libc_start_main.��*/
 �� popl %esi��������/* Pop the argument count.��*/
 �� movl %esp, %ecx��������/* argv starts just at the current stack top.*/

 �� /* Before pushing the arguments align the stack to a double word
 ������boundary to avoid penalties from misaligned accesses.��Thanks
 ������to Edward Seidl <seidl@janed.com> for pointing this out.��*/
 �� andl $0xfffffff8, %esp
 �� pushl %eax��������/* Push garbage because we allocate
 ������������������28 more bytes.��*/

 �� /* Provide the highest stack address to the user code (for stacks
 ������which grow downwards).��*/
 �� pushl %esp

 �� pushl %edx��������/* Push address of the shared library
 ������������������termination function.��*/

 �� /* Push address of our own entry points to .fini and .init.��*/
 �� pushl $_fini
 �� pushl $_init

 �� pushl %ecx��������/* Push second argument: argv.��*/
 �� pushl %esi��������/* Push first argument: argc.��*/

 �� pushl $main

 �� /* Call the user's main function, and exit with its value.
 ������But let the libc call main.����*/
 �� call __libc_start_main

 �� hlt������������/* Crash if somehow `exit' does return.��*/



__libc_start_main在sysdeps\generic\libc_start.c中
假设定义的是PIC的代码。
struct startup_info
{
  void *sda_base;
  int (*main) (int, char **, char **, void *);
  int (*init) (int, char **, char **, void *);
  void (*fini) (void);
};

int
__libc_start_main (int argc, char **argv, char **envp,
 ����������void *auxvec, void (*rtld_fini) (void),
 ����������struct startup_info *stinfo,
 ����������char **stack_on_entry)
{

  /* the PPC SVR4 ABI says that the top thing on the stack will
 ����be a NULL pointer, so if not we assume that we're being called
 ����as a statically-linked program by Linux...���� */
  if (*stack_on_entry != NULL)
 �� {
 ���� /* ...in which case, we have argc as the top thing on the
 ����stack, followed by argv (NULL-terminated), envp (likewise),
 ����and the auxilary vector.��*/
 ���� argc = *(int *) stack_on_entry;
 ���� argv = stack_on_entry + 1;
 ���� envp = argv + argc + 1;
 ���� auxvec = envp;
 ���� while (*(char **) auxvec != NULL)
 �� ++auxvec;
 ���� ++auxvec;
 ���� rtld_fini = NULL;
 �� }

  /* Store something that has some relationship to the end of the
 ����stack, for backtraces.��This variable should be thread-specific.��*/
  __libc_stack_end = stack_on_entry + 4;

  /* Set the global _environ variable correctly.��*/
  __environ = envp;

  /* Register the destructor of the dynamic linker if there is any.��*/
  if (rtld_fini != NULL)
 �� atexit (rtld_fini);/*替动态连接器安排destructor*/

  /* Call the initializer of the libc.��*/

  __libc_init_first (argc, argv, envp);/*一个空函数*/

  /* Register the destructor of the program, if any.��*/
  if (stinfo->fini)
 �� atexit (stinfo->fini);/*安排程序自己的destructor*/

  /* Call the initializer of the program, if any.��*/

  /*运行程序的初始化函数*/
  if (stinfo->init)
 �� stinfo->init (argc, argv, __environ, auxvec);

/*运行程序main函数,到此,控制权才交给我们一般所说的程序入口*/
  exit (stinfo->main (argc, argv, __environ, auxvec));

}



void
__libc_init_first (int argc __attribute__ ((unused)), ...)
{
}

int
atexit (void (*func) (void))
{
  struct exit_function *new = __new_exitfn ();

  if (new == NULL)
 �� return -1;

  new->flavor = ef_at;
  new->func.at = func;
  return 0;
}


/* Run initializers for MAP and its dependencies, in inverse dependency
 ��order (that is, leaf nodes first).��*/

ElfW(Addr)
internal_function
_dl_init_next (struct r_scope_elem *searchlist)
{
  unsigned int i;

  /* The search list for symbol lookup is a flat list in top-down
 ����dependency order, so processing that list from back to front gets us
 ����breadth-first leaf-to-root order.��*/

  i = searchlist->r_nlist;
  while (i-- > 0)
 �� {
 ���� struct link_map *l = searchlist->r_list[i];

 ���� if (l->l_init_called)
 �� /* This object is all done.��*/
 �� continue;

 ���� if (l->l_init_running)
 �� {
 ���� /* This object's initializer was just running.
 ��������Now mark it as having run, so this object
 ��������will be skipped in the future.��*/
 ���� l->l_init_running = 0;
 ���� l->l_init_called = 1;
 ���� continue;
 �� }

 ���� if (l->l_info[DT_INIT]
 ���� && (l->l_name[0] != '\0' || l->l_type != lt_executable))
 �� {
 ���� /* Run this object's initializer.��*/
 ���� l->l_init_running = 1;

 ���� /* Print a debug message if wanted.��*/
 ���� if (_dl_debug_impcalls)
 ������ _dl_debug_message (1, "\ncalling init: ",
 �������������� l->l_name[0] ? l->l_name : _dl_argv[0],
 �������������� "\n\n", NULL);

 ���� /*共享库的基地址+init在基地址中的偏移量*/
 ���� return l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr;
 ����
 �� }

 ���� /* No initializer for this object.
 ����Mark it so we will skip it in the future.��*/
 ���� l->l_init_called = 1;
 �� }


  /* Notify the debugger all new objects are now ready to go.��*/
  _r_debug.r_state = RT_CONSISTENT;
  _dl_debug_state ();

  return 0;
}
在main()之前的程序流程看试有点简单,但正在运行的时候还是比较复杂的
(自己用GBD跟踪下就知道了),因为一般的程序都需要涉及到PLT,GOT标号的
重定位。弄清楚这个对ELF由为重要,以后有机会再补上一篇吧。


★ 手动确定程序和动态连接器的入口

[alert7@redhat62 alert7]$ cat helo.c
#include <stdio.h>
int main(int argc,char **argv)
{
printf("hello\n");
return 0;
}

[alert7@redhat62 alert7]$ gcc -o helo helo.c
[alert7@redhat62 alert7]$ readelf -h helo
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:�������������� 0x8048320
  Start of program headers:����������52 (bytes into file)
  Start of section headers:����������8848 (bytes into file)
  Flags:���������������������������� 0x0
  Size of this header:�������������� 52 (bytes)
  Size of program headers:���������� 32 (bytes)
  Number of program headers:�������� 6
  Size of section headers:���������� 40 (bytes)
  Number of section headers:�������� 29
  Section header string table index: 26
在这里我们看到程序的入口为0x8048320,可以看看是否为main函数。

[alert7@redhat62 alert7]$ gdb -q helo
(gdb) disass 0x8048320
Dump of assembler code for function _start:
0x8048320 <_start>:���� xor����%ebp,%ebp
0x8048322 <_start+2>:�� pop����%esi
0x8048323 <_start+3>:�� mov����%esp,%ecx
0x8048325 <_start+5>:�� and����$0xfffffff8,%esp
0x8048328 <_start+8>:�� push�� %eax
0x8048329 <_start+9>:�� push�� %esp
0x804832a <_start+10>:��push�� %edx
0x804832b <_start+11>:��push�� $0x804841c
0x8048330 <_start+16>:��push�� $0x8048298
0x8048335 <_start+21>:��push�� %ecx
0x8048336 <_start+22>:��push�� %esi
0x8048337 <_start+23>:��push�� $0x80483d0
0x804833c <_start+28>:��call�� 0x80482f8 <__libc_start_main>
0x8048341 <_start+33>:��hlt
0x8048342 <_start+34>:��nop
End of assembler dump.
呵呵,不是main吧,程序的入口是个_start例程。

再来看动态连接器的入口是多少
[alert7@redhat62 alert7]$ ldd helo
 ������ libc.so.6 => /lib/libc.so.6 (0x40018000)
 ������ /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
动态连接器ld-linux.so.2加载到进程地址空间0x40000000。

[alert7@redhat62 alert7]$ readelf -h /lib/ld-linux.so.2
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:������������������������������DYN (Shared object file)
  Machine:�������������������������� Intel 80386
  Version:�������������������������� 0x1
  Entry point address:�������������� 0x1990
  Start of program headers:����������52 (bytes into file)
  Start of section headers:����������328916 (bytes into file)
  Flags:���������������������������� 0x0
  Size of this header:�������������� 52 (bytes)
  Size of program headers:���������� 32 (bytes)
  Number of program headers:�������� 3
  Size of section headers:���������� 40 (bytes)
  Number of section headers:�������� 23
  Section header string table index: 20
共享object入口地址为0x1990。加上整个ld-linux.so.2被加载到进程地址空间0x40000000。
那么动态连接器的入口地址为0x1990+0x40000000=0x40001990。

用户空间执行的第一条指令地址就是0x40001990,既上面#define RTLD_START的开始。

Wednesday, May 09, 2007

GDB 查看指定地址的内容

GDB 查看指定地址的内容
 
 
使用examine命令(简写是x)来查看内存地址中的值。x命令的语法如下所示:
 
  x/<n/f/u> <addr>
 
  n、f、u是可选的参数。
 
  n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
  f 表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。
  u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。
 
  <addr>表示一个内存地址。
  n/f/u三个参数可以一起使用。例如:
 
  命令:x/3uh 0x54320 表示,从内存地址0x54320读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十六进制显示。
 
输出格式
  一般来说,GDB会根据变量的类型输出变量的值。但你也可以自定义GDB的输出的格式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要做到这样,你可以使用GDB的数据显示格式:
 
  x 按十六进制格式显示变量。
  d 按十进制格式显示变量。
  u 按十六进制格式显示无符号整型。
  o 按八进制格式显示变量。
  t 按二进制格式显示变量。
  a 按十六进制格式显示变量。
  c 按字符格式显示变量。
  f 按浮点数格式显示变量。
    (gdb) p i
    $21 = 101  
   
    (gdb) p/a i
    $22 = 0x65
   
    (gdb) p/c i
    $23 = 101 'e'
   
    (gdb) p/f i
    $24 = 1.41531145e-43
   
    (gdb) p/x i
    $25 = 0x65
   
    (gdb) p/t i
    $26 = 1100101