关于作者

姓名:YS

性别:男

出生日期:1987-03-16

地区:北京-丰台

联系电话:67261234

QQ:241605738婚否:未婚
用户名:cjhacker
笔名:cjhacker
地区: 北京-丰台
行业:其他

日历  

快速登录

+ 用户名:
+ 密 码:

在线留言



友情链接

网络天书

放飞天空

个人论坛

访问统计:
文章个数:380
评论个数:174
留言条数:22




Powered by BlogDriver 2.1

Hacker is a Spirit

 

[The Spirit Of Hacker]向往自由,乐于探索.........

文章

C编程规范  (作者置顶)

C编程规范

感谢为编程规范作出辛勤劳动的作者!

本规范总则的内容包括:排版、注释、标识符命名、变量使用、代码可测性、程序效率、质量保证、代码编译、单元测试、程序版本与维护等。
本规范总则的示例都以C语言为背景,采用以下的术语描述:
  ★ 规则:编程时强制必须遵守的原则。
  ★ 建议:编程时必须加以考虑的原则。
  ★ 说明:对此规则或建议进行必要的解释。      
  ★ 示例:对此规则或建议从正、反两个方面给出例子。

目 录
1 排版   
2 注释   
3 标识符命名   
4 可读性   
5 变量、结构   
6 函数、过程   
7 可测性   
8 程序效率   
9 质量保证   
10 代码编辑、编译、审查   
11 代码测试、维护   
12 宏   

