收录查询

[转]调试器和VC Debuger文章---汇总中

drwtsn32 调试windows程序:

原文地址:http://blog.csdn.net/OpenHero/archive/2006/04/28/694232.aspx

一直忙着调试代码,写服务器的代码,有时候真叫人郁闷,郁闷的是要高性能的保持程序继续运行n多天还没有事情,本来这也是程序员该做的事情,但是有时候一些长期积累下来的问题会造成程序指明性的打击~~~

在写这个程序的时候,本机调试代码的时候都没有遇到任何内存泄漏的问题,调试的时候都很清晰,但是到客户那里运行一天到两天就没名的挂掉,真让人郁闷

而且没有任何报错~~无奈,

打开了drwtsn32来检测程序的运行报错情况

drwtsn32是windows调试的利器,调试不常见错误的程序的时候,累积错误的时候,都可以用它.

在命令行窗口,输入drwtsn32

然后就可以看到drwtsn32的运行界面

然后设置"转存到符号表"

"转储全部线程上下文"会在出错的时候把程序运行的上下文都记录下来,如果只想知道出错那个地方的记录,就不要选这个,看起来方便很多

然后确定保存这些信息

相应的windows程序生成的时候,也得做一些改动,要生成map文件,这样好找到出问题的代码地方

vc设置:
1、配置属性-〉c/c++-〉常规-〉调试信息格式,  选择“程序数据库(/Zi)”
2、配置属性-〉c/c++-〉优化,选择“禁用(/Od)”
3、配置属性-〉连接器-〉调试-〉生成调试信息,选择 “是(/DEBUG)”
4、配置属性-〉连接器-〉调试-〉生成影射文件,选择 “是(/MAP)”
5、配置属性-〉连接器-〉调试-〉影射文件名,选择“$(OutDir)/$(ProjectName).map”
6、配置属性-〉连接器-〉调试-〉影射导出,选择“是(/MAPINFO:EXPORTS)”
7、配置属性-〉连接器-〉调试-〉影射行,选择“是(/MAPINFO:LINES)”
8、配置属性-〉连接器-〉高级->固定基址,选择“生成重定位节(/FIXED:NO)”

然后命令行运行drwtsn32

等待程序出错~~

程序出错以后会把log和一个dmp文件存储到故障转存 和日志文件路径 那个目录下面,这个目录可以自己设置.

 

在log信息里面有这样的信息

Microsoft (R) Windows 2000 (TM) Version 5.00 DrWtsn32
Copyright (C) 1985-1999 Microsoft Corp. All rights reserved.

 

发生应用程序意外错误:
        应用程序:  (pid=1248)
        时间: 2006-4-27 @ 09:49:29.613
        意外情况编号: c0000005 (访问侵犯)

*----> 系统信息 <----*
        计算机名: F6B2548C83754CF
        用户名: Administrator
        处理器数量: 1
        处理器类型: x86 Family 6 Model 8 Stepping 6

…………


        0041e6bb 8b45e0           mov     eax,[ebp+0xe0]         ss:00d9907e=????????
错误 ->0041e6be 8b500c           mov     edx,[eax+0xc]          ds:00c69ee6=????????
        0041e6c1 52               push    edx
        0041e6c2 ff1518af4300     call    dword ptr [0043af18]   ds:0043af18=7c177c00

…………………………

很多信息

然后对应的去找你vc 生成的map文件的地方~~

找到代码出现那样情况的地方~~

~~~然后让你的程序VC调试,反汇编,找到对应的地方~~

 

ps:这只是一种最简单的方法:)

更多的方法,看《windows程序调试》
==============================================================

1.调试标记

    适用预处理#define定义一个或多个调试标记,在代码中把调试部分使用#ifdef 和#endif 进行管理。当程序最终调试完成后,只需要使用#undef标记,调试代码就会消失。常用的调试标记为DEBUG, 语句序列:

   #define DEBUG

   #ifdef DEBUG

      调试代码

  #endif

2.运行期间调试标记

   在程序运行期间打开和关闭调试标记。通过设置一个调试bool标记可以实现。这对命令行运行的程序更为方便。

  例如下面代码

    #include<iostream>

  #include <string>

  using namespace std;

 bool debug =false;

int main(int argc,char*argv[])

{

   for(int i=0;i<argc;i++)

      if(string(argv[i])==“--debug=on“)

                debug = true;

     bool go=true;

   while(go)

{

   if(debug)

    {

    调试代码

     }else {}

}

}

 

3.把变量和表达式转换成字符串

 可是使用字符串运算符来实现转换输出定义

#define PR(x) cout<<#x”=”<<x<<'\n'

4.c语言的assert()

    该宏在<assert>中,,当使用assert时候,给他个参数,即一个判读为真的表达式。预处理器产生测试该断言的代码,如果断言不为真,则发出一个错误信息告诉断言是什么以及它失败一会,程序会终止。

#include< assert>

using namsapce std;

int main()

{

   int i=100;

 assert(i!=100); //Fails

}

当调试完毕后在#include<assert>前加入#define NDEBUG即可消除红产生的代码

}
=============================================================

了解调试,首先要知道"断点"这个概念.断点就是程序运行中可能会中断的地方,方便开发者在程序运行的过程中查看程序当前的运行状态,比如变量的值,函数的返回值等等.究竟怎么使用断点呢?
1.F9在当前光标所在的行下断点,如果当前行已经有断点,则取消断点.
  F5调试状态运行程序,程序执行到有断点的地方会停下来.
  F10单步执行程序.
  CTRL+F10运行到光标所在行.
  F11和F10的区别是,如果当前执行语句是函数调用,则会进入函数里面.
  SHIFT+F11跳出当前所在函数.

特别说明:
  a.有的地方不能下断点.比如空行,基本类型定义语句(不初始化),等等非执行语句.比如
