2005年十一月月 发布的文章

打开汇编之门

工作这么长时间,一直在C语言这一层面上钻研和打拼,日积月累,很多关于C的疑惑在书本和资料中都难以找到答案。程序员是追求完美的一个种群,其头脑中哪怕是存在一点点的思维黑洞都会让其坐卧不宁。不久前在itput论坛上偶得《Computer Systems A Programmer's Perspective》(以下称CSAPP)这本经典好书,遂连夜拜读以求解惑。虽说书中没有能正面的回答我的一些疑惑,但是它却为我指明了一条通向“无惑”之路 — 这就是打开汇编之门。

汇编语言是一门非常接近机器语言的语言,其语句与机器指令之间的对应关系更加简单和清晰。打开汇编之门不仅仅能解除高级语言给你带来的疑惑,它更能让你更加的理解现代计算机的运行体系,还有一点更加重要的是它给你带来的是一种自信的感觉,减少了你在高处摇摇欲坠的恐惧,响应了侯捷老师的“勿在浮沙筑高台”的号召。现在学习汇编的目的已与以前大大不同了。正如CS.APP中所说那样“程序员学习汇编的需求随着时间的推移也发生了变化,开始时是要求程序员能直接用汇编编写程序,现在则是要求能够阅读和理解优化编译器产生的代码”。能阅读和理解,这也恰恰是我的需求和目标。

在大学时接触过汇编,主要是Microsoft MASM宏汇编,不过那时的认识高度不够加上态度不端正,错失了一个很好的学习机会。现在绝大部分时间是使用GCC在Unix系列平台上工作,选择汇编语言当然是GNU汇编了,恰好CS.APP中使用的也是GNU的汇编语法。由于学习汇编的主要目的还是“解惑”,所以形式上多是以C代码和汇编代码的比较。

1、汇编让你看到更多
随着你使用的语言的层次的提高,你眼中的计算机将会越来越模糊,你的关注点也越来越远离语言本身而靠近另一端“问题域”,比如通过JAVA,你更多看到的是其虚拟机,而看不到真实的计算机;通过C,你看到的也仅仅是内存一层;到了汇编语言,你就可以深入到寄存器一层自由发挥了。汇编程序员眼里的“独特风景”包括:
a) “程序计数器(%eip)” — 一个特殊寄存器,其中永远存储下一条将要执行的指令的地址;
b) 整数寄存器 — 共8个,分别是%eax、%ebx、%ecx、%edx、%esi、%ebi、%esp和%ebp,它们可以存整数数据,可以存地址,也可以记录程序状态等。早期每个寄存器都有其特殊的用途,现在由于像linux这样的平台多采用“平面寻址[1]”,寄存器的特殊性已经不那么明显了。
c) 条件标志寄存器 — 保存最近执行的算术指令的状态信息,用来实现控制流中的条件变化。
d) 浮点数寄存器 — 顾名思义,用来存放浮点数。
虽说寄存器的特殊性程度已经弱化,但是实际上每个编译器在使用这些寄存器时还是遵循一定的规则的,以后再说。

2、初窥汇编
下面是一个简单的C函数:
void dummy() {
 int a = 1234;
 int b = a;
}
我们使用gcc加-S选项将之转换成汇编代码如下(省略部分内容):
 movl $1234, -4(%ebp)
 movl -4(%ebp), %eax
 movl %eax, -8(%ebp)
看了一眼又一眼,还是看不懂,只是发现些熟悉的内容,因为上面提过如%ebp、%eax等。这只是个引子,让我们感性的认识一下汇编的“容貌”。我们一点点地来看。咋看一眼汇编代码长得似乎很相似,没错,汇编代码就是一条一条的“指令+操作数”的语句的集合。汇编指令是固定的,每条指令都有其固定的用途,而操作数表示则有多种类型。