1 排版
¹    1-1:程序块要采用缩进风格编写,缩进的空格数为4个。
说明:对于由开发工具自动生成的代码可以有不一致。
¹    1-2:相对独立的程序块之间、变量说明之后必须加空行。
示例:如下例子不符合规范。
if (!valid_ni(ni))
{
  ... // program code
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;

应如下书写
if (!valid_ni(ni))
{
  ... // program code
}

repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
¹    1-3:较长的语句(>80字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句可读。
示例:
perm_count_msg.head.len = NO7_TO_STAT_PERM_COUNT_LEN
                + STAT_SIZE_PER_FRAM * sizeof( _UL );

act_task_table[frame_id * STAT_TASK_CHECK_NUMBER + index].occupied
        = stat_poi[index].occupied;

act_task_table[taskno].duration_true_or_false
        = SYS_get_sccp_statistic_state( stat_item );

report_or_not_flag = ((taskno < MAX_ACT_TASK_NUMBER)
              && (n7stat_stat_item_valid (stat_item))
              && (act_task_table[taskno].result_data != 0));

¹    1-4:循环、判断等语句中若有较长的表达式或语句,则要进行适应的划分,长表达式要在低优先级操作符处划分新行,操作符放在新行之首。
示例:
if ((taskno < max_act_task_number)
  && (n7stat_stat_item_valid (stat_item)))
{
  ... // program code
}

for (i = 0, j = 0; (i < BufferKeyword[word_index].word_length)
            && (j < NewKeyword.word_length); i++, j++)
{
  ... // program code
}

for (i = 0, j = 0;
  (i < first_word_length) && (j < second_word_length);
  i++, j++)
{
  ... // program code
}
¹    1-5:若函数或过程中的参数较长,则要进行适当的划分。
示例:
n7stat_str_compare((BYTE *) & stat_object,
            (BYTE *) & (act_task_table[taskno].stat_object),
            sizeof (_STAT_OBJECT));

n7stat_flash_act_duration( stat_item, frame_id *STAT_TASK_CHECK_NUMBER
                        + index, stat_object );
¹    1-6:不允许把多个短语句写在一行中,即一行只写一条语句。
示例:如下例子不符合规范。
rect.length = 0; rect.width = 0;

应如下书写
rect.length = 0;
rect.width = 0;
¹    1-7:if、for、do、while、case、switch、default等语句自占一行,且if、for、do、while等语句的执行语句部分无论多少都要加括号{}。
示例:如下例子不符合规范。
if (pUserCR == NULL) return;

应如下书写:
if (pUserCR == NULL)
{
  return;
}
¹    1-8:对齐只使用空格键,不使用TAB键。
说明:以免用不同的编辑器阅读程序时,因TAB键所设置的空格数目不同而造成程序布局不整齐,不要使用BC作为编辑器合版本,因为BC会自动将8个空格变为一个TAB键,因此使用BC合入的版本大多会将缩进变乱。
¹    1-9:函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case语句下的情况处理语句也要遵从语句缩进要求。
¹    1-10:程序块的分界符(如C/C++语言的大括号‘{’和‘}’)应各独占一行并且位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if、for、do、while、switch、case语句中的程序都要采用如上的缩进方式。
示例:如下例子不符合规范。
for (...) {
  ... // program code
}

if (...)
  {
  ... // program code
  }

void example_fun( void )
  {
  ... // program code
  }

应如下书写。
for (...)
{
  ... // program code
}

if (...)
{
  ... // program code
}

void example_fun( void )
{
  ... // program code
}
¹    1-11:在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。
说明:采用这种松散方式编写代码的目的是使代码更加清晰。
由于留空格所产生的清晰性是相对的,所以,在已经非常清晰的语句中没有必要再留空格,如果语句已足够清晰则括号内侧(即左括号后面和右号前面)不需要加空格,多重括号间不必加空格,因为在C/C++语言中括号已经是最清晰的标志了。
在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。

示例:
(1) 逗号、分号只在后面加空格。
int a, b, c;

(2)比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。
if (current_time >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
a = b ^ 2;

(3)"!"、"~"、"++"、"--"、"&"(地址运算符)等单目操作符前后不加空格。
*p = 'a';     // 内容操作"*"与内容之间
flag = !isEmpty; // 非操作"!"与内容之间
p = &mem;     // 地址操作"&" 与内容之间
i++;         // "++","--"与内容之间

(4)"->"、"."前后不加空格。
p->id = pid;   // "->"指针前后不加空格

(5) if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。
if (a >= b && c > d)
½    1-1:一行程序以小于80字符为宜,不要写得过长。
   
2 注释
¹    2-1:一般情况下,源程序有效注释量必须在20%以上。
说明:注释的原则是有助于对程序的阅读理解,在该加的地方都加了,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。
¹    2-2:说明性文件(如头文件.h文件、.inc文件、.def文件、编译说明文件.cfg等)头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者、内容、功能、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明。
示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
/*************************************************
Copyright (C), 1988-1999, Huawei Tech. Co., Ltd.
File name:     // 文件名
Author:     Version:     Date: // 作者、版本及完成日期
Description:   // 用于详细说明此程序文件完成的主要功能,与其他模块
            // 或函数的接口,输出值、取值范围、含义及参数间的控
            // 制、顺序、独立或依赖等关系
Others:       // 其它内容的说明
Function List: // 主要函数列表,每条记录应包括函数名及功能简要说明
  1. ....
History:     // 修改历史记录列表,每条修改记录应包括修改日期、修改
            // 者及修改内容简述
  1. Date:
    Author:
    Modification:
  2. ...
*************************************************/
¹    2-3:源文件头部应进行注释,列出:版权说明、版本号、生成日期、作者、模块目的/功能、主要函数及其功能、修改日志等。
示例:下面这段源文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
/************************************************************
Copyright (C), 1988-1999, Huawei Tech. Co., Ltd.
FileName: test.cpp
Author:     Version :       Date:
Description:   // 模块描述    
Version:       // 版本信息
Function List:   // 主要函数及其功能
  1. -------
History:       // 历史修改记录
    <author> <time>   <version >   <desc>
    David   96/10/12   1.0   build this moudle
***********************************************************/
说明:Description一项描述本文件的内容、功能、内部各部分之间的关系及本文件与其它文件关系等。History是修改历史记录列表,每条修改记录应包括修改日期、修改者及修改内容简述。
¹    2-4:函数头部应进行注释,列出:函数的目的/功能、输入参数、输出参数、返回值、调用关系(函数、表)等。
示例:下面这段函数的注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
/*************************************************
Function:     // 函数名称
Description:   // 函数功能、性能等的描述
Calls:       // 被本函数调用的函数清单
Called By:     // 调用本函数的函数清单
Table Accessed: // 被访问的表(此项仅对于牵扯到数据库操作的程序)
Table Updated: // 被修改的表(此项仅对于牵扯到数据库操作的程序)
Input:       // 输入参数说明,包括每个参数的作
            // 用、取值说明及参数间关系。
Output:       // 对输出参数的说明。
Return:       // 函数返回值的说明
Others:       // 其它说明
*************************************************/
¹    2-5:边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
¹    2-6:注释的内容要清楚、明了,含义准确,防止注释二义性。
说明:错误的注释不但无益反而有害。
规则2-7:避免在注释中使用缩写,特别是非常用缩写。
说明:在使用缩写时或之前,应对缩写进行必要的说明。
¹    2-8:注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。
示例:如下例子不符合规范。
例1:
/* get replicate sub system index and net indicator */


repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;

例2:
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
/* get replicate sub system index and net indicator */

应如下书写
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
¹    2-9:对于所有有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。变量、常量、宏的注释应放在其上方相邻位置或右方。
示例:
/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000

#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
¹    2-10:数据结构声明(包括数组、结构、类、枚举等),如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,不可放在下面;对结构中的每个域的注释放在此域的右方。
示例:可按如下形式说明枚举/数据/联合结构。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
  N_UNITDATA_IND, /* sccp notify sccp user unit data come */
  N_NOTICE_IND,   /* sccp notify user the No.7 network can not */
            /* transmission this message */
  N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};
¹    2-11:全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明。
示例:
/* The ErrorCode when SCCP translate */
/* Global Title failure, as follows */     // 变量作用、含义
/* 0 - SUCCESS   1 - GT Table error */
/* 2 - GT error Others - no use */     // 变量取值范围
/* only function SCCPTranslate() in */
/* this modual can modify it, and other */
/* module can visit it through call */
/* the function GetGTTransErrorCode() */   // 使用方法
BYTE g_GTTranErrorCode;
¹    2-12:注释与所描述内容进行同样的缩排。
说明:可使程序排版整齐,并方便注释的阅读与解。
示例:如下例子,排版不整齐,阅读稍感不方便。
void example_fun( void )
{
/* code one comments */
  CodeBlock One

    /* code two comments */
  CodeBlock Two
}

应改为如下布局。
void example_fun( void )
{
  /* code one comments */
  CodeBlock One

  /* code two comments */
  CodeBlock Two
}
¹    2-13:将注释与其上面的代码用空行隔开。
示例:如下例子,显得代码过于紧凑。
/* code one comments */
program code one
/* code two comments */
program code two

应如下书写
/* code one comments */
program code one

/* code two comments */
program code two
¹    2-14:对变量的定义和分支语句(条件分支、循环语句等)必须编写注释。
说明:这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。
¹    2-15:对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
说明:这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。
示例(注意斜体加粗部分):
case CMD_UP:  
  ProcessUp();
  break;

case CMD_DOWN:
  ProcessDown();
  break;

case CMD_FWD:
  ProcessFwd();
 
if (...)
{
  ...
  break;
}
else
{
  ProcessCFW_B();   // now jump into case CMD_A
}

case CMD_A:  
  ProcessA();  
  break;

case CMD_B:  
  ProcessB();  
  break;

case CMD_C:  
  ProcessC();  
    break;

case CMD_D:  
  ProcessD();  
  break;
...
½    2-1:避免在一行代码或表达式的中间插入注释。
说明:除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。
½    2-2:通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的。
说明:清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释。
½    2-3:在代码的功能、意图层次上进行注释,提供有用、额外的信息。
说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
示例:如下注释意义不大。
/* if receive_flag is TRUE */
if (receive_flag)

而如下的注释则给出了额外有用的信息。
/* if mtp receive a message from links */
if (receive_flag)
½    2-4:在程序块的结束行右方加注释标记,以表明某程序块的结束。
说明:当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。
示例:参见如下例子。
if (...)
{
  // program code

  while (index < MAX_INDEX)
  {
    // program code
  } /* end of while (index < MAX_INDEX) */ // 指明该条while语句结束
} /* end of if (...)*/ // 指明是哪条if语句结束
½    2-5:注释格式尽量统一,建议使用“/* …… */”。
½    2-6:注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。
说明:注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。

3 标识符命名
¹    3-1:标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。
说明:较短的单词可通过去掉“元音”形成缩写;较长的单词可取单词的头几个字母形成缩写;一些单词有大家公认的缩写。
示例:如下单词的缩写能够被大家基本认可。
temp 可缩写为 tmp ;
flag 可缩写为 flg ;
statistic 可缩写为 stat ;
increment 可缩写为 inc ;
message 可缩写为 msg ;
¹    3-2:命名中若使用特殊约定或缩写,则要有注释说明。
说明:应该在源文件的开始之处,对文件中所使用的缩写或约定,特别是特殊的缩写,进行必要的注释说明。
¹    3-3:自己特有的命名风格,要自始至终保持一致,不可来回变化。
说明:个人的命名风格,在符合所在项目组或产品组的命名规则的前提下,才可使用。(即命名规则中没有规定到的地方才可有个人命名风格)。
¹    3-4:对于变量命名,禁止取单个字符(如i、j、k...),建议除了要有具体含义外,还能表明其变量类型、数据类型等,但i、j、k作局部循环变量是允许的。
说明:变量,尤其是局部变量,如果用单个字符表示,很容易敲错(如i写成j),而编译时又检查不出来,有可能为了这个小小的错误而花费大量的查错时间。
示例:下面所示的局部变量名的定义方法可以借鉴。
int liv_Width
其变量名解释如下:
       l     局部变量(Local) (其它:g   全局变量(Global)...)
       i     数据类型(Interger)
       v     变量(Variable)   (其它:c   常量(Const)...)
       Width 变量含义
这样可以防止局部变量与全局变量重名。
¹    3-5:命名规范必须与所使用的系统风格保持一致,并在同一项目中统一,比如采用UNIX的全小写加下划线的风格或大小写混排的方式,不要使用大小写与下划线混排的方式,用作特殊标识如标识成员变量或全局变量的m_和g_,其后加上大小写混排的方式是允许的。
示例: Add_User不允许,add_user、AddUser、m_AddUser允许。
½    3-1:除非必要,不要用数字或较奇怪的字符来定义标识符。
示例:如下命名,使人产生疑惑。
#define _EXAMPLE_0_TEST_
#define _EXAMPLE_1_TEST_
void set_sls00( BYTE sls );

应改为有意义的单词命名
#define _EXAMPLE_UNIT_TEST_
#define _EXAMPLE_ASSERT_TEST_
void set_udt_msg_sls( BYTE sls );
½    3-2:在同一软件产品内,应规划好接口部分标识符(变量、结构、函数及常量)的命名,防止编译、链接时产生冲突。
说明:对接口部分的标识符应该有更严格限制,防止冲突。如可规定接口部分的变量与常量之前加上“模块”标识等。
½    3-3:用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
说明:下面是一些在软件中常用的反义词组。
add / remove     begin / end     create / destroy
insert / delete   first / last     get / release
increment / decrement           put / get
add / delete     lock / unlock     open / close
min / max       old / new       start / stop
next / previous   source / target   show / hide
send / receive   source / destination
cut / paste     up / down
示例:
int min_sum;
int max_sum;
int add_user( BYTE *user_name );
int delete_user( BYTE *user_name );
½    3-4:除了编译开关/头文件等特殊应用,应避免使用_EXAMPLE_TEST_之类以下划线开始和结尾的定义。
   
4 可读性
¹    4-1:注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级。
说明:防止阅读程序时产生误解,防止因默认的优先级与设计思想不符而导致程序出错。
示例:下列语句中的表达式
word = (high << 8) | low   (1)
if ((a | b) && (a & c))     (2)
if ((a | b) < (c & d))     (3)
如果书写为
high << 8 | low
a | b && a & c
a | b < c & d
由于
high << 8 | low = ( high << 8) | low,
a | b && a & c = (a | b) && (a & c),
(1)(2)不会出错,但语句不易理解;
a | b < c & d = a | (b < c) & d,(3)造成了判断条件出错。
¹    4-2:避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,须用有意义的枚举或宏来代替。
示例:如下的程序可读性差。
if (Trunk[index].trunk_state == 0)
{
  Trunk[index].trunk_state = 1;
  ... // program code
}

应改为如下形式。
#define TRUNK_IDLE 0
#define TRUNK_BUSY 1

if (Trunk[index].trunk_state == TRUNK_IDLE)
{
  Trunk[index].trunk_state = TRUNK_BUSY;
  ... // program code
}
½    4-1:源程序中关系较为紧密的代码应尽可能相邻。
说明:便于程序阅读和查找。
示例:以下代码布局不太合理。
rect.length = 10;
char_poi = str;
rect.width = 5;

若按如下形式书写,可能更清晰一些。
rect.length = 10;
rect.width = 5; // 矩形的长与宽关系较密切,放在一起。
char_poi = str;
½    4-2:不要使用难懂的技巧性很高的语句,除非很有必要时。
说明:高技巧语句不等于高效率的程序,实际上程序的效率关键在于算法。
示例:如下表达式,考虑不周就可能出问题,也较难理解。
* stat_poi ++ += 1;

* ++ stat_poi += 1;

应分别改为如下。
*stat_poi += 1;
stat_poi++;   // 此二语句功能相当于“ * stat_poi ++ += 1; ”

++ stat_poi;
*stat_poi += 1; // 此二语句功能相当于“ * ++ stat_poi += 1; ”
5 变量、结构
¹    5-1:去掉没必要的公共变量。
说明:公共变量是增大模块间耦合的原因之一,故应减少没必要的公共变量以降低模块间的耦合度。
¹    5-2:仔细定义并明确公共变量的含义、作用、取值范围及公共变量间的关系。
说明:在对变量声明的同时,应对其含义、作用及取值范围进行注释说明,同时若有必要还应说明与其它变量的关系。
¹    5-3:明确公共变量与操作此公共变量的函数或过程的关系,如访问、修改及创建等。
说明:明确过程操作变量的关系后,将有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。
示例:在源文件中,可按如下注释形式说明。
RELATION   System_Init   Input_Rec   Print_Rec   Stat_Score
Student   Create       Modify     Access     Access
Score     Create       Modify     Access     Access, Modify

注:RELATION为操作关系;System_Init、Input_Rec、Print_Rec、Stat_Score为四个不同的函数;Student、Score为两个全局变量;Create表示创建,Modify表示修改,Access表示访问。
其中,函数Input_Rec、Stat_Score都可修改变量Score,故此变量将引起函数间较大的耦合,并可能增加代码测试、维护的难度。
¹    5-4:当向公共变量传递数据时,要十分小心,防止赋与不合理的值或越界等现象发生。
说明:对公共变量赋值时,若有必要应进行合法性检查,以提高代码的可靠性、稳定性。
¹    5-5:防止局部变量与公共变量同名。
说明:若使用了较好的命名规则,那么此问题可自动消除。
¹    5-6:严禁使用未经初始化的变量作为右值。
说明:特别是在C/C++中引用未经赋值的指针,经常会引起系统崩溃。
½    5-1:构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的公共变量,防止多个不同模块或函数都可以修改、创建同一公共变量的现象。
说明:降低公共变量耦合度。
½    5-2:使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量。
说明:使用标准的数据类型,有利于程序的移植。
示例:如下例子(在DOS下BC3.1环境中),在移植时可能产生问题。
void main()
{
  register int index; // 寄存器变量

  _AX = 0x4000; // _AX是BC3.1提供的寄存器“伪变量”
  ... // program code
}
½    5-3:结构的功能要单一,是针对一种事务的抽象。
说明:设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。
示例:如下结构不太清晰、合理。
typedef struct STUDENT_STRU
{
  unsigned char name[8]; /* student's name */
  unsigned char age;   /* student's age */
  unsigned char sex;   /* student's sex, as follows */
                  /* 0 - FEMALE; 1 - MALE */
  unsigned char
      teacher_name[8]; /* the student teacher's name */
  unisgned char
      teacher_sex;   /* his teacher sex */
} STUDENT;

若改为如下,可能更合理些。
typedef struct TEACHER_STRU
{
  unsigned char name[8]; /* teacher name */
  unisgned char sex;   /* teacher sex, as follows */
                  /* 0 - FEMALE; 1 - MALE */
} TEACHER;

typedef struct STUDENT_STRU
{
  unsigned char name[8];   /* student's name */
  unsigned char age;       /* student's age */
  unsigned char sex;       /* student's sex, as follows */
                    /* 0 - FEMALE; 1 - MALE */
  unsigned int teacher_ind; /* his teacher index */
} STUDENT;
½    5-4:不要设计面面俱到、非常灵活的数据结构。
说明:面面俱到、灵活的数据结构反而容易引起误解和操作困难。
½    5-5:不同结构间的关系不要过于复杂。
说明:若两个结构间关系较复杂、密切,那么应合为一个结构。
示例:如下两个结构的构造不合理。
typedef struct PERSON_ONE_STRU
{
  unsigned char name[8];
  unsigned char addr[40];
  unsigned char sex;
  unsigned char city[15];
} PERSON_ONE;

typedef struct PERSON_TWO_STRU
{
  unsigned char name[8];
  unsigned char age;
  unsigned char tel;
} PERSON_TWO;

由于两个结构都是描述同一事物的,那么不如合成一个结构。
typedef struct PERSON_STRU
{
  unsigned char name[8];
  unsigned char age;
  unsigned char sex;
  unsigned char addr[40];
  unsigned char city[15];
  unsigned char tel;
} PERSON;
½    5-6:结构中元素的个数应适中。若结构中元素个数过多可考虑依据某种原则把元素组成不同的子结构,以减少原结构中元素的个数。
说明:增加结构的可理解性、可操作性和可维护性。
示例:假如认为如上的_PERSON结构元素过多,那么可如下对之划分。
typedef struct PERSON_BASE_INFO_STRU
{
  unsigned char name[8];
  unsigned char age;
  unsigned char sex;
} PERSON_BASE_INFO;

typedef struct PERSON_ADDRESS_STRU
{
  unsigned char addr[40];
  unsigned char city[15];
  unsigned char tel;
} PERSON_ADDRESS;

typedef struct PERSON_STRU
{
  PERSON_BASE_INFO person_base;
  PERSON_ADDRESS person_addr;
} PERSON;
½    5-7:仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象。
说明:合理排列结构中元素顺序,可节省空间并增加可理解性。
示例:如下结构中的位域排列,将占较大空间,可读性也稍差。
typedef struct EXAMPLE_STRU
{
  unsigned int valid: 1;
  PERSON person;
  unsigned int set_flg: 1;
} EXAMPLE;

若改成如下形式,不仅可节省1字节空间,可读性也变好了。
typedef struct EXAMPLE_STRU
{
  unsigned int valid: 1;
  unsigned int set_flg: 1;
  PERSON person ;
} EXAMPLE;
½    5-8:结构的设计要尽量考虑向前兼容和以后的版本升级,并为某些未来可能的应用保留余地(如预留一些空间等)。
说明:软件向前兼容的特性,是软件产品是否成功的重要标志之一。如果要想使产品具有较好的前向兼容,那么在产品设计之初就应为以后版本升级保留一定余地,并且在产品升级时必须考虑前一版本的各种特性。
½    5-9:留心具体语言及编译器处理不同数据类型的则及有关细节。
说明:如在C语言中,static局部变量将在内存“数据区”中生成,而非static局部变量将在“堆栈”中生成。这些细节对程序质量的保证非常重要。
½    5-10:编程时,要注意数据类型的强制转换。
说明:当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。
½    5-11:对编译系统默认的数据类型转换,也要有充分的认识。
示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。
char chr;
unsigned short int exam;

chr = -1;
exam = chr; // 编译器不产生告警,此时exam为0xFFFF。
½    5-12:尽量减少没有必要的数据类型默认转换与强制转换。
½    5-13:合理地设计数据并使用自定义数据类型,避免数据间进行不必要的类型转换。
½    5-14:对自定义数据类型进行恰当命名,使它成为自描述性的,以提高代码可读性。注意其命名方式在同一产品中的统一。
说明:使用自定义类型,可以弥补编程语言提供类型少、信息量不足的缺点,并能使程序清晰、简洁。
示例:可参考如下方式声明自定义数据类型。

下面的声明可使数据类型的使用简洁、明了。
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned int   DWORD;

下面的声明可使数据类型具有更丰富的含义。
typedef float DISTANCE;
typedef float SCORE;
½    5-15:当声明用于分布式环境或不同CPU间通信环境的数据结构时,必须考虑机器的字节顺序、使用的位域及字节对齐等问题 。
说明:比如Intel CPU与68360 CPU,在处理位域及整数时,其在内存存放的“顺序”正好相反。
示例:假如有如下短整数及结构。
unsigned short int exam;
typedef struct EXAM_BIT_STRU
{               /* Intel 68360 */
  unsigned int A1: 1; /* bit 0     7   */
  unsigned int A2: 1; /* bit 1     6   */
  unsigned int A3: 1; /* bit 2     5   */
} EXAM_BIT;

如下是Intel CPU生成短整数及位域的方式。
内存: 0       1       2   ... (从低到高,以字节为单位)
exam exam低字节 exam高字节

内存:     0 bit   1 bit     2 bit   ... (字节的各“位”)
EXAM_BIT   A1     A2       A3

如下是68360 CPU生成短整数及位域的方式。
内存: 0       1       2   ... (从低到高,以字节为单位)
exam exam高字节 exam低字节

内存:     7 bit   6 bit     5 bit   ... (字节的各“位”)
EXAM_BIT   A1     A2       A3

说明:在对齐方式下,CPU的运行效率要快得多。
示例:如下图,当一个long型数(如图中long1)在内存中的位置正好与内存的字边界对齐时,CPU存取这个数只需访问一次内存,而当一个long型数(如图中的long2)在内存中的位置跨越了字边界时,CPU存取这个数就需要多次访问内存,如i960cx访问这样的数需读内存三次(一个BYTE、一个SHORT、一个BYTE,由CPU的微代码执行,对软件透明),所有对齐方式下CPU的运行效率明显快多了。
   1     8     16     24     32
   ------- ------- ------- -------
   | long1 | long1 | long1 | long1 |
   ------- ------- ------- -------
   |     |     |     | long2 |
   ------- ------- ------- --------
   | long2 | long2 | long2 |     |
   ------- ------- ------- --------
   | ....
6 函数、过程
¹    6-1:对所调用函数的错误返回码要仔细、全面地处理。
¹    6-2:明确函数功能,精确(而不是近似)地实现函数设计。
¹    6-3:编写可重入函数时,应注意局部变量的使用(如编写C/C++语言的可重入函数时,应使用auto即缺省态局部变量或寄存器变量)。
说明:编写C/C++语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性。
¹    6-4:编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
示例:假设Exam是int型全局变量,函数Squre_Exam返回Exam平方值。那么如下函数不具有可重入性。
unsigned int example( int para )
{
  unsigned int temp;

  Exam = para; // (**)
  temp = Square_Exam( );

  return temp;
}

此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使Exam赋与另一个不同的para值,所以当控制重新回到“temp = Square_Exam( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。
unsigned int example( int para )
{
  unsigned int temp;

  [申请信号量操作]       // 若申请不到“信号量”,说明另外的进程正处于
  Exam = para;         // 给Exam赋值并计算其平方过程中(即正在使用此
  temp = Square_Exam( ); // 信号),本进程必须等待其释放信号后,才可继
  [释放信号量操作]       // 续执行。若申请到信号,则可继续执行,但其
                  // 它进程必须等待本进程释放信号量后,才能再使
                  // 用本信号。
  return temp;
}
¹    6-5:在同一项目组应明确规定对接口函数参数的合法性检查应由函数的调用者负责还是由接口函数本身负责,缺省是由函数调用者负责。
说明:对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。
½    6-1:防止将函数的参数作为工作变量。
说明:将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
示例:下函数的实现不太好。
void sum_data( unsigned int num, int *data, int *sum )
{
  unsigned int count;
 
  *sum = 0;
  for (count = 0; count < num; count++)
  {
    *sum += data[count]; // sum成了工作变量,不太好。
  }
}

若改为如下,则更好些。
void sum_data( unsigned int num, int *data, int *sum )
{
  unsigned int count ;
  int sum_temp;
 
  sum_temp = 0;
  for (count = 0; count < num; count ++)
  {
    sum_temp += data[count];
  }
 
  *sum = sum_temp;
}
½    6-2:函数的规模尽量限制在200行以内。
说明:不包括注释和空格行。
½    6-3:一个函数仅完成一件功能。
½    6-4:为简单功能编写函数。
说明:虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。
示例:如下语句的功能不很明显。
value = ( a > b ) ? a : b ;

改为如下就很清晰了。
int max (int a, int b)
{
  return ((a > b) ? a : b);
}

value = max (a, b);

或改为如下。
#define MAX (a, b) (((a) > (b)) ? (a) : (b))

value = MAX (a, b);
½    6-5:不要设计多用途面面俱到的函数。
说明:多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。
½    6-6:函数的功能该是可以预测的,也就是只要输入数据相同就应产生同样的输出。
说明:带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是STATIC的局部变量的地址作为返回值,若为AUTO类,则返回为错针。
示例:如下函数,其返回值(即功能)是不可预测的。
unsigned int integer_sum( unsigned int base )
{
  unsigned int index;
  static unsigned int sum = 0; // 注意,是static类型的。
                      // 若改为auto类型,则函数即变为可预测。
  for (index = 1; index <= base; index++)
  {
    sum += index;
  }

  return sum;
}
½    6-7:尽量不要编写依赖于其他函数内部实现的函数。
说明:此条为函数独立性的基本要求。由于目前大部分高级语言都是结构化的,所以通过具体语言的语法要求与编译器功能,基本就可以防止这种情况发生。但在汇编语言中,由于其灵活性,很可能使函数出现这种情况。
示例:如下是在DOS下TASM的汇编程序例子。过程Print_Msg的实现依赖于Input_Msg的具体实现,这种程序是非结构化的,难以维护、修改。
... // 程序代码
proc Print_Msg // 过程(函数)Print_Msg
  ... // 程序代码
  jmp LABEL
  ... // 程序代码
endp

proc Input_Msg // 过程(函数)Input_Msg
  ... // 程序代码
LABEL:
  ... // 程序代码
endp
½    6-8:避免设计多参数函数,不使用的参数从接口中去掉。
说明:目的减少函数间接口的复杂度。
½    6-9:非调度函数应减少或防止控制参数,尽量只使用数据参数。
说明:本建议目的是防止函数间的控制耦合。调度函数是指根据输入的消息类型或控制命令,来启动相应的功能实体(即函数或过程),而本身并不完成具体功能。控制参数是指改变函数功能行为的参数,即函数要根据此参数来决定具体怎样工作。非调度函数的控制参数增加了函数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一。
示例:如下函数构造不太合理。
int add_sub( int a, int b, unsigned char add_sub_flg )
{
  if (add_sub_flg == INTEGER_ADD)
  {
    return (a + b);
  }
  else
  {
    return (a   b);
  }
}

不如分为如下两个函数清晰。
int add( int a, int b )
{
  return (a + b);
}

int sub( int a, int b )
{
  return (a   b);
}
½    6-10:检查函数所有参数输入的有效性。
½    6-11:检查函数所有非参数输入的有效性,如数据文件、公共变量等。
说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入之前,应进行必要的检查。
½    6-12:函数名应准确描述函数的功能。
½    6-13:使用动宾词组为执行某操作的函数命名。如果是OOP方法,可以只有动词(名词是对象本身)。
示例:参照如下方式命名函数。
void print_record( unsigned int rec_ind ) ;
int input_record( void ) ;
unsigned char get_current_color( void ) ;
建议6-14:避免使用无意义或含义不清的动词为函数命名。
说明:避免用含义不清的动词如process、handle等为函数命名,因为这些动词并没有说明要具体做什么。
建议6-15:函数的返回值要清楚、明了,让使用者不容易忽视错误情况。
说明:函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码。
½    6-16:除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。
½    6-17:让函数在调用点显得易懂、容易理解。
½    6-18:在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转换。
说明:因为数据类型转换或多或少存在危险。
½    6-19:避免函数中不必要语句,防止程序中的垃圾代码。
说明:程序中的垃圾代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。
½    6-20:防止把没有关联的语句放到一个函数中。
说明:防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。
在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理的,否则这种构造将产生随机内聚的函数。
示例:如下函数就是一种随机内聚。
void Init_Var( void )
{
  Rect.length = 0;
  Rect.width = 0; /* 初始化矩形的长与宽 */
 
  Point.x = 10;
  Point.y = 10;   /* 初始化“点”的坐标 */
}

矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。
应如下分为两个函数:
void Init_Rect( void )
{
  Rect.length = 0;
  Rect.width = 0; /* 初始化矩形的长与宽 */
}

void Init_Point( void )
{
  Point.x = 10;
  Point.y = 10;   /* 初始化“点”的坐标 */
}
½    6-21:如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。
说明:若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。
½    6-22:功能不明确较小的函数,特别是仅有一个上级函数调用它时,应考虑把它合并到上级函数中,而不必单独存在。
说明:模块中函数划分的过多,一般会使函数间的接口变得复杂。所以过小的函数,特别是扇入很低的或功能不明确的函数,不值得单独存在。
½    6-23:设计高扇入、合理扇出(小于7)的函数。
说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,如总是1,表明函数的调用层次可能过多,这样不利程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是3-5。扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。
扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。
较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。
½    6-24:减少函数本身或函数间的递归调用。
说明:递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用。
½    6-25:仔细分析模块的功能及性能需求,并进一步细分,同时若有必要画出有关数据流图,据此来进行模块的函数划分与组织。
说明:函数的划分与组织是模块的实现过程中很关键的步骤,如何划分出合理的函数结构,关系到模块的最终效率和可维护性、可测性等。根据模块的功能图或/及数据流图映射出函数结构是常用方法之一。
½    6-26:改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,要遵守以下原则:
(1)不能影响模块功能的实现。
(2)仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(3)通过分解或合并函数来改进软件结构。
(4)考查函数的规模,过大的要进行分解。
(5)降低函数间接口的复杂度。
(6)不同层次的函数调用要有较合理的扇入、扇出。
(7)函数功能应可预测。
(8)提高函数内聚。(单一功能的函数内聚最高)
说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。
½    6-27:在多任务操作系统的环境下编程,要注意函数可重入性的构造。
说明:可重入性是指函数可以被多个任务进程调用。在多任务操作系统中,函数是否具有可重入性是非常重要的,因为这是多个进程可以共用此函数的必条件。另外,编译器是否提供可重入函数库,与它所服务的操作系统有关,只有操作系统是多任务时,编译器才有可能提供可重入函数库。如DOS下BC和MSC等就不具备可重入函数库,因为DOS是单用户单任务操作系统。
½    6-28:避免使用BOOL参数。
说明:原因有二,其一是BOOL参数值无意义,TURE/FALSE的含义是非常模糊的,在调用时很难知道该参数到底传达的是什么意思;其二是BOOL参数值不利于扩充。还有NULL也是一个无意义的单词。
½    6-29: 对于提供了返回值的函数,在引用时最好使用其返回值。
½    6-30:当一个过程(函数)中对较长变量(一般是结构的成员)有较多引用时,可以用一个意义相当的宏代替。
说明:这样可以增加编程效率和程序的可读性。
示例:在某过程中较多引用TheReceiveBuffer[FirstSocket].byDataPtr,
则可以通过以下宏定义来代替:
# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr
 

网通站即日起开通,希望大家点击此处帮助测试! 谢谢
 
[楼 主] | Posted: 2006-09-08 18:41  
 
 
£壞√№人愛


 


 
级别: 一星少将
精华: 0
发帖: 153
联盟游侠威望: 931 点
联盟游侠金币: 4826 银两
联盟贡献值: 2600 点
联盟好评度: 377 点
联盟考核分数: 229 分
在线时间:130(小时)
注册时间:2006-04-11
最后登录:2006-10-07      

7 可测性
¹    7-1:在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明。
说明:本规则是针对项目组或产品组的。
¹    7-2:在同一项目组或产品组内,调测打印出的信息串的格式要有统一的形式。信息串中至少要有所在模块名(或源文件名)及行号。
说明:统一的调测信息格式便于集成测试。
¹    7-3:编程的同时要为单元测试选择恰当的测试点,并仔细构造测试代码、测试用例,同时给出明确的注释说明。测试代码部分应作为(模块中的)一个子模块,以方便测试代码在模块中的安装与拆卸(通过调测开关)。
说明:为单元测试而准备。
¹    7-4:在进行集成测试/系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率。
说明:好的测试用例应尽可能模拟出程序所遇到的边界值、各种复杂环境及一些极端情况等。
¹    7-5:使用断言来发现软件问题,提高代码可测性。
说明:断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。实际应用时,可根据具体情况灵活地设计断言。
示例:下面是C语言中的一个断言,用宏来设计的。(其中NULL为0L)
#ifdef _EXAM_ASSERT_TEST_ // 若使用断言测试

void exam_assert( char * file_name, unsigned int line_no )
{
  printf( "\n[EXAM]Assert failed: %s, line %u\n",
        file_name, line_no );
  abort( );
}

#define EXAM_ASSERT( condition )
  if (condition) // 若条件成立,则无动作
    NULL;
  else // 否则报告
    exam_assert( __FILE__, __LINE__ )

#else // 若不使用断言测试

#define EXAM_ASSERT(condition) NULL

#endif /* end of ASSERT */
¹    7-6:用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况。
¹    7-7:不能用断言来检查最终产品肯定会出现且必须处理的错误情况。
说明:断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。
¹    7-8:对较复杂的断言加上明确的注释。
说明:为复杂的断言加注释,可澄清断言含义并减少不必要的误用。
¹    7-9:用断言确认函数的参数。
示例:假设某函数参数中有一个指针,那么使用指针前可对它检查,如下。
int exam_fun( unsigned char *str )
{
  EXAM_ASSERT( str != NULL ); // 用断言检查“假设指针不为空”这个条件
 
  ... //other program code
}
¹    7-10:用断言保证没有定义的特性或功能不被使用。
示例:假设某通信模块在设计时,准备提供“无连接”和“连接” 这两种业务。但当前的版本中仅实现了“无连接”业务,且在此版本的正式发行版中,用户(上层模块)不应产生“连接”业务的请求,那么在测试时可用断言检查用户是否使用“连接”业务。如下。
#define EXAM_CONNECTIONLESS 0 // 无连接业务
#define EXAM_CONNECTION   1 // 连接业务

int msg_process( EXAM_MESSAGE *msg )
{
  unsigned char service; /* message service class */

  EXAM_ASSERT( msg != NULL );

service = get_msg_service_class( msg );

  EXAM_ASSERT( service != EXAM_CONNECTION ); // 假设不使用连接业务

  ... //other program code
}
¹    7-11:用断言对程序开发环境(OS/Compiler/Hardware)的假设进行检查。
说明:程序运行时所需的软硬件环境及配置要求,不能用断言来检查,而必须由一段专门代码处理。用断言仅可对程序开发环境中的假设及所配置的某版本软硬件是否具有某种功能的假设进行检查。如某网卡是否在系统运行环境中配置了,应由程序中正式代码来检查;而此网卡是否具有某设想的功能,则可由断言来检查。
对编译器提供的功能及特性假设可用断言检查,原因是软件最终产品(即运行代码或机器码)与编译器已没有任何直接关系,即软件运行过程中(注意不是编译过程中)不会也不应该对编译器的功能提出任何需求。
示例:用断言检查编译器的int型数据占用的内存空间是否为2,如下。
EXAM_ASSERT( sizeof( int ) == 2 );
¹    7-12:正式软件产品中应把断言及其它调测代码去掉(即把有关的调测开关关掉)。
说明:加快软件运行速度。
¹    7-13:在软件系统中设置与取消有关测试手段,不能对软件实现的功能等产生影响。
说明:即有测试代码的软件和关掉测试代码的软件,在功能行为上应一致。
¹    7-14:用调测开关来切换软件的DEBUG版和正式版,而不要同时存在正式版本和DEBUG版本的不同源文件,以减少维护的难度。
¹    7-15:软件的DEBUG版本和发行版本应该统一维护,不允许分家,并且要时刻注意保证两个版本在实现功能上的一致性。
½    7-1:在编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关及相应测试代码如打印函数等。
说明:程序的调试与测试是软件生存周期中很重要的一个阶段,如何对软件进行较全面、高率的测试并尽可能地找出软件中的错误就成为很关键的问题。因此在编写源代码之前,除了要有一套比较完善的测试计划外,还应设计出一系列代码测试手段,为单元测试、集成测试及系统联调提供方便。
½    7-2:调测开关应分为不同级别和类型。
说明:调测开关的设置及分类应从以下几方面考虑:针对模块或系统某部分代码的调测;针对模块或系统某功能的调测;出于某种其它目的,如对性能、容量等的测试。这样做便于软件功能的调测,并且便于模块的单元测试、系统联调等。
½    7-3:编写防错程序,然后在处理错误之后可用断言宣布发生错误。
示例:假如某模块收到通信链路上的消息,则应对消息的合法性进行检查,若消息类别不是通信协议中规定的,则应进行出错处理,之后可用断言报告,如下例。
#ifdef _EXAM_ASSERT_TEST_ // 若使用断言测试

/* Notice: this function does not call 'abort' to exit program */
void assert_report( char * file_name, unsigned int line_no )
{
  printf( "\n[EXAM]Error Report: %s, line %u\n",
        file_name, line_no );
}

#define ASSERT_REPORT( condition )
  if ( condition ) // 若条件成立,则无动作
    NULL;
  else // 否则报告
    assert_report ( __FILE__, __LINE__ )

#else // 若不使用断言测试

#define ASSERT_REPORT( condition ) NULL

#endif /* end of ASSERT */

int msg_handle( unsigned char msg_name, unsigned char * msg )
{
  switch( msg_name )
  {
    case MSG_ONE:
        ... // 消息MSG_ONE处理
        return MSG_HANDLE_SUCCESS;
 
        ... // 其它合法消息处理
 
    default:
        ... // 消息出错处理
        ASSERT_REPORT( FALSE ); // “合法”消息不成立,报告
        return MSG_HANDLE_ERROR;
  }
}
8 程序效率
¹    8-1:编程时要经常注意代码的效率。
说明:代码效率分为全局效率、局部效率、时间效率及空间效率。全局效率是站在整个系统的角度上的系统效率;局部效率是站在模块或函数角度上的效率;时间效率是程序处理输入任务所需的时间长短;空间效率是程序所需内存空间,如机器代码空间大小、数据空间大小、栈空间大小等。
¹    8-2:在保证软件系统的正确性、稳定性、可读性及可测性的前提下,提高代码效率。
说明:不能一味地追求代码效率,而对软件的正确性、稳定性、可读性及可测性造成影响。
¹    8-3:局部效率应为全局效率服务,不能因为提高局部效率而对全局效率造成影响。
¹    8-4:通过对系统数据结构的划分与组织的改进,以及对程序算法的优化来提高空间效率。
说明:这种方式是解决软件空间效率的根本办法。
示例:如下记录学生学习成绩的结构不合理。
typedef unsigned char BYTE;
typedef unsigned short WORD;

typedef struct STUDENT_SCORE_STRU


  BYTE name[8];
  BYTE age;
  BYTE sex;
  BYTE class;
  BYTE subject;
  float score;
} STUDENT_SCORE;

因为每位学生都有多科学习成绩,故如上结构将占用较大空间。应如下改进(分为两个结构),总的存贮空间将变小,操作也变得更方便。
typedef struct STUDENT_STRU
{
  BYTE name[8];
  BYTE age;
  BYTE sex;
  BYTE class;
} STUDENT;

typedef struct STUDENT_SCORE_STRU
{
  WORD student_index;
  BYTE subject;
  float score;
} STUDENT_SCORE;
¹    8-5:循环体内工作量最小化。
说明:应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的时间效率。
示例:如下代码效率不高。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
  sum += ind;
  back_sum = sum; /* backup sum */
}

语句“back_sum = sum;”完全可以放在for语句之后,如下。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
  sum += ind;
}
back_sum = sum; /* backup sum */
½    8-1:仔细分析有关算法,并进行优化。
½    8-2:仔细考查、分析系统及模块处理输入(如事务、消息等)的方式,并加以改进。
½    8-3:对模块中函数的划分及组织方式进行分析、优化,改进模块中函数的组织结构,提高程序效率。
说明:软件系统的效率主要与算法、处理任务方式、系统功能及函数结构有很大关系,仅在代码上下功夫一般不能解决根本问题。
½    8-4:编程时,要随时留心代码效率;优化代码时,要考虑周全。
½    8-5:不应花过多的时间拼命地提高调用不很频繁的函数代码效率。
说明:对代码优化可提高效率,但若考虑不周很有可能引起严重后果。
½    8-6:要仔细地构造或直接用汇编编写调用频繁或性能要求极高的函数。
说明:只有对编译系统产生机器码的方式以及硬件系统较为熟悉时,才可使用汇编嵌入方式。嵌入汇编可提高时间及空间效率,但也存在一定风险。
½    8-7:在保证程序质量的前提下,通过压缩代码量、去掉不必要代码以及减少不必要的局部和全局变量,来提高空间效率。
说明:这种方式对提高空间效率可起到一定作用,但往往不能解决根本问题。
½    8-8:在多重循环中,应将最忙的循环放在最内层。
说明:减少CPU切入循环层的次数。
示例:如下代码效率不高。
for (row = 0; row < 100; row++)
{
  for (col = 0; col < 5; col++)
  {
    sum += a[row][col];
  }
}