int i;  // 此行不能下断点
int j=0;// 这里可以下
CStringstr;// 这里可以下
intk = Max(i,j);// 这里可以下
  b.不是所有断点都会(断).比如下断点的语句在程序里面没有被执行.
  c.此外,ALT+F9还可以下条件断点,不过这个不常用,有兴趣的可以自己研究:)

2.如何调试Release版的程序?
  有些程序在debug下运行的很好,但在release下却总是失败,默认情况下release是不能调试的. 怎么解决呢?
  其实"debug"和"release"都只是一个代号而已,各自对应一组不同的编译选项.
  在release的默认设置下,ALT+F7 ,调出工程设置对话框,切换到link选项卡,勾选"Generate debug info",然后再切换到C/C++选项卡,在"Optimizations"里面选"Disable(Debug)",在下面的"Debug info"里面选"Program Database for Edit and Continue".然后点OK保存设置,重新编译程序,下断点即可.
3.上面两点已经能应付很多种情况了,但是有时候即使在debug下也不能下断点,我以前也曾经遇到过这种问题,一个调了4层的dll,就是不能下断点.其实还除了上面的方法,还有一种最底层的实现技术:使用汇编.
  细心的人可能会发现,有时候在调试状态下当程序出现异常的时候,光标会指向汇编代码,而这个代码就是int 3,这其实是一个中断.
  在你的代码的任意地方加上
  __asm{
  int 3
};
  用debug编译,然后直接运行程序.当程序执行到上述代码的时候,就会出现一个框,告诉你说一大堆信息,说程序错了,下面有"调试"、"终止"、"忽略",不用理,点调试就可以进入跟 踪了。这种方法我还没有失 手过:)
==============================================================
原文地址:http://blog.csdn.net/yhb72/archive/2005/12/02/541684.aspx

1.break FUNCTION
在某个函数上设置断点。函数重载时,有可能同时在几个重载的函数上设置了断点

break +OFFSET
break -OFFSET
在当前程序运行到的前几行或后几行设置断点

break LINENUM
在行号为LINENUM的行上设置断点

break FILENAME:LINENUM
在文件名为FILENAME的原文件的第LINENUM行设置断点

break FILENAME:FUNCTION
在文件名为FILENAME的FUNCTION函数上设置断点
当你的多个文件中可能含有相同的函数名时必须给出文件名。

break *ADDRESS
在地址ADDRESS上设置断点,这个命令允许你在没有调试信息的程序中设置断点

break
当break命令不包含任何参数时,break命令在当前执行到的程序运行栈中的
下一条指令上设置一个断点。除了栈底以外,这个命令使程序在一旦从当前
函数返回时停止。相似的命令是finish,但finish并不设置断点。这一点在
循环语句中很有用。gdb在恢复执行时,至少执行一条指令。

break ... if COND
这个命令设置一个条件断点,条件由COND指定;在gdb每次执行到此断点时
COND都被计算当COND的值为非零时,程序在断点处停止

ignore BNUM COUNT'
设置第BNUM号断点的被忽略的次数为'COUNT',即断点BNUM再执行到第COUNT+1
次时程序停止

tbreak ARGS 或者简写为 tb
设置断点为只有效一次。ARGS的使用同break中的参量的使用

hbreak ARGS
设置一个由硬件支持的断点。这个命令的主要目的是用于对EPROM/ROM程序的调试
因为这条命令可以在不改变代码的情况下设置断点。这可以同SPARCLite DSU一起
使用。当程序访问某些变量和代码时,DSU将设置“陷井”。注意:你只能一次使用
一个断点,在新设置断点时,先删除原断点

thbreak ARGS'
设置只有一次作用的硬件支持断点

rbreak REGEX
在所有满足表达式REGEX的函数上设置断点。这个命令在所有相匹配的函数上设置无
条件断点,当这个命令完成时显示所有被设置的断点信息。这个命令设置的断点和
break命令设置的没有什么不同。当调试C++程序时这个命令在重载函数上设置断点时
非常有用。

info breakpoints [N]
info break [N]
info watchpoints [N]
显示所有的断点和观察点的设置表,有下列一些列
*Breakpoint Numbers*----断点号
*Type*----断点类型(断点或是观察点)
*Disposition*---显示断点的状态
*Enabled or Disabled*---使能或不使能。'y'表示使能,'n'表示不使能。
*Address*----地址,断点在你程序中的地址(内存地址)
*What*---地址,断点在你程序中的行号。
如果断点是条件断点,此命令还显示断点所需要的条件。
带参数N的'info break'命令只显示由N指定的断点的信息。
此命令还显示断点的运行信息(被执行过几次),这个功能在使用'ignore'
命令时很有用。你可以'ignore'一个断点许多次。使用这个命令可以查看断点
被执行了多少次。这样可以更快的找到错误。

maint info breakpoints
显示所有的断点,无论是你设置的还是gdb自动设置的。
断点的含义:
breakpoint:断点,普通断点
watchpoint:普通观察点
longjmp:内部断点,用于处理'longjmp'调用
longjmp resume:内部断点,设置在'longjmp'调用的目标上
until:'until'命令所使用的内部断点
finish:finish'命令所使用的内部断点

2.watch EXPR
为表达式(变量)expr设置一个观察点。一量表达式值有变化时,马上停住程序。
这个命令使用EXPR作为表达式设置一个观察点。GDB将把表达式加入到程序中并监
视程序的运行,当表达式的值被改变时GDB就使程序停止。这个也可以被用在SPARClite
DSU提供的新的自陷工具中。当程序存取某个地址或某条指令时(这个地址在调试寄
存器中指定),DSU将产生自陷。对于数据地址DSU支持'watch'命令,然而硬件断点寄
存器只能存储两个断点地址,而且断点的类型必须相同。就是两个'rwatch'型断点
或是两个'awatch'型断点。