1) 操作数表示
大部分汇编指令都有一个或多个操作数,包括指令操作中的源和目的。一条标准的指令格式大致是这样的:“指令 + 源操作数 + 目的操作数”,其中源操作数可以是立即数、从寄存器中读出的数或从内存中读出的数;而目的操作数则可以是寄存器或内存。按这么一分类,操作数就大致有三种:
a) 立即数表示法 — 如“movl $1234, -4(%ebp)”中的“$1234”,就是一个立即数作为操作数,按照GNU汇编语法,立即数表示为“$+整数”。立即数常用来表示代码中的一些常数,如上例中的“$1234”。注意一点的是立即数不能作为目的操作数。
b) 寄存器表示法 — 这种比较简单,它就是表示寄存器之内容。如上面的“movl -4(%ebp), %eax”中的%eax就是使用寄存器表示法作源操作数,而“movl %eax, -8(%ebp)”中的%eax则是使用寄存器表示法作目的操作数。
c) 内存引用表示法 — 计算出的该操作数的值表示的是相应的内存地址。汇编指令根据这个内存地址访问相应的内存位置。如上例“movl -4(%ebp), %eax”中的“-4(%ebp)”,其表示的内存地址为(%ebp寄存器中的内容-4)得到的值。

2) 数据传送指令
汇编语言中最最常用的指令 — 数据传送指令,也是我们接触的第一种类别的汇编指令。其指令的格式为:“mov 源操作数, 目的操作数”。
mov系列支持从最小一个字节到最大双字的访问与传送。其中movb用来传送一字节信息,movw用来传送二字节,即一个字的信息,movl用来传送双字信息。这些不详说了。除此以外mov系列还提供两个带位扩展的指令movsbl和movzbl,我们举个例子来说明一下这两个特殊指令的作用何在:

a) movzbl指令
void dummy1() {
 unsigned char c = 'a';
 unsigned int a = c;
}
其对应的GNU汇编为(省略部分内容):
 movb $97, -1(%ebp)   //'a'的ASCII码为97
 movzbl -1(%ebp), %eax
 movl %eax, -8(%ebp)
说明:在dummy1函数中“unsigned int a = c”语句完成的是一个从unsigned char到unsigned int的赋值操作,由于int的类型长度大于char类型长度,所以实际是将一个字节的内容拷贝到一个可以容纳4个字节的地方,这样的话需要对源数据进行一下扩展,即填充高位的3个字节。

如何填充呢?由于变量a和c都为无符号整型,所以只需要填充0即可。而movzbl就是干这个活的。movzbl指令负责拷贝一个字节,并用0填充其目的操作数中的其余各位,这种扩展方式叫“零扩展”。

b) movsbl指令
void dummy2() {
 signed char c = 'a';
 unsigned int a = c;
}

其对应的GNU汇编为(省略部分内容):
 movb $97, -1(%ebp)   //'a'的ASCII码为97
 movsbl -1(%ebp), %eax
 movl %eax, -8(%ebp)
说明:在dummy2函数中“unsigned int a = c”语句完成的是一个从signed char到unsigned int的赋值操作,由于int的类型长度大于char类型长度,所以实际是将一个字节的内容拷贝到一个可以容纳4个字节的地方,这样的话需要对源数据进行一下扩展,即填充高位的3个字节。如何填充呢?GNU汇编告诉我们它使用了变量c的最高位来填充其余的3个字节。movsbl指令负责拷贝一个字节,并用源操作数的最高位填充其目的操作数中的其余各位,这种扩展方式叫“符号扩展”。实际上dummy2中变量a还是保留了变量c的符号位的,起码GCC是这么做的。

c) 在CS.APP中pushl和popl也别归入“数据传送指令”类别,但对于刚入门选手这两个指令还是稍显复杂,在以后谈到“procedure”时再细说。

3、小结
已经迈出了踏入汇编之门的第一步,汇编的确让我眼前敞亮了许多,看得多了,知道得多了,疑惑也就少了。