可以改为如下方式,以提高效率。
for (col = 0; col < 5; col++)
{
  for (row = 0; row < 100; row++)
  {
    sum += a[row][col];
  }
}
½    8-9:尽量减少循环嵌套层次。
½    8-10:避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。
说明:目的是减少判断次数。循环体中的判断语句是否可以移到循环体外,要视程序的具体情况而言,一般情况,与循环变量无关的判断语句可以移到循环体外,而有关的则不可以。
示例:如下代码效率稍低。
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
  if (data_type == RECT_AREA)
  {
    area_sum += rect_area[ind];
  }
  else
  {
    rect_length_sum += rect[ind].length;
    rect_width_sum += rect[ind].width;
  }
}

因为判断语句与循环变量无关,故可如下改进,以减少判断次数。
if (data_type == RECT_AREA)
{
  for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
  {
    area_sum += rect_area[ind];
  }
}
else
{
  for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
  {
    rect_length_sum += rect[ind].length;
    rect_width_sum += rect[ind].width;
  }
}
½    8-11:尽量用乘法或其它方法代替除法,特别是浮点运算中的除法。
说明:浮点运算除法要占用较多CPU资源。
示例:如下表达式运算可能要占较多CPU资源。
#define PAI 3.1416
radius = circle_length / (2 * PAI);

应如下把浮点除法改为浮点乘法。
#define PAI_RECIPROCAL (1 / 3.1416 ) // 编译器编译时,将生成具体浮点数
radius = circle_length * PAI_RECIPROCAL / 2;
½    8-12:不要一味追求紧凑的代码。
说明:因为紧凑的代码并不代表高效的机器码。
9 质量保证
¹    9-1:在软件设计过程中构筑软件质量。
¹    9-2:代码质量保证优先原则
  (1)正确性,指程序要实现设计要求的功能。
  (2)稳定性、安全性,指程序稳定、可靠、安全。
  (3)可测试性,指程序要具有良好的可测试性。
  (4)规范/可读性,指程序书写风格、命名规则等要符合规范。
  (5)全局效率,指软件系统的整体效率。
  (6)局部效率,指某个模块/子模块/函数的本身效率。
  (7)个人表达方式/个人方便性,指个人编程习惯。
¹    9-3:只引用属于自己的存贮空间。
说明:若模块封装的较好,那么一般不会发生非法引用他人的空间。
¹    9-4:防止引用已经释放的内存空间。
说明:在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块(如C语言指针),而另一模块在随后的某个时刻又使用了它。要防止这种情况发生。
¹    9-5:过程/函数中分配的内存,在过程/函数退出之前要释放。
¹    9-6:过程/函数中申请的(为打开文件而使用的)文件句柄,在过程/函数退出之前要关闭。
说明:分配的内存不释放以及文件句柄不关闭,是较常见的错误,而且稍不注意就有可能发生。这类错误往往会引起很严重后果,且难以定位。
示例:下函数在退出之前,没有把分配的内存释放。
typedef unsigned char BYTE;

int example_fun( BYTE gt_len, BYTE *gt_code )
{
  BYTE *gt_buf;

  gt_buf = (BYTE *) malloc (MAX_GT_LENGTH);
  ... //program code, include check gt_buf if or not NULL.
 
  /* global title length error */
  if (gt_len > MAX_GT_LENGTH)
  {
    return GT_LENGTH_ERROR; // 忘了释放gt_buf
  }
 
  ... // other program code
}

应改为如下。
int example_fun( BYTE gt_len, BYTE *gt_code )
{
  BYTE *gt_buf;

  gt_buf = (BYTE * ) malloc ( MAX_GT_LENGTH );
  ... // program code, include check gt_buf if or not NULL.
 
  /* global title length error */
  if (gt_len > MAX_GT_LENGTH)
  {
    free( gt_buf ); // 退出之前释放gt_buf
    return GT_LENGTH_ERROR;
  }
 
  ... // other program code
}
¹    9-7:防止内存操作越界。
说明:内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要细小心。
示例:假设某软件系统最多可由10个用户同时使用,用户号为1-10,那么如下程序存在问题。
#define MAX_USR_NUM 10
unsigned char usr_login_flg[MAX_USR_NUM]= "";

void set_usr_login_flg( unsigned char usr_no )
{
  if (!usr_login_flg[usr_no])
  {
    usr_login_flg[usr_no]= TRUE;
  }
}

当usr_no为10时,将使用usr_login_flg越界。可采用如下方式解决。
void set_usr_login_flg( unsigned char usr_no )
{
  if (!usr_login_flg[usr_no - 1])
  {
    usr_login_flg[usr_no - 1]= TRUE;
  }
}
¹    9-8:认真处理程序所能遇到的各种出错情况。
¹    9-9:系统运行之初,要初始化有关变量及运行环境,防止未经初始化的变量被引用。
¹    9-10:系统运行之初,要对加载到系统中的数据进行一致性检查。
说明:使用不一致的数据,容易使系统进入混乱状态和不可知状态。
¹    9-11:严禁随意更改其它模块或系统的有关设置和配置。
说明:编程时,不能随心所欲地更改不属于自己模块的有关设置如常量、数组的大小等。
¹    9-12:不能随意改变与其它模块的接口。
¹    9-13:充分了解系统的接口之后,再使用系统提供的功能。
示例:在B型机的各模块与操作系统的接口函数中,有一个要由各模块负责编写的初始化过程,此过程在软件系统加载完成后,由操作系统发送的初始化消息来调度。因此就涉及到初始化消息的类型与消息发送的顺序问题,特别是消息顺序,若没搞清楚就开始编程,很容易引起严重后果。以下示例引自B型曾出现过的实际代码,其中使用了FID_FETCH_DATA与FID_INITIAL初始化消息类型,注意B型机的系统是在FID_FETCH_DATA之前发送FID_INITIAL的。

MID alarm_module_list[MAX_ALARM_MID];