rwatch EXPR'
设置一个观察点,当EXPR被程序读时,程序被暂停。

awatch EXPR'
设置一个观察点,当EXPR被读出然后被写入时程序被暂停。

info watchpoints
在多线程的程序中,观察点的作用很有限,GDB只能观察在一个线程中的表达式的值
如果你确信表达式只被当前线程所存取,那么使用观察点才有效。GDB不能注意一个
非当前线程对表达式值的改变。

rwatch <expr>
当表达式(变量)expr被读时,停住程序。

awatch <expr>
当表达式(变量)的值被读或被写时,停住程序。

info watchpoints
列出当前所设置了的所有观察点。


3.catch EXCEPTIONS
使用这个命令在一个被激活的异常处理句柄中设置断点。EXCEPTIONS是一个你要抓住
的异常。你一样可以使用'info catch'命令来列出活跃的异常处理句柄。

GDB中对于异常处理由以下情况不能处理:
* 如果你使用一个交互的函数,当函数运行结束时,GDB将象普通情况一样把控制返
回给你。如果在调用中发生了异常,这个函数将继续运行直到遇到一个断点,一个信号
或是退出运行。
* 你不能手工产生一个异常( 即异常只能由程序运行中产生 )
* 你不能手工设置一个异常处理句柄。
有时'catch'命令不一定是调试异常处理的最好的方法。如果你需要知道异常产生的
确切位置,最好在异常处理句柄被调用以前设置一个断点,这样你可以检查栈的内容。
如果你在一个异常处理句柄上设置断点,那么你就不容易知道异常发生的位置和原因。
要仅仅只在异常处理句柄被唤醒之前设置断点,你必须了解一些语言的实现细节。

3.cont N
第N次经过该断点时才停止程序运行

4.enable 断点编号
恢复暂时失活的断点,要恢复多个编号的断点,可用空格将编号分开

5.disable 断点编号
使断点失效,但是断点还在

6.delete 断点编号或者表达式
删除某断点

7.clear 断点所在行号
清除某断点

8.查看断点列表
info break

9.watch counter>15
当counter>15的时候程序终止

10.当程序崩溃的时候linux会生成一个core文件,可以用
gdb a.out core
where
查看导致崩溃的原因

11.continue
恢复程序运行,直到遇到下一个断点

12.run
程序开始运行,直到遇到断点

13.step
执行一行代码

14.next
和s不同的是他不跟踪到代码的内部,一步一步执行代码

15.直接回车为执行上一个命令

16.print 变量
打印某一变量的值

17.display 变量
每次运行到断点就显示变量的值,用于观察变量的变化

18.set 变量=
在程序执行中重新设置某变量的值

19.printf "%2.2s\n",(char*)0x120100fa0
结果打印出:He

20. 设置gdb的列宽,以下为将屏幕设置为70列
set width 70

21. info args 列出你程序所接受的命令行参数
info registers列出寄存器的状态
info breakpoint列出在程序中设的断点
要获得详细的关于info的信息用help info.

22. set
这个命令用来为你的程序设置一个运行环境(使用一个表达式)。
set prompt $把gdb的提示符设为$.
set args 可指定运行时参数。(如:set args 10 20 30 40 50)
show args 命令可以查看设置好的运行参数。

23. show
show命令用来显示gdb自身的状态。
使用'set'命令可以改变绝大多数由'show'显示的信息
使用show radix命令来显示基数
用不带任何参变量的'set'命令可以显示所有可以设置的变量的值
有三个变量是不可以用'set'命令来设置的:
show version显示gdb的版本号
show copying显示版权信息
show warranty显示担保信息
===========================================================
DLL下断点调试文章:
http://blog.csdn.net/haoel/archive/2003/07/02/2879.aspx

===========================================================
http://blog.csdn.net/ForNormandy/archive/2004/08.aspx         也有1篇调试文章
===========================================================
VC中内存泄露的诊断:

http://blog.csdn.net/wangle79/archive/2005/12/18/555689.aspx

==========================================================
保护模式概述:

原文地址:http://blog.csdn.net/efiish/archive/2006/03/30/644766.aspx

我记得当我第一次学习保护模式的时候,我刚刚自学完了汇编语言,于是我就有了一个疯狂的念头——自学保护模式。我买了一本包括保护模式示例的80286汇编语言教材,然后就开始学习了。没过几个小时,我意识到我买的书里没有任何有用的示例,因为书里的例子是介绍如何EPROM CHIPS编程的。因此我将那个误导我买此书的海报痛打了一顿。

直到现在,很多年以后,我唯一发现的关于任务切换的示例还是那么的费解和缺少文档说明,虽然我已经无法指出它了。借助IBM技术参考手册和我的那本80286教材,我坐下来尝试着理解保护模式。在3天里花费了大约40个小时之后,我最后从IBM技术参考手册中复制出来一些源代码,能够进入保护模式了,然后我退回了DOS。

自从那时起,我学习了很多关于保护模式的知识,以及CPU内部是如何处理它的。我发现CPU内有一系列应用程序不可访问的隐藏寄存器。我也学习了这些寄存器是如何被装载的,他们在内存管理中的角色,以及更重要的,他们的精确内容。虽然这些寄存器对于应用程序是不可访问的,理解他们在内存管理中的角色的知识也可以被应用到实际编程中。在编程中使用这些知识,可以使用更少的数据,更少的代码,更快的速度来达到我们想要的结果。

保护模式基础

从一个应用的观点来看,保护模式和实模式没有什么太大的区别。都是使用内存段,中断和设备驱动程序去处理硬件。但是有一些细微的区别,使得将DOS应用移植到保护模式下并不是一件琐碎的事情(就是说比较麻烦?)。在实模式中,内存段通过与段寄存器结合起来,使用一种内在机制自动处理。这些段寄存器中的内容构成了CPU当前地址总线上的部分物理地址(参看图1a)。