4、参考资料
1) 《Computer Systems A Programmer's Perspective》

[注1]
平面寻址:简单的将存储器看成一个大的、按照字节寻址的数组。不区分类型、符号、地址还是整数。注意汇编程序员看到也是进程空间的虚拟地址。

C单元测试包设计与实现

在Java、C++和C#等高级语言的单元测试正进行的如火如荼的时候,C好像做了看客,冷清的躲在了一个不起眼的角落里。C并不是没有单元测试工具,像Check和CUnit这样的工具也很有名气,只是和大名鼎鼎的JUnit比起来,还是显得有些英雄气短。很多大型的C项目,如APR等都没有使用像Check、CUnit这样通用的单元测试框架,而是另起炉灶自己编写。其实编写一个仅能满足单个项目需要的C单元测试工具包并非难事。在部分参考APR的ABTS的前提下,我们也来设计一套自己的简单的C语言单元测试包。

鉴于减少复杂性,我们的目标仅仅是设计和实现一套能在单进程、单线程下工作良好的C单元测试包,我们暂且将之命名为CUT – C Unit test Toolkit。
1、CUT涉及的术语解释
曾经接触过多个有名的单元测试框架如JUnit、CppUnit、TestNG等,它们在对单元测试某些概念的理解上并不是全都一样的。这里我们也有我们自己的定义。
a) 一个逻辑unit test包含至少一个或者多个suite;
b) 一个suite包含至少0或者多个test case;
c) 每个test case中至少包含1个或者多个“断言类”语句。

2、CUT预告片
其实每设计一个程序之前自己都会考虑该提供给用户怎样的东东呢?下面是应该是CUT的经典用法:
cut_ts_t *suite = NULL;

CUT_TEST_BEGIN("classic usage of CUT");

CUT_TS_INIT(suite); 
CUT_TC_ADD(suite, "test case: tc_add", tc_add);
CUT_TC_ADD(suite, "test case: tc_sub", tc_sub);
CUT_TS_ADD(suite, my_setup, my_teardown);

CUT_TEST_RUN();

CUT_TEST_REPORT();

CUT_TEST_END();

3、CUT的组织结构
从上面的经典用法中也可以看出我们的CUT的组织是这样的:
            Test
             |
             |
        +————-+
       TS-1    …  TS-N
        |                |
        |                |
   +——-+ …   +——–+
  TC-1   TC-N     TC-1     TC-N
其中:TS – Test Suite,TC – Test Case

4、CUT接口设计与实现
在“预告片”中我们已经暴露了大部分CUT的重要接口,在下面我们将伴随着实现逐一说明。另外在CUT的实现中我们使用了APR RING技术,不了解APR RING的可以参见我的上一篇Blog“APR分析-环篇”。
1) 主要数据结构
typedef void (*tc_func)(cut_tc_t *tc); /* Test Case标准原型函数指针,所有的Test Case都应该符合这个原型 */
typedef void (*fixture_func)();  /* 用于suite环境建立和拆除的func原型 */

/* Test Case数据结构 */
typedef struct cut_tc_t {
        APR_RING_ENTRY(cut_tc_t)        link;
        char                            name[CUT_MAX_STR_LEN+1];
        tc_func                         func;
        int                             failed;
} cut_tc_t;
typedef APR_RING_HEAD(cut_tc_head_t, cut_tc_t) cut_tc_head_t;

/* Test Suite数据结构 */
typedef struct cut_ts_t {
        APR_RING_ENTRY(cut_ts_t)        link;
        cut_tc_head_t                   tc_head;
        int                             failed; /* 失败用例总数 */
        int                             ran; /* 运行用例总数 */
        fixture_func                    sf; /* setup func */
        fixture_func                    tf; /* teardown func */
} cut_ts_t;
typedef APR_RING_HEAD(cut_ts_head_t, cut_ts_t) cut_ts_head_t;