int FAR SYS_ALARM_proc( FID function_id, int handle )
{
  _UI i, j;

  switch ( function_id )
  {
    ... // program code
 
    case FID_INITAIL:
        for (i = 0; i < MAX_ALARM_MID; i++)
        {
          if (alarm_module_list== BAM_MODULE // **)
            || (alarm_module_list== LOCAL_MODULE)
          {

            for (j = 0; j < ALARM_CLASS_SUM; j++)
            {
                FAR_MALLOC( ... );
            }
          }
        }

        ... // program code

        break;
 
    case FID_FETCH_DATA:

        ... // program code

        Get_Alarm_Module( ); // 初始化alarm_module_list

        break;
 
    ... // program code
  }
}

由于FID_INITIAL是在FID_FETCH_DATA之前执行的,而初始化alarm_module_list是在FID_FETCH_DATA中进行的,故在FID_INITIAL中(**)处引用alarm_module_list变量时,它还没有被初始化。这是个严重错误。
应如下改正:要么把Get_Alarm_Module函数放在FID_INITIAL中(**)之前;要么就必须考虑(**)处的判断语句是否可以用(不使用alarm_module_list变量的)其它方式替代,或者是否可以取消此判断语句。
¹    9-14:编程时,要防止差1错误。
说明:此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。
¹    9-15:要时刻注意易混淆的操作符。当编完程序后,应从头至尾检查一遍这些操作符,以防止拼写错误。
说明:形式相近的操作符最容易引起误用,如C/C++中的“=”与“==”、“|”与“||”、“&”与“&&”等,若拼写错了,编译器不一定能够检查出来。
示例:如把“&”写成“&&”,或反之。
ret_flg = (pmsg->ret_flg & RETURN_MASK);
被写为:
ret_flg = (pmsg->ret_flg && RETURN_MASK);

rpt_flg = (VALID_TASK_NO( taskno ) && DATA_NOT_ZERO( stat_data ));
被写为:
rpt_flg = (VALID_TASK_NO( taskno ) & DATA_NOT_ZERO( stat_data ));
¹    9-16:有可能的话,if语句尽量加上else分支,对没有else分支的语句要小心对待;switch语句必须有default分支。
¹    9-17:Unix下,多线程的中的子线程退出必需采用主动退出方式,即子线程应return出口。
¹    9-18:不要滥用goto语句。
说明:goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。
½    9-1:不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性。
½    9-2:除非为了满足特殊需求,避免使用嵌入式汇编。
说明:程序中嵌入式汇编,一般都对可移植性有较大的影响。
½    9-3:精心地构造、划分子模块,并按“接口”部分及“内核”部分合理地组织子模块,以提高“内核”部分的可移植性和可重用性。
说明:对不同产品中的某个功能相同的模块,若能做到其内核部分完全或基本一致,那么无论对产品的测试、维护,还是对以后产品的升级都会有很大帮助。
½    9-4:精心构造算法,并对其性能、效率进行测试。
½    9-5:对较关键的算法最好使用其它算法来确认。
½    9-6:时刻注意表达式是否会上溢、下溢。
示例:如下程序将造成变量下溢。
unsigned char size ;
while (size-- >= 0) // 将出现下溢
{
  ... // program code
}

当size等于0时,再减1不会小于0,而是0xFF,故程序是一个死循环。应如下修改。
char size; // 从unsigned char 改为char
while (size-- >= 0)
{
  ... // program code
}
½    9-7:使用变量时要注意其边界值的情况。
示例:如C语言中字符型变量,有效值范围为-128到127。故以下表达式的计算存在一定风险。
char chr = 127;
int sum = 200;

chr += 1; // 127为chr的边界值,再加1将使chr上溢到-128,而不是128。
sum += chr; // 故sum的结果不是328,而是72。

若chr与sum为同一种类型,或表达式按如下方式书写,可能会好些。
sum = sum + chr + 1;
½    9-8:留心程序机器码大小(如指令空间大小、数据空间大小、堆栈空间大小等)是否超出系统有关限制。
½    9-9:为用户提供良好的接口界面,使用户能较充分地了解系统内部运行状态及有关系统出错情况。
½    9-10:系统应具有一定的容错能力,对一些错误事件(如用户误操作等)能进行自动补救。
½    9-11:对一些具有危险性的操作代码(如写硬盘、删数据等)要仔细考虑,防止对数据、硬件等的安全构成危害,以提高系统的安全性。
½    9-12:使用第三方提供的软件开发工具包或控件时,要注意以下几点:
(1)充分了解应用接口、使用环境及使用时注意事项。
(2)不能过分相信其正确性。
(3)除非必要,不要使用不熟悉的第三方工具包与控件。
说明:使用工具包与控件,可加快程序开发速度,节省时间,但使用之前一定对它有较充分的了解,同时第三方工具包与控件也有可能存在问题。
½    9-13:资源文件(多语言版本支持),如果资源是对语言敏感的,应让该资源与源代码文件脱离,具体方法有下面几种:使用单独的资源文件、DLL文件或其它单独的描述文件(如数据库格式)
10 代码编辑、编译、审查
¹    10-1:打开编译器的所有告警开关对程序进行编译。
¹    10-2:在产品软件(项目组)中,要统一编译开关选项。
¹    10-3:通过代码走读及审查方式对代码进行检查。
说明:代码走读主要是对程序的编程风格如注释、命名等以及编程时易出错的内容进行检查,可由开发人员自己或开发人员交叉的方式进行;代码审查主要是对程序实现的功能及程序的稳定性、安全性、可靠性等进行检查及评审,可通过自审、交叉审核或指定部门抽查等方式进行。
¹    10-:测试部测试产品之前,应对代码进行抽查及评审。
½    10-1:编写代码时要注意随时保存,并定期备份,防止由于断电、硬盘损坏等原因造成代码丢失。
½    10-2:同产品软件(项目组)内,最好使用相同的编辑器,并使用相同的设置选项。
说明:同一项目组最好采用相同的智能语言编辑器,如Muiti Editor,Visual Editor等,并设计、使用一套缩进宏及注释宏等,将缩进等问题交由编辑器处理。
½    10-3:要小心地使用编辑器提供的块拷贝功能编程。
说明:当某段代码与另一段代码的处理功能相似时,许多开发人员都用编辑器提供的块拷贝功能来完成这段代码的编写。由于程序功能相近,故所使用的变量、采用的表达式等在功能及命名上可能都很相近,所以使用块拷贝时要注意,除了修改相应的程序外,一定要把使用的每个变量仔细查看一遍,以改成正确的。不应指望编译器能查出所有这种错误,比如当使用的是全局变量时,就有可能使某种错误隐藏下来。
½    10-4:合理地设计软件系统目录,方便开发人员使用。
说明:方便、合理的软件系统目录,可提高工作效率。目录构造的原则是方便有关源程序的存储、查询、编译、链接等工作,同时目录中还应具有工作目录----所有的编译、链接等工作应在此目录中进行,工具目录----有关文件编辑器、文件查找等工具可存放在此目录中。
½    10-5:某些语句经编译后产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。
说明:在Borland C/C++中,可用“#pragma warn”来关掉或打开某些告警。
示例:
#pragma warn -rvl // 关闭告警
int examples_fun( void )
{
    // 程序,但无return语句。
}
#pragma warn +rvl // 打开告警
编译函数examples_fun时本应产生“函数应有返回值”告警,但由于关掉了此告警信息显示,所以编译时将不会产生此告警提示。
½    10-6:使用代码检查工具(如C语言用PC-Lint)对源程序检查。
½    10-7:使用软件工具(如 LogiSCOPE)进行代码审查。
11 代码测试、维护
¹    11-1:单元测试要求至少达到语句覆盖。
¹    11-2:单元测试开始要跟踪每一条语句,并观察数据流及变量的变化。
¹    11-3:清理、整理或优化后的代码要经过审查及测试。
¹    11-4:代码版本升级要经过严格测试。
¹    11-5:使用工具软件对代码版本进行维护。
¹    11-6:正式版本上软件的任何修改都应有详细的文档记录。
½    11-1:发现错误立即修改,并且要记录下来。
½    11-2:关键的代码在汇编级跟踪。
½    11-3:仔细设计并分析测试用例,使测试用例覆盖尽可能多的情况,以提高测试用例的效率。
½    11-4:尽可能模拟出程序的各种出错情况,对出错处理代码进行充分的测试。
½    11-5:仔细测试代码处理数据、变量的边界情况。
½    11-6:保留测试信息,以便分析、总结经验及进行更充分的测试。
½    11-7:不应通过“试”来解决问题,应寻找问题的根本原因。
½    11-8:对自动消失的错误进行分析,搞清楚错误是如何消失的。
½    11-9:修改错误不仅要治表,更要治本。
½    11-10:测试时应设法使很少发生的事件经常发生。
½    11-11:明确模块或函数处理哪些事件,并使它们经常发生。
½    11-12: 坚持在编码阶段就对代码进行彻底的单元测试,不要等以后的测试工作来发现问题。
½    11-13:去除代码运行的随机性(如去掉无用的数据、代码及尽可能防止并注意函数中的“内部寄存器”等),让函数运行的结果可预测,并使出现的错误可再现。

12 宏
¹    12-1:用宏定义表达式时,要使用完备的括号。
示例:如下定义的宏都存在一定的风险。
#define RECTANGLE_AREA( a, b ) a * b
#define RECTANGLE_AREA( a, b ) (a * b)
#define RECTANGLE_AREA( a, b ) (a) * (b)
正确的定义应为:
#define RECTANGLE_AREA( a, b ) ((a) * (b))
¹    12-2:将宏所定义的多条表达式放在大括号中。
示例:下面的语句只有宏的第一条表达式被执行。为了说明问题,for语句的书写稍不符规范。
#define INTI_RECT_VALUE( a, b )
  a = 0;
  b = 0;

for (index = 0; index < RECT_TOTAL_NUM; index++)
  INTI_RECT_VALUE( rect.a, rect.b );

正确的用法应为:
#define INTI_RECT_VALUE( a, b )
{
  a = 0;
  b = 0;
}

for (index = 0; index < RECT_TOTAL_NUM; index++)
{
  INTI_RECT_VALUE( rect[index].a, rect[index].b );
}
¹    12-3:使用宏时,不允许参数发生变化。
示例:如下用法可能导致错误。
#define SQUARE( a ) ((a) * (a))

int a = 5;
int b;
b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增1。

正确的用法是:
b = SQUARE( a );
a++; // 结果:a = 6,即只执行了一次增1。
 

- 作者: cjhacker 2006年10月13日, 星期五 17:09  回复(2) |  引用(0) 加入博采

偶像英爱!  (作者置顶)

- 作者: cjhacker 2006年09月16日, 星期六 11:49  回复(1) |  引用(0) 加入博采

Lucene倒排索引原理
Lucene是一个高性能的java全文检索工具包,它使用的是倒排文件索引结构。该结构及相应的生成算法如下:
  
  0)设有两篇文章1和2
  文章1的内容为:Tom lives in Guangzhou,I live in Guangzhou too.
  文章2的内容为:He nce lived in Shanghai.
  
  1)由于lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施
  a.我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间是连在一起的需要特殊的分词处理。
  b.文章中的”in”, “once” “too”等词没有什么实际意义,中文中的“的”“是”等字通常也无具体含义,这些不代表概念的词可以过滤掉
  c.用户通常希望查“He”时能把含“he”,“HE”的文章也找出来,所以所有单词需要统一大小写。
  d.用户通常希望查“live”时能把含“lives”,“lived”的文章也找出来,所以需要把“lives”,“lived”还原成“live”
  e.文章中的标点符号通常不表示某种概念,也可以过滤掉
  在lucene中以上措施由Analyzer类完成
  
  经过上面处理后
   文章1的所有关键词为:[tom] [live] [guangzhou] [i] [live] [guangzhou]
   文章2的所有关键词为:[he] [live] [shanghai]
  
  2) 有了关键词后,我们就可以建立倒排索引了。上面的对应关系是:“文章号”对“文章中所有关键词”。倒排索引把这个关系倒过来,变成:“关键词”对“拥有该关键词的所有文章号”。文章1,2经过倒排后变成
  关键词 文章号
  guangzhou 1
  he 2
  i 1
  live 1,2
  shanghai 2
  tom 1
  
  通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置:a)字符位置,即记录该词是文章中第几个字符(优点是关键词亮显时定位快);b)关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),lucene 中记录的就是这种位置。
  
  加上“出现频率”和“出现位置”信息后,我们的索引结构变为:
  关键词 文章号[出现频率] 出现位置
  guangzhou 1[2] 3,6
  he 2[1] 1
  i 1[1] 4
  live 1[2],2[1] 2,5,2
  shanghai 2[1] 3
  tom 1[1] 1
  
  以live 这行为例我们说明一下该结构:live在文章1中出现了2次,文章2中出现了一次,它的出现位置为“2,5,2”这表示什么呢?我们需要结合文章号和出现频率来分析,文章1中出现了2次,那么“2,5”就表示live在文章1中出现的两个位置,文章2中出现了一次,剩下的“2”就表示live是文章2中第 2个关键字。
  
  以上就是lucene索引结构中最核心的部分。我们注意到关键字是按字符顺序排列的(lucene没有使用B树结构),因此lucene可以用二元搜索算法快速定位关键词。
  
  实现时 lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件 (positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。
  
   Lucene中使用了field的概念,用于表达信息所在位置(如标题中,文章中,url中),在建索引中,该field信息也记录在词典文件中,每个关键词都有一个field信息(因为每个关键字一定属于一个或多个field)。
  
  为了减小索引文件的大小,Lucene对索引还使用了压缩技术。首先,对词典文件中的关键词进行了压缩,关键词压缩为<前缀长度,后缀>,例如:当前词为“阿拉伯语”,上一个词为“阿拉伯”,那么“阿拉伯语”压缩为<3,语>。其次大量用到的是对数字的压缩,数字只保存与上一个值的差值(这样可以减小数字的长度,进而减少保存该数字需要的字节数)。例如当前文章号是16389(不压缩要用3个字节保存),上一文章号是16382,压缩后保存7(只用一个字节)。
  
   下面我们可以通过对该索引的查询来解释一下为什么要建立索引。
  假设要查询单词 “live”,lucene先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因而,整个过程的时间是毫秒级的。
  而用普通的顺序匹配算法,不建索引,而是对所有文章的内容进行字符串匹配,这个过程将会相当缓慢,当文章数目很大时,时间往往是无法忍受的。
 

- 作者: cjhacker 2007年07月20日, 星期五 09:59  回复(3) |  引用(0) 加入博采

我的股票预测!

推荐黑马:三房巷(600370)

目标价位:至少30

等着瞧吧!

- 作者: cjhacker 2007年06月21日, 星期四 16:30  回复(2) |  引用(0) 加入博采

MYSQL初学者使用介绍

其实MYSQL的对数据库的操作与其它的SQL类数据库大同小异,您最好找本将SQL的书看看。我在这里只介绍一些基本的,其实我也就只懂这些了,呵呵。

一、连接MYSQL。
格式: mysql -h主机地址 -u用户名 -p用户密码
1、例1:连接到本机上的MYSQL。
首先在打开DOS窗口,然后进入目录 mysqlbin,再键入命令mysql -uroot -p,回车后提示你输密码,如果刚安装好MYSQL,超级用户root是没有密码的,故直接回车即可进入到MYSQL中了,MYSQL的提示符是:mysql>

2、例2:连接到远程主机上的MYSQL。假设远程主机的IP为:110.110.110.110,用户名为root,密码为abcd123。则键入以下命令:
mysql -h110.110.110.110 -uroot -pabcd123
(注:u与root可以不用加空格,其它也一样)

3、退出MYSQL命令: exit (回车)

二、修改密码。
格式:mysqladmin -u用户名 -p旧密码 password 新密码
1、例1:给root加个密码ab12。首先在DOS下进入目录mysqlbin,然后键入以下命令
mysqladmin -uroot -password ab12
注:因为开始时root没有密码,所以-p旧密码一项就可以省略了。
2、例2:再将root的密码改为djg345。
mysqladmin -uroot -pab12 password djg345
三、增加新用户。(注意:和上面不同,下面的因为是MYSQL环境中的命令,所以后面都带一个分号作为命令结束符)
格式:grant select on 数据库.* to 用户名@登录主机 identified by "密码"
例1、增加一个用户test1密码为abc,让他可以在任何主机上登录,并对所有数据库有查询、插入、修改、删除的权限。首先用以root户连入MYSQL,然后键入以下命令:
grant select,insert,update,delete on *.* to test1@"%" Identified by "abc";
但例1增加的用户是十分危险的,你想如某个人知道test1的密码,那么他就可以在internet上的任何一台电脑上登录你的mysql数据库并对你的数据可以为所欲为了,解决办法见例2。
例2、增加一个用户test2密码为abc,让他只可以在localhost上登录,并可以对数据库mydb进行查询、插入、修改、删除的操作(localhost指本地主机,即MYSQL数据库所在的那台主机),这样用户即使用知道test2的密码,他也无法从internet上直接访问数据库,只能通过MYSQL主机上的web页来访问了。
grant select,insert,update,delete on mydb.* to test2@localhost identified by "abc";
如果你不想test2有密码,可以再打一个命令将密码消掉。
grant select,insert,update,delete on mydb.* to test2@localhost identified by "";


我们来看看MYSQL中有关数据库方面的操作。注意:你必须首先登录到MYSQL中,以下操作都是在MYSQL的提示符下进行的,而且每个命令以分号结束。

一、操作技巧
1、如果你打命令时,回车后发现忘记加分号,你无须重打一遍命令,只要打个分号回车就可以了。也就是说你可以把一个完整的命令分成几行来打,完后用分号作结束标志就OK。
2、你可以使用光标上下键调出以前的命令。但以前我用过的一个MYSQL旧版本不支持。我现在用的是mysql-3.23.27-beta-win。

二、显示命令
1、显示数据库列表。
show databases;
刚开始时才两个数据库:mysql和test。mysql库很重要它里面有MYSQL的系统信息,我们改密码和新增用户,实际上就是用这个库进行操作。
2、显示库中的数据表:
use mysql; //打开库,学过FOXBASE的一定不会陌生吧
show tables;
3、显示数据表的结构:
describe 表名;
4、建库:
create database 库名;
5、建表:
use 库名;
create table 表名 (字段设定列表);
6、删库和删表:
drop database 库名;
drop table 表名;
7、将表中记录清空:
delete from 表名;
8、显示表中的记录:
select * from 表名;

三、一个建库和建表以及插入数据的实例
drop database if exists school; //如果存在SCHOOL则删除
create database school; //建立库SCHOOL
use school; //打开库SCHOOL
create table teacher //建立表TEACHER
(
id int(3) auto_increment not null primary key,
name char(10) not null,
address varchar(50) default '深圳',
year date
); //建表结束
//以下为插入字段
insert into teacher values('','glchengang','深圳一中','1976-10-10');
insert into teacher values('','jack','深圳一中','1975-12-23');

注:在建表中(1)将ID设为长度为3的数字字段:int(3)并让它每个记录自动加一:auto_increment并不能为空:not null而且让他成为主字段primary key(2)将NAME设为长度为10的字符字段(3)将ADDRESS设为长度50的字符字段,而且缺省值为深圳。varchar和char有什么区别呢,只有等以后的文章再说了。 (4)将YEAR设为日期字段。
如果你在mysql提示符键入上面的命令也可以,但不方便调试。你可以将以上命令原样写入一个文本文件中假设为school.sql,然后复制到c:\下,并在DOS状态进入目录\mysql\bin,然后键入以下命令:
mysql -uroot -p密码 < c:\school.sql
如果成功,空出一行无任何显示;如有错误,会有提示。(以上命令已经调试,你只要将//的注释去掉即可使用)。

四、将文本数据转到数据库中
1、文本数据应符合的格式:字段数据之间用tab键隔开,null值用\n来代替.
例:
3 rose 深圳二中 1976-10-10
4 mike 深圳一中 1975-12-23
2、数据传入命令 load data local infile "文件名" into table 表名;
注意:你最好将文件复制到\mysql\bin目录下,并且要先用use命令打表所在的库 。


五、备份数据库:(命令在DOS的\mysql\bin目录下执行)
mysqldump --opt school>school.bbb
注释:将数据库school备份到school.bbb文件,school.bbb是一个文本文件,文件名任取,打开看看你会有新发现。

后记:其实MYSQL的对数据库的操作与其它的SQL类数据库大同小异,您最好找本将SQL的书看看。我在这里只介绍一些基本的,其实我也就只懂这些了,呵呵。最好的MYSQL教程还是“晏子“译的“MYSQL中文参考手册“不仅免费每个相关网站都有下载,而且它是最权威的。可惜不是象"PHP4中文手册"那样是chm的格式,在查找函数命令的时候不太方便。

- 作者: cjhacker 2007年05月26日, 星期六 17:41  回复(2) |  引用(0) 加入博采

crack新手进化篇(新手级)
软件怎么判断我们是否注册了
不要忘了,软件最终是按照人的思维做的,我们回到自身来,“如果是你,你怎么判断别人是否注册了呢”,“我要别人输入用户名和注册码啊”,聪明的想法,很多软件也是这样做的,如豪杰超级解霸。(但是不是所以的软件,方法太多了,友情提示:这个世界没有完全通用的东西,除了你聪明的大脑)
具体一点呢??????????
我们把用户名按照某种方法运算得到一个真正的注册码和用户输入的进行比较不就知道了吗?Yeah,也就是
真正的注册码  =  f(用户名)
和Y  =  f(x) 是一样的
然后就是很经典的比较了,为什么说经典呢?
因为大概有60%的软件是这么做的,到底是什么比较呢,看看



请注意这里会有错误处理的噢,在这之前呢,就是经典比较啊,如果这里的错误处理提示我们诸如:注册错误之类的东西,我们就很容易定位到经典比较了。
那么上面的流程在汇编语言里面是怎么实现的呢?
        比较有2种方式,直接和间接,直接就是用:
    cmp x, y  
    je (jne) label
这里的x和y只是一个符号,实际上可能是寄存器和存储器
间接的呢?调用一个子程序比较,如下面的代码
if (strcmp(&x, & y))  //如果strcmp返回值是1
  printf(“right”);
else……………..//当然是错误拉
这里的strcmp也只是一个符号,现实可能有变化
用汇编语言描述呢?
  push &y;
  push &x;
  call strcmp;
  test ax,ax  ;判断返回值(也就是出口参数,也可以在子程序里判断)
  je……

2.为什么可以调试可执行程序呢
可能很多菜鸟有我这样的疑问,呵呵
调试可执行程序的理论基础:
我们都知道在汇编语言里面可以用debug来调试程序。但是为什么可以?
其实和简单,因为机器只识别的是0和1(准确的说你高电平和底电平,你可以简单的理解为灯泡亮和黑),我们称之为机器码,而我们的汇编语言与机器码是一一对应的,所以我们可以根据机器码得到对应的汇编代码,也可以反过来通过汇编代码得到对应的机器码,
如:在debug下我们可以看到
用debug测试一下:
-a
1370:0100 mov ax,bx
1370:0102
-u100    
1370:0100 89D8          MOV     AX,BX
这里的89D8就是MOV     AX,BX的机器码
3.破解教程都告诉我们,找到错误提示上面的第一个有条件跳转,改掉就可以爆破,为什么呢?因为程序是顺序执行的,只要我们找到提示出错的地方,那么在此之前必定已经比较完了,所以再往前面找就看到了关键的比较,关键的地方就是上面的比较,如果我们改变的判断条件呢?如果改成不相等就注册成功,那么。。(嘿嘿,某同志传来不怀好意的笑声),那么不管我们输入什么都是“正版”的了,没有交钱的“正版”,这就是我们改变跳转的原因,也就是改变改变的判断条件

5.下面我们开始实战演习,虽然这只是一个简单的用S-Demo做的动画,但是看完了下面的文章你还是会收获很多,不相信,我晕,把简单的事情做到极限就成功了,呵呵
(1).爆破
这个很简单就不多讲了,运行程序,随便输入密码,确定,提示:“password wrong”,
用ollydbg载入这个动画,查找程序用到的字符串,找到password wrong,下个断点,还记得前面的理论吗?
再向前找找就可以找到比较的关键地方了(条件跳转),好的,找到的地址是:0040203F
0040202A  |. FFB6 A4000000  PUSH DWORD PTR DS:[ESI+A4]               ; /s2
下个断点,看看到底压入了堆栈什么
00402030  |. 8D45 CC        LEA EAX,DWORD PTR SS:[EBP-34]            ; |
00402033  |. 50             PUSH EAX                                 ; |s1
00402034  |. FF15 E0234100  CALL DWORD PTR DS:[<&MSVCRT._stricmp>]   ; \_stricmp    //这里看到了什么,stricmp,难道这就是传说中的关键比较吗?
但是比较之后没有跳转啊,是吗?仔细看看,比较之后的结果放在那里,eax !
下面不是有一个je吗?
0040203A  |. 83C4 20        ADD ESP,20  ;平衡堆栈
0040203D  |. 85C0           TEST EAX,EAX    ;测试返回值
0040203F  |. 74 15          JE SHORT test.00402056
修改为jne,保存,运行,ok,搞定,请你再次回顾前面的流程图
(2).寻寻觅觅找密码
还记得前面的流程图吗?如果我们在程序比较的时候中断程序会有什么发现呢?呵呵,这个时候会看到真正的密码,重新用ollydbg载入这个动画,下断点00402034
为什么要在这里下断点?好问题,因为这里看到了call DWORD PTR DS:[<&MSVCRT._stricmp>],看看流程图,明白了吗,呵呵
这里我们在堆栈区域可以看到我们输入试炼码和真正的密码,为什么在堆栈区域,因为在windows下通过堆栈传递参数。请看上面的简单分析,破解补丁的编写
(3).文件补丁的编写(c语言简单实现)
前面我们把je改成了jne,随便输入密码都可以了
实质是把机器码由74h改成75h,因为机器码和汇编指令是一一对应的,那么我们只要写个小东西,修改就可以了。我已经写好了,很短,很好懂。Crack.c,我们分析一下
#include <stdio.h>
#include <string.h>
int main(void)
{
  FILE *fp_out;  //要写入的文件
  printf("\n\t\t\t\t Copy Right by ngaut\n");
  printf("Cracking......\n");
//打开文件test.exe 
  if ((fp_out = fopen("test.exe", "r+"))==NULL)
  {                    
    printf("error!!!  Can not open test.exe!!!\n\n");
    printf("Press any key to continue\n");
    getchar();
    exit(0);
  }
//定位到要修改的地方,这里是 0x203f,为什么呢?下面给出回答
    fseek(fp_out, 0x203f, SEEK_SET);
    fputc(0x75, fp_out);        //写入数据0x75,也就是把机器码74改为75,
 //汇编则是 je 改为了jne 
    fclose(fp_out);

这里 0x203f = 0x0040203F – 0x00400000
(4).让程序自动弹出正确的密码(过几天给出,太忙了)

- 作者: cjhacker 2007年05月26日, 星期六 11:29  回复(2) |  引用(0) 加入博采

商朝子: 第五章--破解原理

第五章--破解原理
从本章开始,我们来一步一步学习Crack软件(80%读者昏死过去,且不省人世...另有20%在寻找附近可以用来打人的东西)
不可不说一下学习破解的三个阶段:
初级,修改程序,用ultraedit等工具修改exe文件,称暴力破解,简称爆破
中级,追出软件的注册码
高级,写出注册机
先说这爆破。所谓爆破,就是指通过修改可执行文件的源文件,来达到相应的目的。你不明白?呵呵,举个例子好了,比如说某共享软件,它比较用户输入的注册码,如果用户输入的,跟它通过用户名(或其它)算出来的注册码相等的话(也就是说用户输入的注册码正确了),那么它就会跳到注册成功的地方去,否则就跳到出错的地方去。
明白过来了吧,我们只要找到这个跳转指令,把它修改为我们需要的“造型”,这样,我们是不是就可以为所欲为了?(某软件双手放在胸口,你要干嘛?)
常见的修改方法有两种,我给你举例说明:
no.1
在某软件中,这样来进行注册:
00451239 CALL 00405E02  (关键CALL,用来判断用户输入的注册码是否正确)
0045123D JZ 004572E6   (!!!<--此为关键跳转,如果用户输入的注册码正确,就跳向成功处,即004572E6处)
0045XXXX YYYYYYYYYY
XXXXXXXX YYYYYYYYYY
XXXXXXXX YYYYYYYYYY
XXXXXXXX 执行到此处,就提示用户注册失败
...提示用户注册码不正确等相关信息
...
004572E6 ...  <--(注册成功处!!!)
...提示用户注册成功等相关信息
呵呵,看明白了吗?没有的话,我来给你讲一下。在软件执行到00451239处的时候,CALL置0045E02处来进行注册码判断。接着回来后就来一个跳转语句,即如果用户输入的注册码正确就跳到004572E6处,跳到此处,就算是注册成功了。如果用户输入的注册码不正确的话,那么就不会在0045123D处进行跳转,而一直执行下去。在下面等它的,是注册失败部分。
想明白了吗?嘿嘿...没错,我们只要把那个关键跳转JZ给改为JNZ(如果用户输入的注册码错误,就注册成功,输入正确则注册失败)。当然你也可以将JNZ修改为Jmp,这样的话,你输入的注册码无论正确与否。都可以注册成功。
no.2
我们再来讲一下另外的一种情况:
00451239 CALL 00405E02  (关键CALL,用来判断用户输入的注册码是否正确)
005123D JNZ 004572E6   (!!!<--此为关键跳转,如果用户输入的注册码不正确,就跳向失败处,即004572E6处)
0045XXXX YYYYYYYYYY
XXXXXXXX YYYYYYYYYY
XXXXXXXX YYYYYYYYYY
XXXXXXXX 执行到此处,就提示用户注册成功
...提示用户注册成功等相关信息
...
004572E6 ...  <--(注册失败处!!!)
...提示用户注册码不正确等相关信息
这次我相信,并且深信不疑。你一定明白了。我还是不明白...倒...
你一定看出跟第一种情况不同的地方了吧。没错!它与第一种不同的,就是第一种情况是如果注册码正确,就跳到注册成功处,如果没有跳走,就会执行到失败处。而这一种情况则是如果注册码不正确,就跳到注册失败处,否则将执行到注册成功处。
这种情况的修改,除了把JNZ改为JZ外,还可以将其改为Nop,Nop这个指令没有任何意义,将该条指令修改为Nop后,便可随意输入注册码来进行注册了。
原理以经给你讲了,下面我们再来讲一下具体的修改办法吧。(我假设你以经明白了我所说的工具的使用方法)
先说一下虚拟地址和偏移量转换的问题,在SoftICE和W32Dasm下显示的地址值是所谓的内存地址(memory offset),或称之为虚拟地址(Virual Address,VA)。而十六进制工具里,如:Hiew、Hex Workshop等显示的地址就是文件地址,称之为偏移量(File offset) 或物理地址(RAW offset)。
所以当我们要通过那些十六进制工具来对可执行文件中的相应指令进行修改的话,先要找到它的File offset。我们没有必要去使用那些专门的转换工具,在W32Dasm中就有这个功能,比如说你W32Dasm中来到0045123D处,在W32Dasm界面下方的状态栏中就会出现该条指令的虚拟地址和偏移地址,即@:0045123D @offset 0005063Dh 后面的这个0005063Dh就是相应的偏移地址。我们得到该地址后,便可用UltraEdit等十六进制工具来对可执行文件进行修改了。比如使用UltraEdit,你先用UltraEdit打开该可执行文件,然后按Ctrl+G,接着输入你得到的偏移地址,就可以来到其相应的机器码处。
再给你讲一下机器码,所谓的机器码。就是你看到的那些个十六进制数据了。还记的它们与汇编指令是一一对应的吗?
以下这几个是爆破时要用到的,其它的如果感兴趣,可自行查看相关资料:
JZ=74;JNZ=75;JMP=EB;Nop=90
爆破的时候,只要对以上机器码进行相应的修改就行了,比如第一种情况的时候,可以将74修改为EB,即将JZ修改为JMP。而第二种情况,责需将75修改为90,即将JNZ修改为Nop。
由于本章只讲原理,具体一点的。如怎样找到关键跳转等,我们在下一章中再讲。(一个砖头飞了上来!嘿嘿,这次被俺接到了)
上边讲了爆破的原理,你需要明白的是。爆破只是你学习Crack的开始,是很简单的手段。刚入门的时候可以玩玩儿,但希望你不要就此不前!
(嘿嘿,再说了。人家的软件中不是都说了嘛,不准对其进行逆向修改。你动了人家的身子,怎么能不买帐呢?
偶就不喜欢爆破,做不出注册机也要找出注册码。否则我就不会去注册这个软件,既然想不掏钱,就要靠你自己的本事。(等以后我有钱了,会考虑去注册那些优秀的共享软件的 )。所以,从某种意义上来说,我是一个正人君子  
其实要找到注册码并不是一件多么难的事,我是指你所针对的软件不太那个的时候 不过你无需惧怕。
刚才我们说爆破的时候不提到过关键CALL吗?一般情况下,这个关键CALL就是对两个注册码(一个是软件自身通过你的注册名或机器什么的计算出来的正确的注册码,令一个就是你输入的错误的注册码)进行比较。我前边提到过,CALL之前一般会把所用到的数据先放到一个地方,CALL过去的时候再从这些地方把先前放入的数据取出来,进行相应的处理。这个关键CALL也是这样,在CALL之前,一般会把那两个注册码放到堆栈或某个寄存器中。嘿嘿,我们只要在调试器中,单步执行到该CALL,在未进去之前通过CALL之前的指令判断其将正确的和不正确的注册码放到哪里了。然后再用相应指令进行查看就成了,我说过不难的。
下面列出两个最常见的情况(可参考相关教程):
no.1
mov  eax [      ]  这里可以是地址,也可以是其它寄存器
mov  edx [      ]  同上,该条指令也可以是pop edx
call 00??????   关键call
test eax eax      
jz(jnz)或jne(je)  关键跳转
看明白了吧,在关键CALL之前,软件会把两个注册码分别放入eax和edx中,你只要在CALL处下d eax或d edx就能看到正确的注册码了。
no.2
mov  eax [      ]  这里可以是地址,也可以是其它寄存器
mov  edx [      ]  同上,该条指令也可以是pop edx
call 00??????   关键call
jne(je)         关键跳转
以上两种情况最为常见,而那些个不太常见的情况,我们这里就不再提了。到下下一章的时候,我会给你讲相关方法的...
关于查找软件注册码的部分,就到这里。具体内容,下下一章咱们再说。(不是说了吗?我以经可以接到你的砖头了,干嘛还要丢呢? )
最后,再来说最后的所谓的高级阶段,如果你相信自己。并且热爱Crack,那么你一定会熬到这个阶段的,只是时间因人而异。
其实分析软件的算法,是有好多技巧在里面的。呵呵,最起码我刚开始的时候就摸不着头脑,那么多CALL,每个看起来,都很重要,都追一遍?结果连好多API都被追了进去。等你自己真正用心分析了一个软件的算法,并写出了注册机后。你就会明白其中的道理了,我们下下下一章再说。(大哥,你不是吧,连你家太阳能都丢过来了
<本章完>

- 作者: cjhacker 2007年05月26日, 星期六 11:22  回复(2) |  引用(0) 加入博采

test和cmp一个很菜很基础的话题!
看过破解教程,都知道test,cmp是比较关键,可是我一直不清楚它们究竟是怎么比较的,最后下决心找了很多资料,和大家一起把它们弄清楚.

首先看看:状态寄存器(即标志寄存器)

PSW(Program Flag)程序状态字(即标志)寄存器,是一个16位寄存器,由条件码标志(flag)和控制标志构成,
如下所示:

15 14 13 12 11 10 9  8  7  6  5  4  3  2  1  0
        OF DF IF TF SF ZF   AF   PF   CF

条件码:
①OF(Overflow Flag)溢出标志,溢出时为1,否则置0.标明一个溢出了的计算,如:结构和目标不匹配.
②SF(Sign Flag)符号标志,结果为负时置1,否则置0.
③ZF(Zero Flag)零标志,运算结果为0时置1,否则置0.
④CF(Carry Flag)进位标志,进位时置1,否则置0.注意:Carry标志中存放计算后最右的位.
⑤AF(Auxiliary carry Flag)辅助进位标志,记录运算时第3位(半个字节)产生的进位置。
    有进位时1,否则置0.
⑥PF(Parity Flag)奇偶标志.结果操作数中1的个数为偶数时置1,否则置0.

控制标志位:
⑦DF(Direction Flag)方向标志,在串处理指令中控制信息的方向。
⑧IF(Interrupt Flag)中断标志。
⑨TF(Trap Flag)陷井标志。


为举例方便说一下jnz和jz
    测试条件
JZ   ZF=1
JNZ  ZF=0
即Jz=jump if zero (结果为0则设置ZF零标志为1,跳转)
Jnz=jump if not zero

好,接着来看test和cmp

******************************************************************************
test属于逻辑运算指令

功能: 执行BIT与BIT之间的逻辑运算
     测试(两操作数作与运算,仅修改标志位,不回送结果).
Test对两个参数(目标,源)执行AND逻辑操作,并根据结果设置标志寄存器,结果本身不会保存。EST AX,BX 与 AND AX,BX 命令有相同效果

语法: TEST r/m,r/m/data
影响标志: C,O,P,Z,S(其中C与O两个标志会被设为0)

运用举例:
1.Test用来测试一个位,例如寄存器:

test eax, 100b;          b后缀意为二进制
jnz  ******;             如果eax右数第三个位为1,jnz将会跳转

我是这样想的,jnz跳转的条件是ZF=0,ZF=0意味着ZF(零标志)没被置位,即逻辑与结果为1.

2.Test的一个非常普遍的用法是用来测试一方寄存器是否为空:

test ecx, ecx
jz somewhere

如果ecx为零,设置ZF零标志为1,Jz跳转

*******************************************************************************
CMP属于算术运算指令

功能: 比较两个值(寄存器,内存,直接数值)
语法: CMP r/m,r/m/data
标志位: C,P,A,Z,O

CMP比较.(两操作数作减法,仅修改标志位,不回送结果).
cmp实际上是只设置标志不保存结构的减法,并设置Z-flag(零标志).
零标志很像carry,也是内部标志寄存器的一位.

例如:
Cmp eax, 2;       如果eax-2=0即eax=2就设置零标志为1
Jz ****;          如果设置了零标志就跳转


*******************************************************************************
我得出的结论
test逻辑与运算结果为零,就把ZF(零标志)置1;
cmp 算术减法运算结果为零,就把ZF(零标志)置1.

结论很简单嘛,之前我怎么就分不清呢,真是笨哪!

- 作者: cjhacker 2007年05月26日, 星期六 10:04  回复(3) |  引用(0) 加入博采

c++作业之运算符第4题

#include <iostream>

using namespace std;

const int SETSIZE = 500;

class Set
{
private:
 int member[SETSIZE];
public:
 Set()
 {
  for(int i=0;i<SETSIZE;i++)
   member[i]=false;
 }
 Set(int a[],int n)
 {
  for(int i=0;i<SETSIZE;i++)
   member[i]=false;
  for(i=0;i<n;i++)
   member[a[i]]=true;
 }
 int Insert(int n);
 int Delete(int n);
 friend Set operator +(Set &s1,Set &s2);
 friend Set operator *(Set &s1,Set &s2);
 friend bool operator ^(int n,Set &s);
 void display();
};

int Set::Insert(int n)
{
 if (n<SETSIZE)
 {
  member[n]=true;
  return 1;
 }
 else
  return 0;//插入失败
}

int Set::Delete(int n)
{
 if (n<SETSIZE)
 {
  member[n]=false;
  return 1;
 }
 else
  return 0;//删除失败
}

Set operator +(Set &s1,Set &s2)
{
 Set temp;
 for(int i=0;i<SETSIZE;i++)
  if (s1.member[i] || s2.member[i])
   temp.member[i]=true;
 return temp;
}

Set operator *(Set &s1,Set &s2)
{
 Set temp;
 for(int i=0;i<SETSIZE;i++)
  if (s1.member[i] && s2.member[i])
   temp.member[i]=true;
 return temp;
}

bool operator ^(int n,Set &s)
{
 if (s.member[n])
  return true;
 else
  return false;
}

void Set::display()
{
 cout << "{";
 for(int i=0;i<SETSIZE;i++)
  if (member[i])
   cout << i << ",";
 cout << "\b}";
}

int fillSet()
{
 Set p;
 for(int i=0;i<5;i++)
 {
  p.Insert(rand() % 5);
 }
 for(i=0;i<5;i++)
 {
  if(!(i^p))
   return 0;
 }
 return 1;
}
int main()

 int a[]={1, 5, 7, 12, 24, 36, 45, 103, 355, 499};
 int b[]={2, 3, 5, 7, 8, 9, 12, 15, 36, 45, 103, 255, 355, 498};
 Set S(a,10),T(b,14),U;
 for (int i=0;i<50;i++)
  U.Insert(i+1);
 cout << "2." << endl;
 cout << "S+T= " ;(S+T).display();cout << endl;
 cout << endl << "3." << endl;
 cout << "S*T= " ;(S*T).display();cout << endl;
 cout << endl << "4." << endl;
 cout << "S*U= " ;(S*U).display();cout << endl;
 cout << endl << "5." << endl;
 cout << "删除前:";T.display();cout << endl;
 T.Delete(8);T.Delete(36);T.Delete(103);T.Delete(498);
 cout << "删除后:";T.display();cout << endl;
 cout << endl << "6." << endl;
 int temp;
 cout << "集合S: ";S.display();cout << endl;
 for(i=0;i<5;i++)
 {
  temp=rand()%9+1;
  cout << "生成随机数(1-9):" << temp ;
  if (temp^S)
   cout << " 该数在集合S中" << endl;
  else
   cout << " 该数不在集合S中" << endl;
 }
 cout << endl << "7." << endl;
 int n=0;
 for (long j=0;j<100000;j++)
  if(fillSet()) n++;
 cout << "概率是: " << n/100000.0 << endl;
 return 0;
}

- 作者: cjhacker 2007年05月3日, 星期四 20:41  回复(1) |  引用(0) 加入博采

c++作业之运算符重载第2题

#include <iostream>
#include <math.h>
using namespace std;
class Complex
{
public:
 Complex()
 {
  real=0.0;
  imag=0.0;
 }
 Complex(double r,double i)//如果在参数里设默认值会和下面冲突
 {
  real=r;
  imag=i;
 }
 Complex(double r)//转换构造函数
 {
  real=r;
  imag=0.0;
 }

 friend Complex operator + (Complex &c1,Complex &c2);
 friend Complex operator - (Complex &c1,Complex &c2);
 friend Complex operator * (Complex &c1,Complex &c2);
 friend Complex operator / (Complex &c1,Complex &c2);
 Complex operator - ();
 double magnitude();
 double argz();
 Complex conjugate();
 friend ostream &operator << (ostream &output,Complex &c);
 friend istream &operator >> (istream &input,Complex &c);
 double getreal();
 double getimag();
private:
 double real;
 double imag;
};

Complex operator + (Complex &c1,Complex &c2)//加:u + v = (a + c) + i(b + d)
{
 return Complex(c1.real+c2.real,c1.imag+c2.imag);

}

Complex operator - (Complex &c1,Complex &c2)//减:u - v = (a - c) + i(b - d)
{
 return Complex(c1.real-c2.real,c1.imag-c2.imag);

}

Complex operator * (Complex &c1,Complex &c2)//乘:u * v = (a*c - b*d) + i(b*c + a*d)
{
 return Complex(c1.real*c2.real-c1.imag*c2.imag,c1.imag*c2.real+c1.real*c2.imag);
}

Complex operator / (Complex &c1,Complex &c2)//除:u / v = (a*c + b*d) / (c2 + d2) + i((b*c - a*d) / (c2 + d2))
{
 return Complex((c1.real*c2.real+c1.imag*c2.imag)/(c2.real*c2.real+c2.imag*c2.imag)
         ,(c1.imag*c2.real-c1.real*c2.imag)/(c2.real*c2.real+c2.imag*c2.imag));
}

Complex Complex::operator - ()//求反:-u = -a + i(-b)
{
 return Complex(-real,-imag);
}

double Complex::magnitude()//求模
{
 return sqrt(real*real+imag*imag);
}

double Complex::argz()//求幅角
{
 return atan(imag/real)*180/3.14159;
}

Complex Complex::conjugate()//求共轭
{
 return Complex(real,-imag);
}


ostream& operator << (ostream& output,Complex& c)//重载<<
{
 if(c.imag>=0)
  output << "(" << c.real << "+" << c.imag << "i)";
 else
  output << "(" << c.real << c.imag << "i)";
 return output;
}

istream& operator >> (istream& input,Complex& c)//重载>>
{
 cout << "input real part and imaginary part of complex number:";
 input >> c.real >> c.imag;
 return input;
}

double Complex::getreal()//实部属性访问
{
 return real;
}

double Complex::getimag()//虚部属性访问
{
 return imag;
}

double Distance(Complex& a, Complex& b)//求两向量间距离
{
 return sqrt((a.getreal()-b.getreal())*(a.getreal()-b.getreal())
  +(a.getimag()-b.getimag())*(a.getimag()-b.getimag()));
}
int main()
{
 Complex c1(1,1),c2(5,0),c3(0,-7);
 cout << "1." << endl;
 cout << "c1=" << c1 << endl;
 cout << "c2=" << c2 << endl;
 cout << "c3=" << c3 << endl;

 cout << endl << "2." << endl;
 cout << "i*i=" << Complex(0,1)*Complex(0,1) << endl;

 cout << endl << "3." << endl;
 cout << "c1共轭的模是 " << c1.conjugate().magnitude()<< " 幅角是 " << c1.conjugate().argz() << endl;
 cout << "c2共轭的模是 " << c2.conjugate().magnitude()<< " 幅角是 " << c2.conjugate().argz() << endl;
 cout << "c3共轭的模是 " << c3.conjugate().magnitude()<< " 幅角是 " << c3.conjugate().argz() << endl;

 cout << endl << "4." << endl;
 cout << "c1到c2的距离是 " << Distance(c1,c2) << endl;
 cout << "c1到c3的距离是 " << Distance(c1,c3) << endl;
 cout << "c2到c3的距离是 " << Distance(c2,c3) << endl;

 cout << endl << "5." << endl;
 Complex c[5]={
  Complex(2,3),
  Complex(-1,1),
  Complex(1,1),
  Complex(1,-1),
  Complex(1,0)
 };
 for(int i=0;i<5;i++)
 {
  if ((c[i]*c[i]*c[i]-Complex(3)*c[i]*c[i]+Complex(4)*c[i]-Complex(2)).magnitude()==0)
   cout << c[i] << endl;
 }
 return 0;
}

- 作者: cjhacker 2007年05月3日, 星期四 14:36  回复(2) |  引用(0) 加入博采

C/C++字节对齐

什么是对齐,以及为什么要对齐:
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32 位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效
率上下降很多。这也是空间和时间的博弈。
对齐的实现
通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。
但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
对齐的算法:
由于各个平台和编译器的不同,现以本人使用的gcc version 3.2.2编译器(32位x86平台)为例子,来讨论编译器对struct数据结构中的各成员如何进行对齐的。
设结构体如下定义:
struct A
{
int a;
char b;
short c;
};
结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐。
所以使用sizeof(strcut A)值为8。
现在把该结构体调整成员变量的顺序。
struct B
{
char b;
int a;
short c;
};
这时候同样是总共7个字节的变量,但是sizeof(struct B)的值却是12。
下面我们使用预编译指令#progma pack ()来告诉编译器,使用我们指定的对齐值来取代缺省的。
#progma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct C)值是8。

修改对齐值为1:
#progma pack (1) /*指定按1字节对齐*/
struct D
{
char b;
int a;
short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct D)值为7。

对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
这里面有四个概念值:
1.数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。
2.指定对齐值:#progma pack ()时的指定对齐值。
3.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。这样就不能理解上面的几个例子的值了。
例子分析:
分析例子B;
struct B
{
char b;
int a;
short c;
};
假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为 2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节,sizeof(struct B)=12;

同理,分析上面例子C:
#progma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1= 0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放
在0x0006、0x0007中,符合 0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C 只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.

 

- 作者: cjhacker 2007年04月21日, 星期六 15:59  回复(1) |  引用(0) 加入博采

动物识别专家系统的程序(animal.h)
#ifndef ANIMAL_H
#define ANIMAL_H
#define True 1
#define False 0
#define DontKnow -1
#include<stdio.h>
#include<math.h>
#include<string.h>
//事实集(概念集)
char *str[]={"",
                "chew_cud"    /*  1 */,  "hooves"           /* 2 */,   "mammal"        /* 3 */,
                "forward_eyes"/*  4 */,  "claws"            /* 5 */,   "pointed_teeth" /* 6 */,
    "eat_meat"    /*  7 */,  "lay_eggs"         /* 8 */,   "fly"           /* 9 */,
    "feathers"    /* 10 */,  "ungulate"         /* 11 */,  "carnivore"     /* 12 */,
    "bird"        /* 13 */,  "give_milk"        /* 14 */,  "has_hair"      /* 15 */,
    "fly_well"    /* 16 */,  "black&while_color"/* 17 */,  "can_swim"      /* 18 */,
    "long_legs"   /* 19 */,  "long_neck"        /* 20 */,  "black_stripes" /* 21 */,
    "dark_spots"  /* 22 */,  "tawny_color"      /* 23 */,  "albatross"     /* 24 */,
    "penguin"     /* 25 */,  "ostrich"          /* 26 */,  "zebra"         /* 27 */,
    "giraffe"     /* 28 */,   "tiger"       &nbp;   /* 29 */,  "cheetah"       /* 30 */,
    "\0"};
//规则之前件(条件)集,注意与下面对应
int rulep[][6]={{22,23,12,3,0,0},  {21,23,12,3,0,0},  {22,19,20,11,0,0},
    {21,11,0,0,0,0},   {17,19,20,13,-9,0},{17,18,13,-9,0,0},
    {16,13,0,0,0,0},   {15,0,0,0,0,0},    {14,0,0,0,0,0},
    {10,0,0,0,0,0},    {8,7,0,0,0,0},     {7,0,0,0,0,0},
    {4,5,6,0,0,0},     {2,3,0,0,0,0},     {1,3,0,0,0,0}};
//规则之后件(结论)集,注意与上面对应
int rulec[]={      30,                  29,                 28,
                   27,                  26,                 25,
       24,                  3,                  3,
       13,                  13,                 12,
       12,                  11,                 11};  //前7个是要识别的动物
//事实类
class fact{
 private:
  int Number;  //事实ID
  char Name[21];  //事实名
  int Active;     //激活标志
  int Succ;  //事实断言:真、假、不知道三种情况值
 public:
  fact *Next;  //事实链表后继指针
       
  /* <Function 事实类构造函数,由它激活事实对象集 />
     <para> Num:事实ID </para>
     <para> L:事实名   </para>
  */
  fact(int Num,char *L) 
  {
   strcpy(Name,L);
   Number=Num;
   Active=False;  //初始不激活
   Succ=DontKnow;  //初始断言不确定
   Next=NULL;   //初始后继为空
  }
  
  char *GetName()   //获取事实名
  {
   char *L;
   L=new char[21];
   strcpy(L,Name);
   return L;
  }
  int GetNumber(){return Number;}  //获取事实ID
  int GetAct(){return Active; }    //获取事实激活标志
  int GetSucc(){return Succ;}   //获取事实断言值
  
  //设置事实激活标志和断言值
  void PutAct(const int Act0,int Suc0)  
  {
    Active=Act0;
    Succ=Suc0;
  }
};   //end fact
fact *Fact;    //事实链表
class list{    //前提(前提来源与事实集)链表类
 private:
  int Number;    //前提ID,即事实ID
 public:
  list *Next;    //前提链表后继指针
  /*<Function 前提链表类构造函数 />
    <para> Num:事实ID  </para>
  */
  list(int Num)
  {
    Number=Num;
    Next=NULL;
  }
  int GetNumber(){return Number;}   //获取前提ID(也就是事实ID)
};  //end list
class rule{   //规则类
  char *Name;   //规则名
  list *Pre;   //规则前件(前提、条件)链表头指针
  int Conc;        //规则后件(结论)ID(也是事实ID)
 public:
  rule *Next;      //规则链表后继指针
  rule(char *N,int P[],int C);  //规则类构造函数
  ~rule();   //析构函数
  int Query();     //推理机函数
  void GetName(){ printf("%s%",Name);}   //输出规则名

- 作者: cjhacker 2007年04月21日, 星期六 11:07  回复(7) |  引用(0) 加入博采

Protected Mode tutorial
Auf Deutsch

What is protected mode?

The 8088 CPU used in the original IBM PC was not very scalable. In particular, there was no easy way to access more than 1 megabyte of physical memory. To get around this while allowing backward compatability, Intel designed the 80286 CPU with two modes of operation: real mode, in which the '286 acts like a fast 8088, and protected mode (now called 16-bit protected mode). Protected mode allows programs to access more than 1 megabyte of physical memory, and protects against misuse of memory (i.e. programs can't execute a data segment, or write into a code segment). An improved version, 32-bit protected mode, first appeared on the '386 CPU.

How do real mode and protected mode differ?

Table 1: differences between real- and protected modes
Real Mode 16-bit Protected Mode 32-bit Protected Mode
Segment base address 20-bit (1M byte range)
= 16 * segment register
24-bit (16M byte range),
from descriptor
32-bit (4G byte range),
from descriptor
Segment size (limit) 16-bit, 64K bytes (fixed) 16-bit, 1-64K bytes 20-bit, 1-1M bytes or 4K-4G bytes
Segment protection no yes yes
Segment register segment base address / 16 selector selector

I thought protected mode didn't use segmented memory...

The segments are still there, but in 32-bit protected mode, you can set the segment limit to 4G bytes. This is the maximum amount of physical memory addressable by a CPU with a 32-bit address bus. Limit-wise, the segment then 'disappears' (though other protection mechanisms remain in effect). This reason alone makes 32-bit protected mode popular.

What's a descriptor?

In real mode, there is little to know about the segments. Each is 64K bytes in size, and you can do with the segment what you wish: store data in it, put your stack there, or execute code stored in the segment. The base address of the segment is simply 16 times the value in one of the segment registers.

In protected mode, besides the segment base address, we also need the segment size (limit) and some flags indicating what the segment is used for. This information goes into an 8-byte data structure called a descriptor:

Table 2: code/data segment descriptor

Lowest byte Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 Highest byte
Limit 7:0 Limit 15:8 Base 7:0 Base 15:8 Base 23:16 Access Flags, Limit 19:16 Base 31:24

This is a 32-bit ('386) descriptor. For 16-bit ('286) descriptors, the top two bytes (Limit 19:16, Flags, and Base 31:24) are zero. The Access byte indicates segment usage (data segment, stack segment, code segment, etc.):

Table 3: access byte of code/data segment descriptor

Highest bit Bits 6, 5 Bit 4 Bits 3 Bit 2 Bit 1 Lowest bit
Present Privilege 1 Executable Expansion direction/conforming Writable/readable Accessed

  • Present bit. Must be set to one to permit segment access.
  • Privilege. Zero is the highest level of privilege (Ring 0), three is the lowest (Ring 3).
  • Executable bit. If one, this is a code segment, otherwise it's a stack/data segment.
  • Expansion direction (stack/data segment). If one, segment grows downward, and offsets within the segment must be greater than the limit.
  • Conforming (code segment). Privilege-related.
  • Writable (stack/data segment). If one, segment can be written to.
  • Readable (code segment). If one, segment can be read from. (Code segments are not writable.)
  • Accessed. This bit is set whenever the segment is read from or written to.
The 4-bit Flags value is non-zero only for 32-bit segments:

Table 4: flags nybble

Highest bit Bit 6 Bit 5 Bit 4
Granularity Default Size 0 0

The Granularity bit indicates if the segment limit is in units of 4K byte pages (G=1) or if the limit is in units of bytes (G=0).

For stack segments, the Default Size bit is also known as the B (Big) bit, and controls whether 16- or 32-bit values are pushed and popped. For code segments, the D bit indicates whether instructions will operate on 16-bit (D=0) or 32-bit (D=1) quantities by default. To expand upon this: when the D bit is set, the segment is USE32, named after the assembler directive of the same name. The following sequence of hex bytes:

B8 90 90 90 90
will be treated by the CPU as a 32-bit instruction, and will disassemble as
mov eax, 90909090h
In a 16-bit (USE16) code segment, the same sequence of bytes would be equivalent to
mov ax,9090h
nop
nop
Two special opcode bytes called the Operand Size Prefix and the Address Length Prefix reverse the sense of the D bit for the instruction destination and source, respectively. These prefixes affect only the instruction that immediately follows them.

Bit 4 of the Access byte is set to one for code or data/stack segments. If this bit is zero, you have a system segment. These come in several varieties:

  • Task State Segment (TSS). These are used to 'simplify' multitasking. The '386 or higher CPU has four sub-types of TSS.
  • Local Descriptor Table (LDT). Tasks can store their own private descriptors here, instead of in the GDT.
  • Gates. These control CPU transitions from one level of privilege to another. Gate descriptors have a different format than other descriptors:
Table 5: gate descriptor
Lowest byte Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 Highest byte
Offset 7:0 Offset 15:8 Selector 7:0 Selector 15:8 Word Count 4:0 Access Offset 23:16 Offset 31:24

Note the Selector field. Gates work through indirection, and require a separate code or TSS descriptor to function.

Table 6: access byte of system segment descriptor.

Highest bit Bits 6, 5 Bit 4 Bits 3, 2, 1, 0
Present Privilege 0 Type

Table 7: System segment types

Type Segment function Type Segment function
0 (invalid) 8 (invalid)
1 Available '286 TSS 9 Available '386 TSS
2 LDT 10 (undefined, reserved)
3 Busy '286 TSS 11 Busy '386 TSS
4 '286 Call Gate 12 '386 Call Gate
5 Task Gate 13 (undefined, reserved)
6 '286 Interrupt Gate 14 '386 Interrupt Gate
7 '286 Trap Gate 15 '386 Trap Gate

Whew! For now, just remember that TSSes, LDTs, and gates are the three main types of system segment.

Where are the descriptors?

They are stored in a table in memory: the Global Descriptor Table (GDT), Interrupt Descriptor Table (IDT), or one of the Local Descriptor Tables (LDTs). The CPU contains three registers: GDTR, which must point to the GDT, IDTR, which must point to the IDT (if interrupts are used), and LDTR, which must point to the LDT (if an LDT is used). Each of these tables can hold up to 8192 descriptors.

What's a selector?

In protected mode, the segment registers contain selectors, which index into one of the descriptor tables. Only the top 13 bits of the selector are used for this index. The next lower bit choses between the GDT and LDT. The lowest two bits of the selector set a privilege value.

How do I enter protected mode?

Entering protected mode is actually rather simple, and is is described in many other tutorials. You must:
  • Create a valid Global Descriptor Table (GDT), and create the 6-byte 'pseudo descriptor' pointing to the GDT
  • Disable interrupts
  • LGDT. The operand of this instruction points to the GDT pseudo-descriptor, which in turn points to the GDT
  • Set the PE bit in the MSW register
  • Load all data segment registers with valid selectors
  • Do a far jump (load both CS and IP/EIP) to load CS and enter pmode

How do I get back to Real Mode?

  1. Disable interrupts
  2. Do a far jump to a code segmnt with limit 64K (FFFFh)
  3. Load SS with a selector pointing to a descriptor that is 'appropriate for real mode':
            Limit = 64K (FFFFh)     Byte-granular (G=0)
    	Expand-up (E=0)		Writable (W=1)
            Present (P=1)           Base address = any value
    The bit values correspond to an Access byte with the binary value = 1xx1001x
  4. Clear the PE bit in register CR0
  5. Jump to a 16:16 real-mode far address
  6. Load all data segment registers with real-mode values
  7. If you used interrupts in protected mode, use LIDT to load an IDT that is 'appropriate for real mode', with base address = 0 and limit = 3FFh. On 386+ CPUs, use the 32-bit operand size override prefix so all 32 bits of the IDT base are set (otherwise, only the bottom 24 bits will be set).
            ; this code in NASM syntax
            o32 lidt [real_idt]
    	    ...
    	real_idt:
    		dw 1023
    		dd 0
  8. Zero the high 16 bits of 32-bit registers. If the register value is not important, just zero the entire 32-bit register, otherwise use 'movzx':
            xor eax,eax
    	    ...
    	movzx ebp,bp
    	movzx esp,sp
  9. Enable interrupts

What pitfalls have you encountered?

  • You must pay extreme attention to detail here. One wrong bit will make things fail. Protected mode errors often triple-fault the CPU, making it reset itself. Be prepared to see this happen again and again.
  • Most library routines probably won't work. printf(), for example, won't work because it evenutally calls either a DOS or BIOS service to put text on the screen. Unless you have a DOS extender, these services are unavailable in protected mode. I had good luck using sprintf() to put formatted text in a buffer, which I then wrote to the screen with my own protected-mode routine.
  • Before clearing the PE bit, the segment registers must point to descriptors that are appropriate to real mode. This means a limit of exactly 0xFFFF (see other restrictions above). One of my demo programs had ES pointing to a text-video segment. With a limit of 0xFFFF, things worked well. With a limit of 3999 (80 * 25 * 2 - 1), the system froze up after returning to real mode and trying to use the ES register.
    Actually, for DS, ES, FS and GS, the segment limit must be 0xFFFF or greater. If you give the segment a limit of 0xFFFFF and make it page-granular, you can access up to 4G of memory from real mode -- this is unreal mode. However, limits other than 0xFFFF (or page-granularity) for CS or SS cause big problems in real mode.
  • You can not use the '286 LMSW instruction to clear the PE bit. Use MOV CR0, nnn. (On the '286 CPU, the only way to return to real mode is to reset the CPU!)
  • Load all segment registers with valid selectors after entering protected mode. I forgot to do this with ES. A protected-mode routine pushed ES, loaded it with a valid selector, and used it. When it tried to pop the old, invalid (real-mode) selector back into ES, it crashed.
  • The IDTR must also be reset to a value that is appropriate to real-mode before re-enabling interrupts (see above).
  • Not all instructions are legal in real mode. If you attempt to use task state segments for multitasking, note that executing the LTR instruction in real-mode will cause an illegal instruction interrupt.
  • Descriptor tables in ROM? Section 10.4.3 of 386INTEL.TXT states
    The GDT (as well as LDTs) should reside in RAM, because the processor modifies the accessed bit of descriptors.
    However, one of my sources (thanks Vinay) states that later CPUs will not attempt to set the Accessed bit in a descriptor if that bit is already set. Check the docs for the CPU you are using.
  • The naive code described here will crash if the PC is in Virtual 8086 (V86) mode. This is a fourth mode of operation found on the 386 CPU, with addressing similar to real mode but some of the protection mechanisms of protected mode. You may know that a Windows (or OS/2, or Linux) DOS box runs in V86 mode, but you may not realize that memory managers such as EMM386 also put the CPU in V86 mode.
If you want to start simple, try these tips:
  • Don't worry about returning to real mode. Use the reset button :)
  • Leave interrupts disabled.
  • Don't use an LDT.
  • Put only four descriptors in the GDT: null, code, stack/data, and linear data (base address = 0).
  • Set the segment bases to real-mode values i.e. 16 * real-mode segment register value. This lets you address variables in the same way in both real and protected modes.
  • Set all segment limits to their maximum.
  • Leave all privilege values set to 0 (Ring 0, highest privilege).
  • Before each step of switching to pmode, poke a character into video memory, to see (literally!) how far the code gets. Text-mode VGA memory starts at address 0B8000h.

- 作者: cjhacker 2007年04月20日, 星期五 16:20  回复(1) |  引用(0) 加入博采

堆与栈的区别

一、预备知识—程序的内存分配

一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于

数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与

数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静

态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有

系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。

例子程序
这是一个前辈写的,非常详细
//main.cpp
int a = 0; 全局初始化区
char *p1;  全局未初始化区
main()
{
 int b;    栈
 char s[] = "abc"; 栈
 char *p2; 栈
 char *p3 = "123456";   123456\0在常量区,p3在栈上。
 static int c =0;      全局(静态)初始化区
 p1 = (char *)malloc(10);
 p2 = (char *)malloc(20);      分配得来得10和20字节的区域就在堆区。
 strcpy(p1, "123456");         123456\0放在常量区,编译器可能会将它与p3所指向

的"123456"优化成一个地方
}

二、堆和栈的理论知识

2.1申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。

2.2
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该

结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,

这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于

申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

2.3申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址

和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时

就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小


堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址

的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟

内存。由此可见,堆获得的空间比较灵活,也比较大。

2.4申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的

地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活

2.5堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的

地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变

量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函

数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排.

2.6存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据

edx读取字符,显然慢了。

2.7小结:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切

菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

堆和栈的区别主要分:
操作系统方面的堆和栈,如上面说的那些,不多说了。
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先

队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构


虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。

-----------------------------------------------------------------------------------------------------------------
      堆(heap)和栈(stack)是C/C++编程不可避免会碰到的两个基本概念。首先,这两个概念都可以在讲数

据结构的书中找到,他们都是基本的数据结构,虽然栈更为简单一些。

      在具体的C/C++编程框架中,这两个概念并不是并行的。对底层机器代码的研究可以揭示,栈是机器

系统提供的数据结构,而堆则是C/C++函数库提供的。

      具体地说,现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。这体现在,有专门的

寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这种机制的特点是效率高,支持

的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因

为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。机器的

call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的ret指令则隐含从堆

栈中弹出返回地址并跳转之的操作。C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返

回时,该函数的自动变量自动失效的原因(因为堆栈恢复了调用前的状态)。

      和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的

。基本的malloc/realloc/free函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存

空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用

系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当

的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当

的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机

制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:

    1. 系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存

请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。

    2. 系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。

    3. 没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。

    堆和栈的对比

    从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数

库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈是系统数据结构,对于进程

/线程是唯一的;堆是函数库内部数据结构,不一定唯一。不同堆分配的内存逻辑上无法互相操作。栈空

间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由

alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动

态分配操作是不被鼓励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统

,但是精确的申请内存/释放内存匹配是良好程序的基本要素。

- 作者: cjhacker 2007年02月24日, 星期六 11:34  回复(1) |  引用(0) 加入博采

C++0x,崭新的C++,还是另一个JAVA?
    最近,全球最活跃的C++社区boost传来消息,新的C++标准:C++0x,最早将在2007年10月发布。很早就有人猜测,C++0x中的x到底是8还是9,不过现在看来很可能是7,但是也不尽然,因为上一个C++标准本来计划是1997年发布,但是因为STL的引入而推迟到1998年。这次又遇到了相同的情况,大家都不怀疑TR1和TR2(Technical Report)将成为新标准的一部分,但是随着越来越多的boost库被移到TR2中,TR2的进一个完善很可能拖新标准的后腿。不过,C++0x是C++的一次重大升级,恐怕编译器厂商最快也只能在2009年推出符合新标准的编译器。

    新的C++标准将在几个方面对C++标准进行扩充:

1. 增强STL库,标准库TR1和TR2中的新组建,比如“正则表达式”;
2. 并发内存模型和并发库(面向多核处理器);
3. 垃圾收集器;
4. 可简化的泛型程序的开发;

    从上一个标准C++98发布到现在,将近有10年的时间了,在这十年的时间里编程语言发生了翻天覆地的变化,首先是JAVA异军突起,然后是微软借助.NET平台的优势推出了C#,C++已经不再是编程语言中的明星了,它被挤下了神坛,慢慢沦为边缘编程语言。这中间的主要原因并不是C++的语法落伍了,而是使用C++开发软件效率低下并且容易出错。效率低下的原因是因为C++缺少库的支持,C++只提供了编程语言的基本功能,缺少强有力的库支持,比如字符串处理就一直是C++的诟病。容易出错的原因是C++从C继承了动态内存分配和指针,这一点被认为是软件发生错误的根源,虽然C++引入STL来解决这个问题,比如用vector代替内置数组可以防止内存泄漏,用迭代器代替直接下标操作可以防止地址越界。但是STL毕竟太简单了,比如字符串处理就只有一个简单的string,比起Perl差远了。C++唯一的优势就是速度,但是随着处理器和内存的快速发展也慢慢被遗忘了,所以就沦落到了现在的地步。

    C++要想摆脱这种局面,就必须解决这两个问题,而在新标准中我们已经看到了答案。TR1和TR2已经成为新的标准库的一部分,它们不仅提供了象“正则表达式”和“哈希表”这样程序员盼望已久的功能,还有针对多核处理器的并发内存模型和并发库,对网络也有socket库等等。针对内存出错问题,新标准通过强化STL代替内存访问,使用垃圾收集器防止内存泄漏。在C++社区上关于新标准讨论最多的就是到底应不应该修改C++语言,争论的核心之一就是垃圾收集器。老鸟们总是认为“不要为了新手而将C++降格,适合新手的语言已经有很多了!”,很多C++程序员认为新标准应该将精力集中于扩充标准库,而不是解决内存泄漏问题,因为那是程序员自己的事。在本人看来这种思想将会害死C++,因为现实中总是新手比老鸟多,软件公司的老板招一个程序员是希望他(她,它)能够立即开始编写没有错误的代码,而不是培养他们知道他们成为专家后再开始干活,所以,如果C++不能成为一种“菜鸟友好”的语言,那么C++的堕落将不可避免。设想某个程序员分配了一块内存而忘记释放它,但是垃圾收集器捕获了这个异常并默默地替他释放了这块内存,从而使内存泄漏这种悲惨的事情得以避免,而程序运行起来状态良好,那还有什么理由不在C++中添加垃圾收集功能呢?

    C++0x能够重振C++的雄风吗?有了垃圾收集器的C++,并且所有内存和指针操作都被“友好地”建议用STL代替,那还是C++吗?还是另一个JAVA?答案当然是“这仍旧是C++,请放心使用”,不过C++0x之后还会有什么新的内容,会不会有一套GUI库?如果是那样的话就真的成JAVA了。

- 作者: cjhacker 2007年02月17日, 星期六 15:10  回复(1) |  引用(0) 加入博采

免杀加花个人总结

花指令其实是一堆废话,也就是有没有这段花指令都不影响程序的正常运行,注意,添加的花指令要保持堆栈的平衡

我们把一段花指令比喻成一道复杂的数学运算题,无论你在这个过程用什么加减乘除,最终要保持这道数学题的结果是0 ,这样就达到堆栈的平衡的目的.

一般有四方法:替换法, 添加法, 移位法, 去除法

可综合使用,若程序找不到0区域,可先加个区段,再加花

方法1:替换法


注意事项:注意替换和被替换的指令功能一定要相同,否则导致程序出错.


mov eax,00478ac0

jmp eax

 换成:

push 00478ac0

retn

方法2:添加法


注意:添加的指令也要保持堆栈平衡,否则导致程序出错.

添加以下指令:

push eax

pop eax

push eax

pop eax

方法3:移位法


注意:移位互换法灵活性比较强,只要改变顺序,让杀毒软件识别不出来,就达到目的了.


第11句的push eax 和第13句的pop eax 互换一下位置就可达到免杀效果.

方法4:去除法


注意:要去除的指令也要保持堆栈平衡.


把第11句的push eax 和第13句的pop eax 全部nop掉. 就达到免杀效果.

方法5:综合以上四种方法


注意:有时候一种方法修改后,不能达到免杀效果.这时候可以综合运用替换法,添加法,移位法,去除法进行修改.达到免杀效果.

三.解释一些指令含意

 

push ebp ----把基址指针寄存器压入堆栈

pop  ebp ----把基址指针寄存器弹出堆栈

 

push eax ----把数据寄存器压入堆栈

pop  eax ----把数据寄存器弹出堆栈

 

nop      -----不执行

 

add esp,1-----指针寄存器加1

sub esp,1-----指针寄存器减1

 

 

inc ecx  -----计数器加1

dec ecx  -----计数器减1

 

sub esp,1 ----指针寄存器加1

sub esp,-1----指针寄存器加-1

 

jmp 入口地址----跳到程序入口地址

 

push 入口地址---把入口地址压入堆栈

retn       ------ 反回到入口地址,效果与jmp 入口地址一样.

 

mov eax,入口地址 ------把入口地址转送到数据寄存器中. 

jmp eax      ----- 跳到程序入口地址 

 

jb 入口地址

jnb 入口地址  ------效果和jmp 入口地址一样,直接跳到程序入口地址.


四.免杀花指令编写手册:

 

注:以后编写花指令,都可以参考本手册,灵活组合,快速写出自己的花指令.

 

*******************************************************************

 

push ebp

pop ebp

 

push eax

pop eax

 

push esp

pop esp

 

push 0

push 0

 

push 10   -------其中数字可以任意,注意与下面对应

push -10

 

nop   -----------可任意在中间添加

move edi,edi ----效果与nop一样

 

add esp,1  -------其中数字可以任意,注意以下面对应

add esp,-1

 

add esp,1  --------其中数字可以任意,注意以下面对应

sub esp,1

 

inc ecx

dec ecx

 

sub eax, 2 ----------其中数字可任意,与inc的个数对应

inc eax

inc eax

 

add eax -2 ----------其中数字可任意,与inc的个数对应

inc eax

inc eax

 

jmp 下一个jmp地址

jmp 下一个地址

 

 

push ebp

mov ebp,esp -------可做为花指令的开头句

 

 

 

jmp 入口地址  ------跳到程序入口地址

与它效果一样的还有(以下三个):

 

push 入口地址

retn

 

jb  入口地址

jnb 入口地址

 

mov eax,入口地址

jmp eax

- 作者: cjhacker 2007年02月5日, 星期一 17:38  回复(2) |  引用(0) 加入博采

一篇很好的了解机器人入门知识的文章

给刚接触机器人的朋友们 -2003-9-4 2:21:17

你对机器人感兴趣吗?那好,那也是我建立这个网站唯一理由。但是很不幸的是,玩机器人不是一个很容易的爱好,他需要的不仅仅是一些专业知识,比如,机械,电子,计算机,同时还需要你投入极大的热情,精力和Money。如果你对机器人没有很强烈的愿望去了解它,学习它,没有一定的毅力去研究它,那我劝你还是放弃把机器人当作兴趣的爱好吧。当然,如果上面所需的条件你都不缺的话,那我们可以继续下面的内容了。

在开始写这篇文章之前,还有一件事情要声明一下,研究机器人和研究别的东西不一样,你可以把它当作一个玩具来研究,所以研究机器人应该是一件很愉快的工作,千万别把它当作一件苦差事来对待,不然也别玩了。机器人经过几十年的发展,国外网站上已经有n资料可以找了。而国内目前机器人资源目前来说还是一个空白。写这篇文章主要是给国内刚玩机器人的爱好者们一些小小的意见。 机器人到底什么?自从机器人诞生以来就有很多对机器人的定义了,但是正如目前很多学者所指出的一样,很多所谓的机器人看上去像机器人,其实并不是真正的机器人,比如一个无线遥控的有4条会动的腿的狗也能叫机器人吗?当然不能,但是现在很多产品就是这么叫的。但是根据我个人对机器人的理解,一个所谓的机器人必须有2个最主要的功能。一是能和外界交流,第二是能够自动根据交流的信息产生一些反应。这反应可以是事先写好的程序,或者机器人自己产生的程序,一句话就是要有反应。当然,我本来就没什么资格来定义机器人,但是就目前的技术水平和对机器人的理解程度,我觉得最简单的机器人应该有以上2大主要功能。

接下来是玩机器人对基础知识的需求,这里面的知识是任何一个玩机器人的人都必须会的。 简单的说,你必须具备高中物理知识,同时知道电容的作用,电感的作用,二极管的作用以及使用方法,直流电机的工作原理,发光二极管的使用,变压器的使用,如果学过大学模拟电路和数字电路再加上电工原理,那你的电子学理论知识就基本上够了,剩下就是实践了。说实话,机械部分的基础基本上不需要,只要有一点机械常识就可以了,相信玩机器人的朋友们以前玩具一定没少拆,多留心一下里面的机械结构就没什么大问题了。当然如果你要学习如何设计比如2条腿的机器人,或者别的几条腿的,那还需要大学里面一点点的机械原理的内容,不过不用担心,只要不涉及到什么疲劳强度阿,载荷校验的这部分内容,其他部分相信是很容易就明白的。但是如果只是做一个最简单的轮式机器人的话,那这些内容也可以省了。最后是机器人的灵魂了,编程,无论怎么样,要会做一个所谓的机器人,你就必须写程序,这是最最基本的,因为就目前的是市场情况,你可以很容易买到电子和机械套件,但是你绝对没法买到机器人软件,如果能买到,那你如果不会写程序,就没法改进,这个机器人严格意义上就不是你的孩子了,就像父母生了你,但是给你思想,养你的人对你而言更重要。不要怕麻烦,事实上学写简单的程序也不是很难的一件事情,一般来说花个几个星期的晚上看看一些简单的编程,比如C,只要学会一门,再学其它编程语言是很容易的。

所以一定要静下心来,认真学一门,这个时候,支持你的动力就是对机器人的无与伦比的执着了。以上这些知识最好是边学边玩。学了机械就开始设计机器人地盘,学电子的时候就设计机器人电路,学编程就开始写机器人的程序。不然我相信没多少人能支持下来的。当然,你也可以跳开机械部分直接购买成品,这样可以直接使用目前成熟的而且相对比较多的产品。


下面介绍 一下一个机器人“玩具”应该有的物理组成部分:

机器人的大脑:

它可以有很多叫法,可以叫做 微控制器,微处理器,处理器或者计算器等,不过这都不要紧,通常微处理器是指一块芯片,而其它的是一整套控制器,包括微处理器和一些别的元件。任何一个机器人大脑就必须要有这块芯片,不然就称不上机器人了。如果你是初学的,或者刚开始玩机器人的话,那最好买一套控制器玩,千万别之间玩微处理器哦,除非你电子系毕业的,而且专业是芯片设计,那你可以直接开始玩微处理器(呵呵,开个玩笑,只不过微处理器对初学者而言比较困难,还是由简入繁吧)。在选择微控制器的时候,主要要考虑:处理器的速度,Rom和Ram的大小,I/O端口类型和数量,编程语言以及功耗。

在选购微控制器时候,最先考虑的一个是速度,主要的一个指标就是运行速度MHz,这个计算机的评价指标是一样的,越高的MHz意味着越快的执行速度和你付出越多的钱。这第二个存储容量的指标是决定你的控制器可以实现的功能的多少和复杂程度,越多的存储空间可以让你存储更多的程序代码,从而实现更多,更复杂的功能。I/O接口的种类和接口数量(pin numbers)是指接口数量,目前而言有2种接口种类,Analog(模拟)和Digital(数字)接口,这个接口种类决定你控制器可以控制硬件的类型,如果你的电动机控制卡是数字接口的,那你的控制器也必须要有数字输出接口,如果你的传感器是模拟接口的话,那你的控制器也必须要有模拟输入接口。接口数量就是指同时能够连接设备的多少了。

编程语言是一个控制器能够接受的语言类型,一般有C语言,汇编语言或者basic语言,这些通常能被高级一点的控制器直接执行,因为在高级控制器里面内置了编译器能够直接把一些高级语言翻译成机器码,但是现在差不多每种控制器都支持C语言,如果你会C语言的话,那个编程的问题就很小了,如果你不会,而又不想学,那你就要花点时间找一个相应的编译器能够将你写的高级语言编译成你所选择控制器的执行代码。所以,建议想玩机器人的人们都学一点C,毕竟这个是目前被绝大多数编译器或者控制器所接受的语言哦。

最后一个要考虑的是,如何考虑功耗的问题,由于机器人不太好拖一根长长的电源线在屁股上,所以,除了考虑执行电机的功耗以外,另外一个耗电大户就是控制器了。在考虑这个参数的时候,主要考虑你的电源供应是什么,机器人预计的运行时间是多少。不过这个问题目前对我们而言不是很大,除非你做那种很小的机器人,不然现在控制器的功耗都不是很大,所以,稍微大一点的机器人都会有足够的电源供应的。


传感器,是机器人和现实世界之间的纽带。但就目前传感器技术而言,我们现在所能选用的传感器或者说负担起的传感器可不多。根据传感器的的工作特征,可以分为,光学传感器,顾名思义就是对光产生反应的的传感器,比如红外传感器,声传感器,比如microphone,力传感器,比如压敏传感器,位置传感器,比如陀螺仪。

光传感器:

光传感器的范围很广,有最简单的光敏电阻,就是光强度的大小改变电阻的阻值从而实现对光强度的感知,也有目前最复杂的摄像头Camera。然而在我们机器人中,用的最多的是 Infra-red 红外传感器,其中的红外接近传感器是机器人在运动过程中必不可少的传感器,通过它,目前我们可以获得机器人的在移动过程中与前面障碍物之间的距离,当然这距离非常短。不过,对于我们的机器人来说,这也是目前唯一的可承受的选择。

还有一种目前比较有用的红外传感器是叫红外探测器,它不同于上面的红外接近传感器,它没有红外发射器,只有一个红外接受单元,由被测物体发出红外信号。一般用来做热感应用,比如人走近或者动物走进这样的传感器时,它就会产生信号。在生产安全领域用的比较多。对于我们吗,最多就是有人走进的时候能够作为一种感知手段来让机器人获得:“哇,有个发红外线的家伙冲我走过来了”这样的信息。

红外接近传感器 (Infrared Sensor)

还有一种目前我们也能承受的是摄像头,随着电脑技术的飞速发展,今天获得一个廉价的摄像头已经是非常easy的事情了,一个普通摄像头目前在市场上售价也就RMB200多点。但是对我们而言,与之相配套的,能应用于机器人的识别软件和硬件还相对比较少和昂贵,目前成熟的市场产品有美国卡耐基-梅隆大学开发的CMU-Camera套装,但是要100美金,差不多850RMB,相对而言价格还是高了一点。但是如果我们不用他们的套装,自己开发的话,其它硬件投入相对少了,但是你要对计算机编程相当精通,而且难度相对很大。一个比较简单的图像识别的软件少说也得一个计算机专业毕业的本科生4-5个月的工作时间才能写出来。更别说其它功能多一点的软件。不过随着现在对图像研究的在全世界范围如火如荼的展开,相信未来2年以后就会有很多产品可供选择了。

力传感器:

力传感器是用来检测碰撞或者接触信号的,比如机械手的应用,当你放一个东西到机械手的时候,机械手自动抓住它,它就需要力传感器检测东西抓的紧不紧。典型的力传感器是微动开关和压敏传感器。微动开关其实就是一个小开关,通过调节开关上的杠杆长短,能够调节触动开关的力的大小。用来做碰撞检测这是最好不过了。但是这种传感器必须事先确定好力的阀值,也就是说只能实现硬件控制。而压敏传感器是能根据受力大小,自动调节输出电压或者电流,从而可以实现软件控制。

声觉传感器:

哈哈,这个部分我想都不用介绍了,目前最多的也就是麦克风了。几块钱一个,满世界都是。但是目前对我们而言唯一的问题是如何处理声音信号。当前没有一个比较好的声音解决方案,国外目前有几个产品,但是都没法识别中文,都是英文的。 不知道超声波传感器属不属于这个范畴,超声波传感器和红外接近传感器很像,也属于距离探测传感器,但是它能提供比红外传感器更远的探测范围,而且还能提供一个范围的探测而不是一条线的探测。也是目前我们用的最多的距离传感器之一了。

位置和姿态传感器:

机器人在移动或者动作的时候必须时时刻刻知道自己的姿态动作,否则就会产生控制中的一个开环问题,没有反馈。 位置传感器和姿态传感器就是这个用的。常用的有光电编码器,由于机器人的执行机构一般是电机驱动,通过计算电机转的圈数,可以得出电机带动部件的大致位置,编码器就是这样一种传感器,它一般和电机轴或者转动部件直接连接,电机或者转动部件转了多少圈或者角度能够通过编码器读出,控制软件再根据读出数据进行位置估计。还有一种是陀螺仪,这是利用陀螺原理制作的传感器,主要可以测得移动机器人的移动加速度,转过的角度等信息。相关原理要参考大学物理的相关内容了。难度稍微大一点。而且目前价格也比较贵。罗盘对我们而言是陀螺仪的替代品,它利用了罗盘的原理,来测得机器人的角度,价格也比陀螺仪要便宜得多。大概150RMB左右一个。.最后一个要介绍的是GPS定位仪了,对于室外的移动机器人来说,这个是必须的定位手段了。它通过卫星来定位你的机器人,听上去很高科技,所以价格也很高咯。一个GPS定位模块目前售价大约是1500RMB左右。 传感器小结: 当然,以上所说的只是传感器中的一部分,还有很多传感器没谈,比如现在比较流行的激光范围探测器,这个传感器能提供50m的探测范围,而红外接近传感器只能提供50cm,超声波传感器能提供10m以内。随着探测距离的提高,价格也是以几何级数的提高,这么一个激光传感器通常需要2W RMB,绝对不是我们做能负担得起的,而且目前还没有小型化,一个最轻的也要2kg左右。这些传感器由于这样或者那样的原因,是目前我们所不能选用的。


驱动器

驱动器就是驱动机器人的动的部件。最常用的是电机了。当然还有液压,气动等别的驱动方式,但是由于控制负责,价格高等原因,并不太适合我们用。一个机器人最主要的控制量就是控制机器人的移动,无论是自身的移动还是手臂等关节的移动,所以机器人驱动器中最根本和本质的问题就是控制电机,控制电机转的圈数,就可以控制机器人移动的距离和方向,机械手臂的弯曲的程度或者移动的距离等。所以,第一个要解决的问题就是如何让电机能根据自己的意图转动。一般来说,有专门的控制卡和控制芯片来进行控制的。有了这些控制卡和芯片,我们所要做的就是把微控制器和这些连接起来,然后就可以用程序来控制电机了。第二个问题是控制电机的速度,在机器人上的实际表现就是机器人或者手臂的实际运动速度了,机器人走的快慢全靠电机的转速,这样,我们就要求控制卡对电机有速度控制。电机目前常用的有2种,步进电机(stepper motor)和直流电机(DC motor).

直流电机

这是最最普通的电机了,我们小时候的电动玩具全部用的直流电机,直流电机最大的问题是你没法精确控制电机转的圈数,也就前面所说的位置控制。你必须加上一个编码盘,来进行反馈,来获得实际转的圈数。这时的直流电机就变成了伺服电机(Servo Motor)。但是直流电机的速度控制相对就比较简单,用一种叫PWM的调速方法可以很轻松的调节电机速度。现在也有很多控制芯片带调速功能的。选购时要考虑的参数是电机的输出力矩,电机的功率,电机的最高转速。

RC Servo Motor

步进电机

步进电机看名字就知道了,它是一步一步前进的。也就是说,它可以一个角度一个角度旋转,不象直流电机,你可以很轻松的调节步进电机的位置,如果你发一个转10圈的指令,步进电机就不会转11圈,但是如果是直流电机,由于惯性作用,它可能转11圈半。至于为什么会有这样的差别,建议还是去看看相关电机的原理。步进电机的调速是通过控制电机的频率来获得的。一般控制信号频率越高,电机转的越快,频率越低,转的越慢。选购时要考虑的参数是电机的输出力矩,电机的功率,每个脉冲电机的最小转角。一般情况下,电机都没法直接带动轮子或者手臂,因为力矩不够大,所以我们需要加上一个减速箱来增加电机的输出力矩,但是代价是电机速度的减小,比如一个1:250的齿轮箱,会让你电机的输出力矩增大250倍,但是速度只有原来的1/250了。首先计算出机器人所需要的力矩大小,然后根据力矩去选择电机。

Stepper Motor

现在应该对机器人3大硬件部分有个大致的了解了吧。

Ricky July 15 2003 at Nottingham,UK

- 作者: cjhacker 2007年01月29日, 星期一 22:25  回复(2) |  引用(0) 加入博采

数学学科分类标准

    一份中国学科分类国家标准,看看,就一个数学中的一个分支一个人一辈子都研究不完。其中也说明了,应用数学归为每个具体应用学科里面。除了专门数学专业的,其他专业的也只是学了其中在本学科需要的一小部分而已。


110 数学
  a.. 110.11 数学史
  b.. 110.14 数理逻辑与数学基础
    a.. 110.1410 演绎逻辑学 亦称符号逻辑学
    b.. 110.1420 证明论 亦称元数学
    c.. 110.1430 递归论
    d.. 110.1440 模型论
    e.. 110.1450 公理集合论
    f.. 110.1460 数学基础
    g.. 110.1499 数理逻辑与数学基础其他学科
  c.. 110.17 数论
    a.. 110.1710 初等数论
    b.. 110.1720 解析数论
    c.. 110.1730 代数数论
    d.. 110.1740 超越数论
    e.. 110.1750 丢番图逼近
    f.. 110.1760 数的几何
    g.. 110.1770 概率数论
    h.. 110.1780 计算数论
    i.. 110.1799 数论其他学科
  d.. 110.21 代数学
    a.. 110.2110 线性代数
    b.. 110.2115 群论
    c.. 110.2120 域论
    d.. 110.2125 李群
    e.. 110.2130 李代数
    f.. 110.2135 Kac-Moody代数
    g.. 110.2140 环论 包括交换环与交换代数,结合环与结合代数,非结合环与非结
合代数等
    h.. 110.2145 模论
    i.. 110.2150 格论
    j.. 110.2155 泛代数理论
    k.. 110.2160 范畴论
    l.. 110.2165 同调代数
    m.. 110.2170 代数K理论
    n.. 110.2175 微分代数
    o.. 110.2180 代数编码理论
    p.. 110.2199 代数学其他学科
  e.. 110.24 代数几何学
  f.. 110.27 几何学
    a.. 110.2710 几何学基础
    b.. 110.2715 欧氏几何学
    c.. 110.2720 非欧几何学 包括黎曼几何学等
    d.. 110.2725 球面几何学
    e.. 110.2730 向量和张量分析
    f.. 110.2735 仿射几何学
    g.. 110.2740 射影几何学
    h.. 110.2745 微分几何学
    i.. 110.2750 分数维几何
    j.. 110.2755 计算几何学
    k.. 110.2799 几何学其他学科
  g.. 110.31 拓扑学
    a.. 110.3110 点集拓扑学
    b.. 110.3115 代数拓扑学
    c.. 110.3120 同伦论
    d.. 110.3125 低维拓扑学
    e.. 110.3130 同调论
    f.. 110.3135 维数论
    g.. 110.3140 格上拓扑学
    h.. 110.3145 纤维丛论
    i.. 110.3150 几何拓扑学
    j.. 110.3155 奇点理论
    k.. 110.3160 微分拓扑学
    l.. 110.3199 拓扑学其他学科
  h.. 110.34 数学分析
    a.. 110.3410 微分学
    b.. 110.3420 积分学
    c.. 110.3430 级数论
    d.. 110.3499 数学分析其他学科
  i.. 110.37 非标准分析
  j.. 110.41 函数论
    a.. 110.4110 实变函数论
    b.. 110.4120 单复变函数论
    c.. 110.4130 多复变函数论
    d.. 110.4140 函数逼近论
    e.. 110.4150 调和分析
    f.. 110.4160 复流形
    g.. 110.4170 特殊函数论
    h.. 110.4199 函数论其他学科
  k.. 110.44 常微分方程
    a.. 110.4410 定性理论
    b.. 110.4420 稳定性理论
    c.. 110.4430 解析理论
    d.. 110.4499 常微分方程其他学科
  l.. 110.47 偏微分方程
    a.. 110.4710 椭圆型偏微分方程
    b.. 110.4720 双曲型偏微分方程
    c.. 110.4730 抛物型偏微分方程
    d.. 110.4740 非线性偏微分方程
    e.. 110.4799 偏微分方程其他学科
  m.. 110.51 动力系统
    a.. 110.5110 微分动力系统
    b.. 110.5120 拓扑动力系统
    c.. 110.5130 复动力系统
    d.. 110.5199 动力系统其他学科
  n.. 110.54 积分方程
  o.. 110.57 泛函分析
    a.. 110.5710 线性算子理论
    b.. 110.5715 变分法
    c.. 110.5720 拓扑线性空间
    d.. 110.5725 希尔伯特空间
    e.. 110.5730 函数空间
    f.. 110.5735 巴拿赫空间
    g.. 110.5740 算子代数
    h.. 110.5745 测度与积分
    i.. 110.5750 广义函数论
    j.. 110.5755 非线性泛函分析
    k.. 110.5799 泛函分析其他学科
  p.. 110.61 计算数学
    a.. 110.6110 插值法与逼近论
    b.. 110.6120 常微分方程数值解
    c.. 110.6130 偏微分方程数值解
    d.. 110.6140 积分方程数值解
    e.. 110.6150 数值代数
    f.. 110.6160 连续问题离散化方法
    g.. 110.6170 随机数值实验
    h..110.6180 误差分析
    i.. 110.6199 计算数学其他学科
  q.. 110.64 概率论
    a.. 110.6410 几何概率
    b.. 110.6420 概率分布
    c.. 110.6430 极限理论
    d.. 110.6440 随机过程 包括正态过程与平稳过程、点过程等
    e.. 110.6450 马尔可夫过程
    f.. 110.6460 随机分析
    g.. 110.6470 鞅论
    h.. 110.6480 应用概率论 具体应用入有关学科
    i.. 110.6499 概率论其他学科
  r.. 110.67 数理统计学
    a.. 110.6710 抽样理论 包括抽样分布、抽样调查等
    b.. 110.6715 假设检验
    c.. 110.6720 非参数统计
    d.. 110.6725 方差分析
    e.. 110.6730 相关回归分析
    f.. 110.6735 统计推断
    g.. 110.6740 贝叶斯统计 包括参数估计等
    h.. 110.6745 试验设计
    i.. 110.6750 多元分析
    j.. 110.6755 统计判决理论
    k.. 110.6760 时间序列分析
    l.. 110.6799 数理统计学其他学科
  s.. 110.71 应用统计数学
    a.. 110.7110 统计质量控制
    b.. 110.7120 可靠性数学
    c.. 110.7130 保险数学
    d.. 110.7140 统计模拟
  t.. 110.7199 应用统计数学其他学科
  u.. 110.74 运筹学
    a.. 110.7410 线性规划
    b.. 110.7415 非线性规划
    c.. 110.7420 动态规划
    d.. 110.7425 组合最优化
    e.. 110.7430 参数规划
    f.. 110.7435 整数规划
    g.. 110.7440 随机规划
    h.. 110.7445 排队论
    i.. 110.7450 对策论 亦称博奕论
    j.. 110.7455 库存论
    k.. 110.7460 决策论
    l.. 110.7465 搜索论
    m.. 110.7470 图论
    n.. 110.7475 统筹论
    o.. 110.7480 最优化
    p.. 110.7499 运筹学其他学科
  v.. 110.77 组合数学
  w.. 110.81 离散数学
  x.. 110.84 模糊数学
  y.. 110.87 应用数学 具体应用入有关学科
  z.. 110.99 数学其他学科

 

- 作者: cjhacker 2007年01月20日, 星期六 13:17  回复(1) |  引用(0) 加入博采

楼宇的迭代式开发

谈到架构,人们最常想到的就是建筑。这得感谢亚历山大大叔的功劳。另外,设计模式的成功引入到软件工程,也是一个促进。这个成功案例让很多人都去拜读《建筑之永恒之道》。

最近在讨论中,突然发现楼房在构建的时候,大部分都是采用迭代式开发的。感觉甚是有意义,特拿出来和大家分享。

我们将楼房中的每一个房屋比喻成软件中的功能。那么在开发过程中,如何计划这些功能的开发顺序,以及每一个功能的开发步骤,就是一个非常大的学问。相信每一个经历过项目的人都知道。安排不好,就会导致无谓的相互等待。

我们以前默认的工作方式是,将所有的功能模块,全部分出去。先做区域性划分。比如,什么类型的模块,由什么小组来负责。然后在此基础上,每个小组再安排自己的模块。因为人手一定是比模块数少的多的,所以必然有一部分认为不重要的模块被安排在后面。

请注意上面的模块安排方式。整个功能的安排,是根据开发小组的特性来安排的,加上层级的划分,对于软件工程整体计划的把握性越来越低。特别是项目在每一个时刻的整体状况不好判断。你说写完了一半的模块的软件是什么状况?不好说吧。我们一般可能会采用另外一种容易接受的方式来描述:

“嗯,现在的软件可以完成基本功能了。下周就可以交付客户验证业务流程了。”

这种描述,由于是从最终结果来描述的,所以往往能被领导和客户所接受。如果你细心点的话,你就会发现这里面有一个问题,如何衡量进度?软件工程目前并不成熟,所以才导致进度衡量非常麻烦。其中最麻烦的就是软件模块的整合。每一个人对于整合都是持怀疑态度的。观察一下其他成熟行业,特别是建筑行业,高度的规格化建设,保障了其在整合的时候的低风险。有一句话常常说,工业化就是规范化。一个软件公司或一个软件项目,如果做不到这些规范化,那么就会一直担心整合。对于进度的把握就会存在问题。

废话少说,我们看看楼宇的迭代过程。先来做基础,越高的房子,地基越要打号。再做框架部分,一般是浇筑钢筋混凝土;然后填补减力墙;水、电管道;装饰装修。显然,每一次里程碑的划分,并没有按照模块的多少,而是按照大楼的整体结构。

我们软件做不到类似建筑这样成熟的划分。但是我认为可以从需求角度,进行几个里程碑式的划分。

第一、完成系统基础技术的工作。类似于建筑的地基。

第二、完成系统中功能组合关系的框架。所有的功能都可以在这个框架下进行增加、调试。这里面要完成系统在架构过程中要求的模型及各种应对业务变化的模式。

第三、完成系统的基本功能。什么是基本功能?软件的目的是解决问题,如果能用使用我们的软件,虽然比较繁琐,但是已经可以解决问题了。那就是完成了基本功能。比如一个邮件系统,已经可以发送收取邮件了,那么基本功能就完成了。

第四、完成基础易用性需求。什么较基础易用性?我们软件在业务分析的时候,必然考虑了用户的一些基本需求。这时候已经是一个完整的软件了。但是没有什么亮点。

第五、完整实现系统的规划。将最后展现给用户的功能完全实现。其实用户只看到这些,其他的他们都不是很在乎。当然了,虽然他们会一直使用。就比如你住的房子,你最关注的是房子里的装修。但是如果没有水、电,房子有危险,你肯定会非常不满意。

我们一直在说迭代,但是基于迭代的方法及步骤却没有细化下来。建议我们来多做做这方面的知识积累。大家也多分享一些迭代的过程,共同成长。上面的里程碑划分,是参考建筑来做的,我在实践中并没有完全按照上面的方式,一般遇到第四个里程碑的时候,就会出现混乱。大家想法不容易在一个层次上集中。不过总算是一个想法吧。

- 作者: cjhacker 2007年01月12日, 星期五 17:21  回复(1) |  引用(0) 加入博采

Google女性工程师揭密:如何准备Google软件工程师面试

如何准备软件工程师的面试



(作者简介: 王忻,Google 工程师。北京出生,五岁时跟随父母移居美国。中学期间跳了三级,十五岁进入了加州理工大学,加入 Google 前曾在微软等公司工作。)

六月份的时候,我曾经在黑板报上介绍过“如何写一份好的工程师简历”, 今天想跟大家来谈谈如何准备软件工程师的面试?假设,现在您的杀手简历 (killer resume)已经吸引了某大公司的注意并约你面试。那么接下来该如何准备呢?

我 在 Google(以前是微软)工作期间面试了不下 300人,其中某些应聘者确实表现非凡,但有些却显得准备不足。当然许多面试准备不足的人最后依然获得了录用通知,因为他们本身确实才华出众。但如果应聘 者能提前准备妥当,那么面试过程将更为保险和轻松。以下所列出的就是我根据多年经验总结得出的建议:

1.使用相同的工具(如铅笔和纸张)和时间限制(例如半个小时)模拟面试训练


Google 和微软都会让应聘者在白板上手工解答编程问题,但通常大部分的应聘者都是习惯于在电脑上利用编程工具系统编写程序。因此面试的时候,某些应聘者离开了熟悉 的电脑光标,站在白板前感觉手足无措不知该如何起行。又或者他们不习惯在编程之时旁边有人观看,这会让他们感到紧张而无法正常思考。

在现实生活中,如果你想要横渡英吉利海峡,自然不能总是在室内游泳池练习。你必须投身于大海在波涛之中训练,在准备面试的时候也是如此。:)

在 面试开始之前你最好向招聘单位询问面试形式和面试问题。如果招聘单位让你在某个房间考试且仅提供没有汇编程序的编辑器,那么就应该在家中按照这种情景进行 练习。如果招聘公司单位让你在白板上回答问题并会安排考官在旁监督,那么你就要找一位软件工程师来扮演考官配合你练习。即使找来的考官经验不如你也没有关 系,他们依然能帮助你消除在他人面前出错所带来的紧张感,这样可以让你适应有人在旁边盯着看的面试氛围。

如果你恰巧认识我并希望由我来帮你联系,那我的条件就是必须请我吃饭:如果你已经工作了就吃日本寿司大餐;如果你还是学生,那么吃比萨饼也可以。:)

2.在面试过程中不要对细小错误耿耿于怀


我 曾不止一次的在面试过程中碰到这种情况:当应聘者知道编程问题后,他马上就想到了最佳的方案、确定了边界条件,然后开始编写程序。但在编写过程中,应聘者 犯了诸如首先检查是不是操作顺序错误或忘记设定某变量等无关大局的小错误,当我指出其错误之后,应聘者立刻变得十分紧张,这种焦虑情绪影响了他在后面环节 的正常发挥。

其实这种恐惧心理完全不必要。一名优秀的程序员在编程过程中出现错误也是很正常的,就像是小提琴手在演奏高难度的巴赫交响乐时也会偶尔失误。音乐会的听众可能会觉察到这些错误,但是听众绝对不会因为这种细小失误就把出色的小提琴手看作是门外汉。

即便应聘者彻底搞砸了某个编程问题,面试考官也可能会提出不同的问题并会容忍应聘者在某个问题上的失误。再退一步说,就算某次面试彻底失败,你也有机会在其它面试上补救。

我 的一位同事(一个项目的技术负责人)最近面试了一个人,在开始面试时他觉得面试者的交流方式存在问题,因此开始表现的相当不友好。但经过了整个面试过程 后,面试者证明了自身的能力,而我的那位同事也成了那位面试者最坚定的支持者。在过去的一年中,我从未见过这位同事如此强烈的支持哪位面试者。

所以,因此就算面试进展不顺,也务必坚持到底不要放弃。

3.在面试过程中不要失礼

这似乎是不用说的问题,但在面试过程中我确实碰到过影响很不好的失礼行为。曾有一位前来应聘软件工程师的人看到我就说:“哇,我真不敢相信你这么年轻!你看上去好小!!我觉得你才 18 岁!”

面试者的这种言行实在要不得。

面试者也要注意不要说出诸如此类的话:“哇,你真的就是考官吗?你看上去好老!”“哇,你真的是来面试我的,你看上去好胖!”(相信应该不会有人说这样的话)。

在 我的另外一次面试中,应聘者的手机在面试开始 15 分钟之后就响了,她没有理会,手机连续响了 20 秒,这样不免会对面试造成影响。5 分钟之后,她的手机又响了,她依然没有理会;5分钟之后,手机第三次响起。最后她终于抓过手提包在里面翻出了手机。我想:“是时候关掉手机了,她在进来之 前就应该把手机关掉。”但是她在手提包中拿出手机之后却旁若无人的打起电话来,而且就在面试过程中间!

这种情况唯一可接受的理由就是他有什么非常紧急的事,但是即便情况如此,那么他也应该在面试开始之时就讲清楚,让面试官有所准备。

4.不要在面试中喧宾夺主

我曾经面试过几个应聘者,他们好像铁了心肠一定要告诉我他们最近的“超级项目”。当我开始发话他们就立刻打断:“我想让你了解我们近期处理的超级项目,10年之前当这个项目开始之时还默默无闻……”,然后接下来的5分钟时间都在那里滔滔不绝唾沫横飞。

有时应聘者好像打定主意要给每个考官详细描述其引以为豪的项目,然后一整天都在那里翻来覆去的说这个项目。

记住:面试官在面试过程中有具体的问题需要询问。但是如果应聘者喧宾夺主,那么考官就可能无法获得充分的信息来做出判断,同时这种行为也会让考官觉得应聘者很难共事。

如果你确实想谈论自己的项目,那么就应询问面试官:“我觉得最近的某某项目能充分体现我的能力,我能不能用 10分钟的时间来描述一下具体情况?”这样就会给面试官空间来调整面试过程,由此也避免毫无征兆就让面试离题万里。

5.在回答需要具体答案的问题之时,记得首先要有总括性的发言

有 时我会问一个答案可以很简练的问题,例如:“在你的那个成功项目中总共有多少人参与?”但应聘者往往会就此打开话匣:“恩,张三参与了这个项目,他负责 UI部分,当然我也会给他一些指导。李四也在项目中,她在宾州远程工作,负责后端服务器。两年之后我们又有新人王五加入……”

在应聘者滔滔不绝的讲了三分钟之后,我还是不知道这个项目到底有多少人参与。

因此首先要简练的回答问题,然后再展开描述:“在我接手项目时有三个人,但当我离开项目时人数已经增加到12人。”

当然如果能简练的回答问题,然后征询意见之后再展开论述那就更好了:“在我接手项目时有三个人,但当我离开项目时人数已经增加到 12 人。我可以讲一下各人在项目中的具体分工吗?”

6.(不是特别重要)在面试中要衣着得体,舒适的商务便装是最佳的选择

人们有时候会为衣着犯愁。但是最重要的是要让自己感觉舒适。如果需要具体的建议,那么我建议穿衬衫甚至T恤衫。对于某些公司(例如 Google),西装革履显然是太隆重了。

这 条建议不必太看中,因为面试官不会管应聘者穿什么。最好应该询问人事招聘部门穿什么合适,因为不同国家有不同习俗,就算美国东海岸和西海岸的公司着装文化 也会有差别。像 Google 这样的公司在着装方面更加随意,因此如果你穿着“三件套”的经典西服去 Google 面试,考官可能会有异样的感觉。因此如果你真的具备软件工程的本领,穿什么其实并不重要。某个应聘者曾经穿着皱巴巴脏兮兮的T恤就跑来面试,他的T恤衫上 还有着许多破洞。但最后他还是拿到了录取通知(当然我绝不建议如此穿着)。

最后的一个小故事


最后我想讲一场极为尴尬的面试。在看完之后,我希望你能这样想:无论你的面试如何糟糕,你至少要比这位应聘者幸运。

以前我还在微软的时候,我们通常会为应聘者准备一些饮料,某位暂称其为 Jeff 的应聘者要了一听百事可乐。我们走进面试房间后,他就在桌前坐下了。接下来我们简要的谈了谈他的工作经历,然后他开始在白板上解答编程问题,此时他还没有打开他的可乐。

我们俩站在白板前,然后杰夫开始在上面写程序。在写程序之时他沉浸在对整体构架的思考中,下意识的退了一步来查看整个白板。在后退时他不小心碰到了桌子,放在桌上的百事可乐掉到了地上。

因为可乐还没有打开,因此当可乐罐落地的时候,可乐罐炸开了。

可乐罐在地上打转,泡沫喷的到处都是。你可以想象当时的场景,可乐喷到了墙上、书架还有我电脑的键盘上。我俩楞在那里,手都半伸着(根本来不及抓到可乐罐),眼睁睁的看着可乐弄得到处都是。

我们花了 5 分钟的时间用纸巾来清理现场(虽然我的书本自那天之后都粘页了,而墙壁也不再是干净的了)。
随后我们重新开始白板测试。杰夫此时已非常紧张(换了谁都会紧张吧?)。他写了几行程序,然后擦掉,然后再写。他是用自己的手擦拭白板而不是用板刷。他急得额头冒汗,然后他又用刚刚擦过白板的手擦汗。在面试过程结束之时,他的脸上布满了红色、绿色和蓝色的颜料。

我说:“你的手上粘了很多颜料,我带你去卫生间洗洗吧,”然后我把他领到洗手间让他从镜中看到了自己的尊容。

- 作者: cjhacker 2006年12月15日, 星期五 21:36  回复(2) |  引用(0) 加入博采