这些物理地址通过段寄存器乘以16得到,然后再加上一个16bit的偏移量。使用16bit偏移量也就暗示了CPU使用的段最大尺寸为64KB。一些程序员通过增加段寄存器中的内容来解决64K段的尺寸限制。他们的程序通过将指向64K段的指针递增16字节的方式来一个段紧接着一个段的方式访问内存。任何在保护模式下使用这种技术访问内存的程序都会产生一个异常错误(CPU产生的异常中断),因为在保护模式下,段寄存器的使用方法是不同的。

在保护模式下,内存段被一系列的表定义着(这些表成为描述符表),段寄存器被用来保存指向这些表的指针。每一个表项有8个字节宽,因此在段寄存器中的数值被定义为8的整数倍(如08h,10h,18h等等)。段寄存器中的低3位被定义了,但是由于一些简单的原因,我们说任何加载了内容不是8的倍数的段寄存器的程序,都会引起一个保护错误。

有两种表格被用来定义内存段:全局描述符表(Global Descriptor Table:GDT),和局部描述符表(Local Descriptor Table: LDT)。

GDT中保存了所有应用程序都可以访问到的段信息,LDT中保存着为某一个特定的任务或者程序指定的段信息。如前所述,段寄存器在保护模式下不够成物理地址的任何一部分,而是被用作指向GDT或者LDT的表项的指针(见图 1b)。每一次段寄存器被加载时,基地址从表项中被取出,然后保存在一个内部的、程序员不可见的被称为“段描述符缓冲(segment descriptor cache)”的寄存器中。出现在CPU地址总线上的物理地址通过将描述符缓冲中的基址加上32位的偏移量而构成。

描述符缓冲寄存器不论是在实模式,或者是在保护模式下,CPU将每一个段的基地址保存在一些叫做描述符缓冲寄存器的隐藏寄存器中。每次CPU加载一个段寄存器,段基地址、段大小限制和访问属性(访问权限)信息也被加载,(或者被缓冲)到这些隐藏的寄存器中。为了提高性能,CPU让之后的内存引用都通过描述符缓冲寄存器来计算,以替代通过查找描述符表来计算物理地址。理解这些隐藏的寄存器的角色和作用,对于采用新的更先进的编程技术和采用未公开的LOADALL指令是非常重要的。图2a展示了80286上描述符缓冲的结构,图2b展示了80386和80486上的描述符缓冲的结构。Figure 2 (a) 80286 Descriptor Cache Register
[47..32] 31 [30..29] 28 [27..25] 24 [23..00]
16-bit Limit P DPL S Type A 24-bit base address
Figure 2 (b) 80386/80486 Descriptor Cache Register
[31..24] 23 [22..21] 20 [19..17] 16 15 14 [13..00]
0 P DPL S Type A 0 D 0

[63..32]
32-bit Physical Address

[95..64]
32-bit Limit

在上电时,描述符缓冲寄存器使用固定的缺省值加载,CPU处于实模式,所有的段都被标记为可读/写的数据段,包括代码段(CS)。依照Intel的说法,每一次CPU在实模式下load一个段寄存器时,基地址将是段值的16倍(什么意思?),并且访问权限和尺寸限制属性都是固定的“实模式兼容”值。 这不是真的。实际上,只有段描述符缓冲访问权限在段寄存器每次加载时使用固定值加载——当遇到一个far jump的时候,也是如此。在实模式下加载任何其他的段寄存器不会改变存储在描述符缓冲寄存器中的访问权限或者段尺寸限制属性。对于这些段而言,访问权限和段尺寸大小属性都取决与任何先前的设置(查看图3)。因此,在80386的实模式下是有可能拥有一个4GB,只读的数据段的。但是Intel将不会承认,或者支持这种操作模式。每次CPU加载一个段寄存器时,保护模式和实模式是不同的。保护模式会加载全部的描述符缓冲寄存器,不继承原先的数值。CPU从描述符标中直接加载描述符缓冲。CPU通过测试描述符表中的访问权限来检查一个段的合法性,非法值将会产生一个异常。任何将代码段加载到一个可读/写的数据段,都水产生一个保护错误。同样,任何尝试将数据段寄存器加载到一个可执行段的尝试都会产生一个异常。(保护错误和异常一样吗?)如果描述符表项通过了所有的检测,CPU会非常严格的执行这些保护规则,然后CPU加载描述符缓冲寄存器。Figure 3 -- Descriptor Cache Contents (Real Mode)

另一个将实模式应用程序移植到保护模式下的关键点是中断。在实模式下,指向中断处理例程的双字长指针从物理地址的0开始排列(对于386:除非IDTR被修改了,要不然也是这样)。

图4a举例说明了实模式下的中断服务例程寻址方式。当产生或调用一个中断时,CPU在中断向量表中查看中断服务例程(ISR)的地址。当CPU将各种标志压到栈中之后,它就远程调用(far call)表中的地址。这些压到堆栈中的信息对于由软件、硬件、CPU产生的中断而言都是一样的。

Figure 4(a) -- Interrupt service addressing in Real Mode

Fig 4(b) Interrupt service addressing in Protected Mode

对于保护模式,压入栈中的信息是可以变化的,就像中断向量的基地址和中断表的大小可以改变一样。保护模式下的中断向量查找机制同实模式下也有很大不同。

图4b图示了在保护模式下中断是如何被调用的。

当一个中断产生后,CPU将中断号同存储在中断描述符寄存器中的中断描述符表的大小进行比较。如果中断号没有超过IDT的大小,则这个中断被视为可调用的,

然后就从描述符缓冲中取出IDT的基地址;然后就可以从IDT中获取中断服务程序的保护模式下地址。这个中断服务例程的地址不是一个物理地址,而是一个保护模式下的段地址。要使用IDT中指明的段选择信息,CPU必须将同样的限制检查过程对GDT重新进行一次,以计算中断处理例程的物理地址。一旦物理地址计算出来了,CPU就将FLAGS,SEGMENT(选择器),OFFSET和可能的错误码压入栈中,然后再跳转到中断服务例程处。

对于软件和硬件的中断服务例程本身,在实模式和保护模式下没有太大的区别。但是针对CPU产生的中断和错误的中断处理例程必定是不同的。
Table 1 -- Exceptions and Interrupts
Description Int # Type Return Addr points to faulting instruction Error Code This interrupt first appeared in this CPU
Division by 0Debug ExceptionNMIBreakpointOverflowBoundsInvalid OP CodeDevice not availableDouble FaultCopr. segment overrunInvalid TSSSegment not presentStack faultGeneral ProtectionPage faultFloating point errorAlignment checkMachine checkSoftware interrupts 012345678910111213141617180-255 Fault*1*2TrapTrapFaultFaultFaultAbortFaultFaultFaultFaultFaultFaultFaultFaultAbortTrap Yes*1NoNoYesYesYesYesNoYesYesYesYesYesYesYesYesNoNo NoNoNoNoNoNoNoNoYesNoYesYesYesYesYesNoYesYesNo 808680868086808680868018680186801868028680286 *380286802868028680286803868038680486Pentium *4All
*1 在386级的CPU上,debug异常既可以当作trap,也可当作faults。一个trap是通过在flags image中设置TF(Trap Flag),或者使用debug寄存器来产生一个数据断点来引起的,在这种情况下,返回地址是紧跟着trap的下一条指令。Faults是通过为代码执行断点设置debug寄存器产生的。对于所有的faults,返回地址指向fault的指令本身。
*2 Non-maskable.
*3 从80486中去掉了,在之后的CPU中不再产生13号异常。
*4 同具体型号相关,对于未来的处理器,处理方法可能不同,也可能去掉。

CPU产生3种类型的中断:traps,faults,和aborts。对于不同类型,栈内容也是变化的,例如错误码,可能被压入栈中,也可能不压入栈中,这取决于CPU产生的中断的类型。Traps从来不会将错误码压栈;faults通常会将错误码压栈(就是说有时候也会不压);aborts总会将错误码压栈。Traps非常相似,并且也包括了软件的中断。这类中断的命名非常恰当,正如对当前的一个时间被“圈中(trapping)”了。

在trap之前,CPU是不会知道这个事件发生了的。因此,在向中断发送信号之前一定要首先trap这个事件。所以ISR的返回地址指向紧跟着这个事件的指令。Traps包括:被0除,数据断点,INT03。Faults是因为某些错误并需要修改的时候发生了才产生的。CPU会立即知道错误发生了,并且信号通知给中断产生机制。这一类ISR的主要意图,是修改问题然后从刚才发生问题的地方重新运行原指令。正因为如此,此类ISR的返回地址指向发生错误的指令——这样就可以使这个指令被重新执行。Aborts是最严重的中断类型,被认为是不能够重新开始的。这时错误代码被压栈,但通常都是0。

CPU的栈段,和状态机,很可能会处于不确定的状态,因此重新执行一个abort可能会导致不可预料的结果。Table1是对保护模式下CPU产生的中断的分组列表。在大多数情况下,CPU也会在实模式下产生同样的中断,但是永远不会有错误代码压入栈中。

我曾经奇怪为什么BIOS不能工作在保护模式下。那时,我想编写模式无关的代码也许是比较容易的:只要不进行任何的Far Jump,或者Far Call就可以。但是要做到这点却不是容易的事情。

为了避免使用far jump或者far call,ISR必须将压入栈中的任何错误代码都删除(为什么必须?)。这就是不可能的开始(要改)。由于错误码只有在保护模式下才会被放入栈中,因此在移去错误码之前,我们必须判断是在实模式下还是在保护模式下。要做到这一点,我们必须访问机器状态字MSW,或者系统寄存器CR0。

访问MSW可以在任何优先级中进行,但是访问CR0只能在最高优先级(level 0)中才能执行。如果用户程序运行于level之外的其他优先级,我们也许就没有办法访问这些寄存器。

在调用中断服务例程之前,我们可以通过特定的调用门切换自己的优先级。如果我们使用SMSW指令,这就不需要了。但是即使这个问题解决了,让我们想象一个程序在任何的段寄存器中保留有实模式的数值。如果ISR将这些寄存器的值压栈,并稍后在推栈,这个推栈指令将会导致CPU在GDT中查找一个selector(段选择器?)。这时使用一个实模式值将会导致一个保护错误(protection error)。因此在保护模式中使用BIOS例程几乎是不可能的。但是如果有一系列所有程序和操作系统都需要遵守的规则(或标准)时,也许就可以在保护模式中运行BIOS了。
进入保护模式
我们的目标是进入保护模式,然后离开保护模式返回DOS。286没有退出保护模式的内部机制:一旦你进入了保护模式,你就只能一直呆在那里了。

IBM认识到这一点就实现了一种解决方案可以通过reset CPU让286从保护模式返回。286的power-on状态是处于实模式的,因此简单的reset CPU就可以让CPU返回实模式。

但这导致一个小问题,就是CPU不能继续运行之前的程序了。当reset后,CPU开始运行保存在内存顶部的指令,即BIOS指令代码。由于没有一个协议告诉BIOS我们为了退出保护模式而reset了CPU,因此BIOS没有办法将控制权返回给用户程序。

IBM实现了一种非常简单的协议,将一个代码写入CMOS RAM(CMOS),这样BIOS可以通过检查这个代码决定去做什么。当BIOS从reset向量开始执行后,它立刻在CMOS中检查这个代码以判别是否CPU是为了退出保护模式所以才被reset的。依靠这个保存在CMOS中的代码,BIOS可以将控制权返回给用户程序,使之继续执行。

Reset CPU不会没有副作用;所有CPU寄存器的内容都被破坏了,而有时候可编程中断控制器(PIC)中的中断掩码(interrupt mask)也被BIOS重新设置了(取决于系统shutdown的类型)。因此在进入保护模式之前保存PIC的掩码、栈指针和返回地址是应用程序自己要做的事情。

PIC掩码和栈指针必须被保存在用户数据段中,但是返回地址必须存储在一个预定义在BIOS数据段的固定位置——40:67h。(这我们就知道40:67h这个地址中保存着应用程序从保护模式返回实模式时的返回地址。)

然后,我们设置CMOS中的代码,告诉BIOS我们将从保护模式退出并且返回到用户程序。这个很容易实现——向2个CMOS I/O端口写入数值即可。当CPU被reset后,BIOS检查这个CMOS代码之后,就会清楚这个CMOS代码,这样之后的reset就不会导致预料外的结果了。设置了CMOS中的代码之后,程序必须建立GDT。(查阅相应的Intel Programmer’s reference manual中关于GDT的描述 )。由于访问权限、尺寸限制等是静态值,因此可以通过编译器来填写。但是每一个段的基地址只有在运行中才能知道;因此程序必须将他们填写入GDT。我们的程序将会建立一个包含这些代码、数据和栈段地址的GDT(应该是这个程序自己本身的)。最后一个GDT项将指向1M以示示例(不明白)。


访问位于1M的内存可不像建立和使用一个GDT项那么简单。8086具有在超过1MB的空间上寻址64K(减去16字节)的潜能,但是它缺少第21根地址线(所以不行:-)。8086仅有20根地址线(A00-A19),由于缺少A20,任何尝试1M以上地址的尝试都会绕回到地址0的位置。286具有24bit的寻址能力,因此在这方面与8086有所不同。

任何尝试对超过1M地址(FFFF:0010-FFFF:FFFF)的访问都会声明使用A20,所以不会转回到地址0处。任何使用内存绕回特性的8086程序,将会在286上运行失败。

作为这个兼容性问题的解决方案,IBM通过计算机上的某个芯片的可编程输出引脚增加了一个CPU A20输出。这个CPU A20信号实际上是一个AND门,这个AND门连接到地址总线上。

基于CPU A20的输入,AND一个外部可编程source,地址总线A20就被assertred了。由于可编程控制器下有一些有效的引脚可以被设置为高电平、低电平或者锁定,因此当引脚被设置为高电平,当CPU声明使用A20时,AND门的输入就会变高;

当引脚输出被设为低电平时,A20在地址总线上就总是低电平——即忽略了CPU A20的状态。
这样通过控制A20是否在地址总线上被声明使用,285级别的机器就可以模拟8086处理器上的内存绕回(memory wrapping)特性了。

注意到只有地址线上的A20是被通过门控制的。因此,当没有使能A20门的输入时,CPU只能寻址偶数MB的内存,例如:0-1M,2-3M,4-5M等等。实际上,作为将地址总线A20置低电平的结果,这些内存块的内容同1-2M,3-4M,5-6M等的范围内的内容对应的都是相同的。

为了使能所有的24位的寻址能力,必须向键盘控制器发送一个命令。键盘控制器将会将他的某个输出引脚的输出置高电平,作为A20门的输入。一旦设置成功之后,内存将不会再被绕回(memory wrapping),这样我们就可以寻址整个286的16M内存,或者是寻址80386级别机器的所有4G内存了。

剩下的为了进入保护模式要做的事情就是改变CPU的状态到保护模式,然后执行一个jump指令以清楚CPU的预读取指令队列(在Pentium上就不必要了)。

下表总结了在286下进入(还能返回到实模式的)保护模式所需要的步骤。

l 保存8259 PIC掩码到程序数据段。
l 保存SS:SP到程序数据段。
l 保存从保护模式返回的返回地址到40:67。
l 设置CMOS中的shutdown代码,以告诉BIOS当CPU reset以后我们还要返回原先实模式的用户程序。
l 建立GDT,使能地址总线的A20
l 通过CPU的机器状态字(MSW)使能保护模式,执行一个JUMP,以清楚CPU的预读取指令队列。
以上6部的顺序不分前后。

由于386可以不通过复位 CPU既可以退出保护模式返回实模式,因此在386或者486上进入保护模式所需要的步骤要比286下简单的多。为了兼容的目的,所有386BIOS将会识别定义在286级别机器上的CPU shutdown协议,但是遵循这个协议是没有必要的。

在386上退出保护模式,程序只需要简单的清除CPU控制寄存器上的一个位即可,不需要保存PIC 掩码,SS:SP,返回地址和设置CMOS 代码。因此在386上进入保护模式的步骤就简化为:
l 建立GDT。
l 在地址总线上使能A20。
l 设置CPU控制寄存器(CR0或MSW)以使能保护模式
l 执行一个跳转指令以清空CPU预取指令队列。

这些必须的步骤中,只有建立GDT是不同的。在386中基地址扩展为32位,大小限制扩展到20位,并引入了2个新的控制属性。列表1列举了进入保护模式需要的所有辅助子程序。
退出保护模式
同进入保护模式一样,退出保护模式在286和386的机器上也是不同的。386仅仅是简单的清除CPU控制寄存器CR0上的一位,而286必须reset CPU。


复位CPU也是要花费时间的,大约需要几百个时钟周期(不至于到上千个),才能够使CPU从保护模式退回到实模式运行用户程序。IBM最初采用键盘控制器连接到CPU RESET线的一个输出引脚上。为了产生正确的命令,KBC需要锁住CPU的RESET线。这种方法是可行的,但是非常慢。

很多新一代286芯片组具有一个FAST RESET特性。这些芯片组通过向一个I/O端口写入简单信息将RESET信号线锁住。所以如果允许的话,FAST RESET是返回实模式更好的方法。

不过这里还有第三种方法,虽然很晦涩,但是确实是一种不使用KBC或者FAST RESET的一种复位CPU的有效方法(见efficient method for resetting the CPU)。这种方法很精巧,比使用KBC快,并且可以在386上运行而不必复位CPU!在退出保护模式返回实模式的各种方法中,这个方法应该是最精巧的——因为它可以工作在286和386两种CPU上,并且还很快。

列表2给出了使用KBC方式和刚才提高的这种高效方式的必须的代码。

使用KBC去复位CPU是很正规的做法,但是为了理解这个精巧的技术,需要一些说明。回忆一下我们在中断那部分的讨论,CPU通过中断描述符缓冲寄存器中的limit域(即最多有多少项)来检查中断号。如果这个测试通过了,那么下一步就是开始中断处理了。但是如果测试失败,那么CPU就会产生一个DOUBLE FAULT(INT08)信号。例如,让我们假定IDTR的limit域为80h。我们的IDT将会提供16个中断:00-15。如果中断16或者更高中断号的中断产生,CPU就会产生一个DOUBLE FAULT,这是由于在中断调用的初期产生了一个错误(fault)。现在,假定IDTR的limit域为0,这就禁止对所有的中断服务。任何中断的产生都会导致DOUBLE FAULT。但是由于limit域小于40h,因此DOUBLE FAULT自己将会产生一个错误(fault)。这最终会导致一个TRIPLE FAULT,然后CPU会进入shutdown循环。这个shutdown循环不会复位CPU,就像shutdown循环被认为是总选循环一样。外部硬件设备会跟随CPU一起去识别这个shutdown循环信号。一旦这个信号被识别了,外部硬件就会锁定CPU的RESET输入。因此,我们要引起RESET信号所需要做的唯一的事情就是设置IDTR的限制为0(IDTR.LIMIT=0),然后产生一个中断(什么中断都行)。

为了让这个方法看起来更优雅一些,我们不使用INT来中断CPU,我们产生一个无效的操作数。我们的操作数经过精心选择,肯定不会出现在286上,但是却在386上存在。挑选这个操作数方法是为了这个目的:MOV CRO,EAX。这将在286上产生一个期望的无效操作数异常,但是确实在386上退出保护模式的指令序列中的第一个。这样将会在286上产生RESET,而在386上可以没有影响的继续运行下去以体面的退出保护模式。

退出286和386的保护模式的步骤同进入保护模式的反向步骤非常相似,

在286上,你必须:
l 复位CPU进入实模式
l 用实模式兼容的数值加载段寄存器
l 恢复SS:SP
l 限制地址线上的A20(关闭A20门)
l 恢复PIC掩码
在386上,步骤会简单一些:
l 使用实模式兼容的数值加载段寄存器
l 复位CR0的PE位(Protection Enable bit)
l 使用实模式数值加载段寄存器
l 限制地址线上的A20(关闭A20门)
(列表3把扩了退出保护模式后恢复机器状态所需要的子程序)

注意:在386下从保护模式退出到实模式时需要加载两次段寄存器。

第一次加载段寄存器是为了确保实模式兼容的值保存在隐藏的描述符缓冲寄存器(们)中,
由于从保护模式退回到实模式时描述符缓冲寄存器会继承访问属性,段大小限制(就是说返回以后,这些数值还在,不变)。
第二次加载段寄存器是为了使用实模式段数值定义段寄存器。

现在我们拥有进入和退出保护模式所需要的所有工具和理论,我们可以通过写一个进入保护模式的程序来应用这些知识,从扩展内存中移动一个数据块,然后再退出保护模式——返回到DOS。

列表4展示了一个从内存1M处复制1KB数据到我们程序的数据段的程序的基本步骤。
小结
运行在实模式和运行在保护模式下的应用软件并没有太多的不同。在两种模式下我们都是用内存段、中断和设备驱动去支持硬件。无论在实模式或者保护模式中,一系列称为描述符缓冲寄存器的用户不可访问的寄存器们————在内存段和内存管理中扮演了非常重要的角色。

描述符缓冲寄存器保存着定义段基地址、段大小限制和段访问权限属性的信息,并被用于所用的内存引用场合——而忽略在段寄存器中的数值。

进入和退出保护模式需要采用适当的机制即可:进入保护模式需要保存退出保护模式时会用到的当前机器状态。返回实模式的机制依赖于CPU的类型:286需要复位CPU,386可以在程序的控制下进入实模式。

为了对我们的关于CPU内部操作的知识加以应用,我们可以编写一段对应不同CPU类型的退出保护模式源程序来试试。


下面是本文中提到的参考源代码
ftp://ftp.x86.org/source/pmbasics/tspec_a1.asm
ftp://ftp.x86.org/source/pmbasics/tspec_a1.l1
ftp://ftp.x86.org/source/pmbasics/tspec_a1.l2
ftp://ftp.x86.org/source/pmbasics/tspec_a1.l3
http://www.x86.org/ftp/source/pmbasics/tspec_a1.l4
下载所有的源代码:
http://www.x86.org/ftp/dloads/pmbasics.zip

========================================================
某公司面视题目:

原文地址:http://blog.csdn.net/ForNormandy/archive/2004/08/22/81396.aspx

以下是我在某个公司面试时遇到的笔试题目。这些题目都是我在一个叫做臭臭泥的人网站上找到的,让人惊奇的是,不知道这个臭臭泥是否与我面试的那家公司有什么关系没有,所出的题目一模一样:

一、请填写BOOL , float, 指针变量 与“零值”比较的 if 语句。(10分)
提示:这里“零值”可以是0, 0.0 , FALSE或者“空指针”。例如 int 变量 n 与“零值”比较的 if 语句为:
if ( n == 0 )
if ( n != 0 )
以此类推。

请写出 BOOL flag 与“零值”比较的 if 语句:
请写出 float x 与“零值”比较的 if 语句:
请写出 char *p 与“零值”比较的 if 语句:

二、以下为Windows NT下的32位C++程序,请计算sizeof的值(10分)

char str[] = "Hello" ;
char *p = str ;
int n = 10; 请计算 sizeof (str ) = sizeof ( p ) = sizeof ( n ) =
void Func ( char str[100]) {请计算 sizeof( str ) = }
void *p = malloc( 100 ); 请计算 sizeof ( p ) =

三、简答题(25分)

1、头文件中的 ifndef/define/endif 干什么用?

 

2、#include 和 #include “filename.h” 有什么区别?

 

3、const 有什么用途?(请至少说明两种)

 

4、在C++ 程序中调用被 C编译器编译后的函数,为什么要加 extern “C”声明?

 

5、请简述以下两个for循环的优缺点

// 第一个
for (i=0; i
{
if (condition)
DoSomething();
else
DoOtherthing();
}
// 第二个
if (condition)
{ for (i=0; i
DoSomething();
}
else
{
for (i=0; i DoOtherthing();
}
优点: 缺点: 优点: 缺点:

四、有关内存的思考题(20分)

void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
} 请问运行Test函数会有什么样的结果? 答:

char *GetMemory(void)
{
char p[] = "hello world"; return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
} 请问运行Test函数会有什么样的结果? 答:

void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100); strcpy(str, "hello");
printf(str);
} 请问运行Test函数会有什么样的结果? 答:

void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, “hello”);
free(str);
if(str != NULL)
{
strcpy(str, “world”);
printf(str);
}
} 请问运行Test函数会有什么样的结果? 答:


五、编写strcpy函数(10分)
已知strcpy函数的原型是
char *strcpy(char *strDest, const char *strSrc);
其中strDest是目的字符串,strSrc是源字符串。

(1)不调用C++/C的字符串库函数,请编写函数 strcpy

(2)strcpy能把strSrc的内容复制到strDest,为什么还要char * 类型的返回值?


六、编写类String的构造函数、析构函数和赋值函数(25分)
已知类String的原型为:
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operate =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
请编写String的上述4个函数。
========================================================
为什么要加 extern "C"
原文地址:http://blog.csdn.net/ForNormandy/archive/2004/08/22/81396.aspx

在最近的面试中,发现有无数的公司笔试题目都是来自林锐的《高质量C++编程指南》,其中就有如题目这样的一道题目,不一定是原话,但问题差不多,我的答案是c与c++的函数调用规则不一样。以下是林锐的答案:

如果C++程序要调用已经被编译后的C 函数,该怎么办?
假设某个C 函数的声明如下:
void foo(int x, int y);
该函数被C 编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用C 函数。C++提供了一个C 连接交换指定符号extern“C”来解决这个问题。
例如:
extern “C”
{
void foo(int x, int y);
 // 其它函数
}
或者写成
extern “C”
{
#include “myheader.h”
 // 其它C 头文件
}
这就告诉C++编译译器,函数foo 是个C 连接,应该到库中找名字_foo 而不是找
_foo_int_int。C++编译器开发商已经对C 标准库的头文件作了extern“C”处理,所以
我们可以用#include 直接引用这些头文件。
===========================================================

用CreateProcess创建的子进程所获得的命令行参数:

原文地址:http://blog.csdn.net/ForNormandy/archive/2004/08/22/81832.aspx

用CreateProcess创建的子进程所获得的命令行参数有以下几种情况:

1.子进程中,WinMain函数的第三个参数lpCmdLine表示的命令行参数中除去应用程序路径、文件名以及与参数相隔的空格等字符串后的内容。比如

父进程:

CreateProcess(NULL, “c:\\test.exe -p“, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

子进程中,lpCmdLine参数为-p,注意没有双引号

 

2.如果通过lpCmdLine参数查看命令行参数,那么父进程创建子进程的时候,需要在子程序和参数中加上空格号。比如:

父进程:

CreateProcess(“c:\\test.exe”,“ -p“, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

子进程中,lpCmdLine参数为-p,没有空格了。

 

3.如果想获得子进程的完整命令行参数,调用GetCommandLine函数。通常通过GetCommandLine函数获得的命令行参数,是父进程调用时的完整参数。

CreateProcess(NULL, “c:\\test.exe -p“, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

子进程中,GetCommandLine返回的参数为c:\\test.exe -p,注意没有双引号

 

以下情况都是调用GetCommandLine函数获得命令行参数

4.CreateProcess第一个参数是NULL,第二个参数指定子应用程序和命令行参数,那么子进程的命令行参数是第二个参数,不带双引号。比如:

父进程:

CreateProcess(NULL, “c:\\test.exe -p“, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

子进程中,GetCommandLine获得的命令行参数为c:\\test.exe -p

 

5.CreateProcess第一个参数指定子应用程序路径和文件名,第二个参数为NULL,那么子进程的命令行参数是第一个参数,带双引号。比如:

父进程:

CreateProcess(“c:\\test.exe“, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

子进程中,GetCommandLine获得的命令行参数为”c:\\test.exe“

 

6.CreateProcess第一个参数指定子应用程序路径和文件名,第二个参数为传递给子进程的参数,那么子进程的命令行参数是第二个参数,不带双引号。比如:

父进程:

CreateProcess(“c:\\test.exe“, ”-p“, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

子进程中,GetCommandLine获得的命令行参数为-p。

此处注意同第二点比较,如果用lpCmdLine查看命令行参数,没什么都没有。但是将”-p”改成” -p”,用可以看到参数是-p,是不是很混乱?

posted @ 2006-09-24 16:46  ->  阅读(1160)  评论(0编辑  收藏  举报