/* 逻辑单元测试数据结构 */
typedef struct cut_test_t {
        char                            name[CUT_MAX_STR_LEN+1];
        cut_ts_head_t                   ts_head;
} cut_test_t;

2) CUT_TEST_BEGIN和CUT_TEST_END
这两者分别是一个逻辑Test的开始与结束。我们在CUT_TEST_BEGIN建立好我们的内部数据结构,其唯一宏参数用来加强可读性,在CUT_TEST_END中释放在测试过程中获取的系统资源。其实现如下:
#define CUT_TEST_BEGIN(desc) \
        cut_test_t *_cut_test = NULL; \
        _cut_test = malloc(sizeof(cut_test_t)); \
        if (_cut_test == NULL) { \
                return errno; \
        } \
        memset(_cut_test, 0, sizeof(cut_test_t)); \
        APR_RING_INIT(&(_cut_test->ts_head), cut_ts_t, link); \
        strncpy(_cut_test->name, desc, CUT_MAX_STR_LEN)

#define CUT_TEST_END() do { \
  /* 这里遍历Ring,释放其他相关内存,这里限于篇幅未写出 */
                if (_cut_test != NULL) { \
                        free(_cut_test); \
                } \
        } while(0)

3) CUT_TS_ADD和CUT_TC_ADD
前者负责向一逻辑单元测试中添加Test Suite,后者则负责向一个Test Suite中添加测试用例。在CUT中,每个Test Suite依赖两个Fixture Function- setup和teardown。setup用于建立测试环境,比如打开某文件,获得文件句柄供该Test Suite中的若干Test Case使用;而teardown则用来做后处理,释放setup以及在众多Test Case执行时分配的资源,比如上面关闭提到的文件句柄。

在实现CUT的Test Suite时,实际上加了一个对用户使用的限制,那就是CUT负责管理Test Suite的内存分配,说限制也好我觉得倒是给用户提供了一种方便。这两个宏的实现如下:
#define CUT_TEST_SUITE_INIT(suite) do { \
                if (suite == NULL) { \
                        suite = malloc(sizeof(cut_ts_t)); \
                        if (suite == NULL) { \
                                return errno; \
                        } \
                } \
                memset(suite, 0, sizeof(cut_ts_t)); \
                APR_RING_INIT(&(suite->tc_head), cut_tc_t, link); \
                suite->ran = 0; \
                suite->failed = 0; \
        } while(0)

#define CUT_TS_ADD(suite, f1, f2) do { \
                APR_RING_ELEM_INIT(suite, link); \
                suite->sf = f1; \
                suite->tf = f2; \
                APR_RING_INSERT_TAIL(&(_cut_test->ts_head), suite, cut_ts_t, link); \               
        } while(0)

#define CUT_TC_ADD(suite, desc, f1) do { \
                cut_tc_t *tc = NULL; \
                tc = malloc(sizeof(cut_tc_t)); \
                if (tc == NULL) { \
                        return errno; \
                } \
                memset(tc, 0, sizeof(cut_tc_t)); \
                strncpy(tc->name, desc, CUT_MAX_STR_LEN); \
                tc->func = f1; \
                APR_RING_ELEM_INIT(tc, link); \
                APR_RING_INSERT_TAIL(&(suite->tc_head), tc, cut_tc_t, link); \
        } while(0)

4) CUT_TEST_RUN和CUT_TEST_REPORT
这两个宏的作用分别是运行所有逻辑单元测试中的测试用例和报告测试情况,在这里CUT_TEST_REPORT输出形式较为简单,只是打印出此次单元测试运行用例总数和失败的用例数。当然要丰富其输出形式,让用户更快更早定位哪个测试用例失败也并不难,只需对CUT的实现稍作修改即可,这里仅是抛砖引玉。具体可参见成熟的工具的输出形式,如CUnit等。
#define CUT_TEST_RUN() do { \
                cut_ts_t *ts = NULL; \
                cut_tc_t *tc = NULL; \
                APR_RING_TRAVERSE(ts, &(_cut_test->ts_head), cut_ts_t, link) { \
                        if (ts != NULL) { \
                                if (ts->sf != NULL) { \
                                        ts->sf(); \ /* execute setup func */
                                } \
                                APR_RING_TRAVERSE(tc, &(ts->tc_head), cut_tc_t, link) { \
                                        if (tc != NULL) { \
                                APR_RING_TRAVERSE(tc, &(ts->tc_head), cut_tc_t, link) { \
                                        if (tc != NULL) { \
                                                tc->func(tc); \
                                        } \
                                } \
                                if (ts->tf != NULL) { \
                                        ts->tf(); \ /* execute teardown func */
                                } \
                        } \
                } \
        } while(0)

#define CUT_TEST_REPORT() do { \
                int ran = 0; \
                int failed = 0; \
                cut_ts_t *ts = NULL; \
                cut_tc_t *tc = NULL; \
                APR_RING_TRAVERSE(ts, &(_cut_test->ts_head), cut_ts_t, link) { \
                        if (ts != NULL) { \
                                APR_RING_TRAVERSE(tc, &(ts->tc_head), cut_tc_t, link) { \
                                        if (tc != NULL) { \
                                                ran++; \
                                                failed += tc->failed; \
                                        } \
                                } \
                        } \
                } \
                printf("total tc is %d, and failed tc is %d\n", ran, failed); \
        } while(0)

5) 断言集合
评价一个单元测试工具好坏的重要标准之一就是它的断言集的多寡和易用性。大部分单元测试工具都提供几十个各种各样的断言接口,我这里仅仅是举一个断言接口例子:
我们提供一个整型数判等断言接口:
void cut_int_equal(cut_case_t *tc, const int expected, const int actual, int lineno)
{
        if (expected != actual) {
                tc->failed += 1;
  /* 其他处理,如记录断言发生位置信息等 */
        }
}

这样我们就可以在我们的测试用例中这样使用了:
void tc_add(cut_case_t *tc) {
        int a = 1;
        int b = 2;
        cut_int_equal(tc, 3, add(a, b), __LINE__);
}

5、一个简单但完整的测试实例
CUT以头文件和静态库的形式发布,使用CUT只需要引用其头文件,并在链接时链接CUT的静态库即可。
在下面的例子中我们执行了两个测试用例:
#include "cut.h"
#include "my_math.h" //for add and sub interface

void my_setup() {
        printf("setup for suite\n");
}

void my_teardown() {
        printf("teardown for suite\n");
}

void tc_add(cut_case_t *tc) {
        int a = 1;
        int b = 2;
        cut_int_equal(tc, 3, add(a, b), __LINE__);
}

void tc_sub(cut_case_t *tc) {
        int a = 3;
        int b = 1;
        cut_int_equal(tc, 1, sub(a, b), __LINE__); // 会导致断言错误
}

int main() {
        cut_suite_t *suite = NULL;

        CUT_TEST_BEGIN("test with cut");

        CUT_TEST_SUITE_INIT(suite);

        CUT_TC_ADD(suite, "test tpl_addition:", tc_add);
        CUT_TC_ADD(suite, "test tpl_subtraction:", tc_sub);
        CUT_TS_ADD(suite, my_setup, my_teardown);

        CUT_TEST_RUN();

        CUT_TEST_REPORT();

        CUT_TEST_END();

        return 0;
}

测试结果:
total tc is 2, and failed tc is 1

6、小结
这里仅仅是提出一种实现C Unit Testing Framework的方案,而且仅仅是证明其可行,其离成熟的程度还远得很。我们可以从已经成熟的单元测试工具那里借鉴很多东西过来,如Test Group概念、XML配置等。改进是永无止境的,任重道远啊:)。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats