编写简单的c运行库(一)
看了《程序员自我修养》这本书后,对目标文件、可执行文件的结构有了比较清晰的了解,对目标文件链接成可执行文件的过程和程序运行库有了大致的认识。不过正如“纸上得来终觉浅,绝知此事需恭行”,很多东西看似容易,但实践的时候却往往不是这样,在实践中往往能发现很多的问题。《程序员自我修养》这本书我觉得是理论与实践很好的结合了,它在最后一章给出了一个c和c++运行库的简单版的实现,通过实现这个可以更为深刻地理解可执行文件的结构、程序的执行、运行库的实现。参考这边书,我在linux下实现的一个简单的c运行库,这个运行库主要实现了文件操作、字符串操作、动态内存分配三个方面。
1 程序的入口函数实现
当被问到程序的入口函数是什么的时候,很多人都会回答是main函数。其实这是不准确的,因为如果main是第一个开始执行的函数,那么对于在main函数外面定义的变量,特别是c++中的对象,由谁来初始化它们呢?还有就是我们用atexit函数注册的清理函数在main函数结束之后才被调用。种种都说明在main函数之外还有函数,它负责建立程序执行所需的环境,包括变量的初始化和堆的初始化,清理函数的调用,构造函数和析构函数调用(如果是c++的话)。这个函数在为程序准备好了运行环境之后才开始调用main函数。我实现的入口函数很简单,它主要负责对堆进行初始化、调用main函数和退出程序。
1 /*初始化堆*/ 2 if (CrtInitHeap() == -1) 3 CrtFataError("init heap error\n"); 4 /*获取argc和argv*/ 5 __asm__ volatile( 6 "movl 0x4(%%ebp), %0\n\t" 7 "lea 0x8(%%ebp), %1" 8 :"=r"(argc),"=r"(argv) 9 ); 10 environ = &argv[argc + 1]; 11 ret = main(argc, argv); 12 exit(ret);
当程序被加载到内存中,在运行上面的入口函数前,操作系统就将程序的命令行参数和环境变量放到了该程序的栈中,这时候栈的分布为:
此时,栈顶指针指向ebp,正如我在程序的栈结构中说的那样,当进入函数时,通常做的第一件事就是把ebp保存到栈中。从上图可以知道argc在esp+4中,argv在esp+8中,程序中用了嵌入汇编来获得它们的值,程序中第10行获得环境变量,然后就是调用main函数了,向main中传递了argc和argv(现在可以明白为什么我们可以再main中直接用argc和argv了吧!!),最后根据main的返回值调用exit结束程序。
这里最主要的还是初始化堆和管理堆比较麻烦些,为什么要初始化堆呢?因为我们程序经常要用到malloc函数来动态申请内存,而malloc函数是从堆中获取内存的。你可能会说堆不是在程序开始运行前就由操作系统分配好了堆给程序吗?其实我们这个初始化堆的意思是说把分配给堆的地址空间变大,并不是真正的申请内存,只有当程序用malloc申请并用到时才真正分配内存(用时才分配,这个是内存管理的时,我们就不深究了)。因为程序运行时给堆的地址空间是比较小的,所以需要初始化堆。管理堆的工作主要是负责堆的分配和回收。
2 堆的初始化
正如上面讲的,我们的堆初始化就是扩充堆的地址空间。我们可以用系统调用brk来完成这个,不过现在我们不能用glibc的库,所以只能用汇编来调用了brk系统调用了。
1 int brk(void *addr) 2 { 3 int ret; 4 5 __asm__ volatile( 6 "movl %1, %%ebx\n\t" 7 "movl $45, %%eax\n\t" 8 "int $0x80 \n\t" 9 :"=a"(ret) 10 :"m"(addr) 11 ); 12 return ret; 13 }
brk的系统调用号时45,当传递的参数是NULL时,brk会返当前堆的起始地址,brk的参数是堆的结束地址,所以要扩充堆地址空间,必须先获取堆的起始地址,然后根据起始地址来扩充堆地址空间。
1 void *heapBase, *heapEnd; 2 int heapSize = 1024 * 1024 * 32; 3 4 /*扩展堆的大小到32MB*/ 5 heapBase = (void *)brk((void *)0); 6 heapEnd = (void *)brk(ADDR_ADD(heapBase, heapSize));7 if (heapEnd == heapBase) 8 return -1;
3 堆的管理
堆的工作主要是管理用brk申请来的空间,负责空间的分配和回收。我用的是双向链表实现的,链表的节点结构为heap_header结构体,当用malloc函数申请堆空间时,采用最先适应算法,只要找到一个满足申请空间大小的空闲节点,就将该节点所在的空间分配给它,如果该节点的空间比较大,则拆分这个节点为两个节点,前一个节点分配申请空间的大小,并标记为占用,后一个为空闲节点。在用free函数释放堆空间时,确定该空间所在的节点,并将该节点标记为空闲,查找该节点的相邻节点是否是空闲节点,如果是则合并。
堆空间中节点的数据结构为:
typedef struct _heap_header { enum heap_state state;/*是否空闲*/ int size;/*本节点空间大小*/ struct _heap_header *next;/*下一个节点*/ struct _heap_header *pre;/*上一个节点*/ }heap_header;
附件:入口函数和堆
单元测试的两种方式
在单元测试中,可通过两种方式来验证代码是否正确地工作。一种是基于结果状态的测试,一种是基于交互行为的测试。
测试结果与测试行为之间有什么区别呢?
基于结果状态的测试,也就意味着我们需要验证被测试代码需要返回正确的结果。
1 [TestMethod] 2 public void TestSortNumberResult() 3 { 4 IShellSorter<int> shellSorter = new ShellSorter<int>(); 5 IBubbleSorter<int> bubbleSorter = new BubbleSorter<int>(); 6 7 NumberSorter numberSorter = new NumberSorter(shellSorter, bubbleSorter); 8 int[] numbers = new int[] { 3, 1, 2 }; 9 numberSorter.Sort(numbers); 10 11 // 验证返回值是否已经被正确排序。 12 // 只要返回值正确即可,并不关心使用了哪个算法。 13 CollectionAssert.AreEqual(new int[] { 1, 2, 3 }, numbers); 14 }
基于交互行为的测试,也就意味着我们需要验证被测试代码是否正确合理地调用了某些方法。
1 [TestMethod] 2 public void TestUseCorrectSortingAlgorithm() 3 { 4 IShellSorter<int> mockShellSorter = Substitute.For<IShellSorter<int>>(); 5 IBubbleSorter<int> mockBubbleSorter = Substitute.For<IBubbleSorter<int>>(); 6 7 NumberSorter numberSorter = new NumberSorter(mockShellSorter, mockBubbleSorter); 8 int[] numbers = new int[] { 3, 1, 2 }; 9 numberSorter.Sort(numbers); 10 11 // 验证排序器是否使用冒泡排序算法。 12 // 如果排序器未使用冒泡排序算法,或者使用了该算法但传递了错误的参数,则验证失败。 13 mockBubbleSorter.Received().Sort(Arg.Is<int[]>(numbers)); 14 }
第二种测试方法可能会得出较好的代码覆盖率,但它却没有告诉我们排序结果是否正确,而只是确认调用了 bubbleSorter.Sort() 方法。所以交互行为测试并不能证明代码可以正确工作。这也就是在大多数情况下,我们需要测试结果和状态,而不是测试交互和行为。
通常来说,如果程序的正确性不能仅仅靠程序的输出结果来决定,而还需要判断结果是怎么产生的,在这种条件下,我们就需要对交互和行为进行测试。在上面的示例中,你可能想在得到正确测试结果的前提下,额外的再测试下交互行为,因为可能确认正确地使用了某种算法非常重要,例如某些算法在给定条件下运行速度更快,否则的话测试交互行为的意义并不大。
通常在什么条件下需要对交互行为进行测试呢?
这里给出两种较适合的场景:
- 假设被测试代码需要调用了一个方法,但可能由于其被调用的次数不同,或者被调用的顺序不同,而导致产生了不同的结果,或者出现了其他类似时间延迟、多线程死锁等副作用。例如该方法负责发送邮件,我们需要确认只调用了一次邮件发送函数。或者例如该方法的不同调用顺序会产生不同的线程锁控制,导致死锁。在类似这些情况下,测试交互行为可以有效地帮助你确认方法调用是否正确。
- 假设我们在测试一个UI程序,其中已经通过抽象将UI渲染部分与UI逻辑部分隔离,可以考虑是某种MVC或MVVM模式。那么在我们测试 Controller 或 ViewModel 层时,如果有的话,可能只关心 View 上的哪些方法被调用了,而并不关系具体该方法内部是如何渲染的,所以此处测试与 View 的交互就比较合适。类似的,对于 Model 层也一样。
完整代码
1 [TestClass] 2 public class UnitTestTwoWays 3 { 4 public interface IShellSorter<T> 5 where T : IComparable 6 { 7 void Sort(T[] list); 8 } 9 10 public interface IBubbleSorter<T> 11 where T : IComparable 12 { 13 void Sort(T[] list); 14 } 15 16 public class ShellSorter<T> : IShellSorter<T> 17 where T : IComparable 18 { 19 public void Sort(T[] list) 20 { 21 int inc; 22 23 for (inc = 1; inc <= list.Length / 9; inc = 3 * inc + 1) ; 24 25 for (; inc > 0; inc /= 3) 26 { 27 for (int i = inc + 1; i <= list.Length; i += inc) 28 { 29 T t = list[i - 1]; 30 int j = i; 31 32 while ((j > inc) && (list[j - inc - 1].CompareTo(t) > 0)) 33 { 34 list[j - 1] = list[j - inc - 1]; 35 j -= inc; 36 } 37 38 list[j - 1] = t; 39 } 40 } 41 } 42 } 43 44 public class BubbleSorter<T> : IBubbleSorter<T> 45 where T : IComparable 46 { 47 public void Sort(T[] list) 48 { 49 int i, j; 50 bool done = false; 51 52 j = 1; 53 while ((j < list.Length) && (!done)) 54 { 55 done = true; 56 57 for (i = 0; i < list.Length - j; i++) 58 { 59 if (list[i].CompareTo(list[i + 1]) > 0) 60 { 61 done = false; 62 T t = list[i]; 63 list[i] = list[i + 1]; 64 list[i + 1] = t; 65 } 66 } 67 68 j++; 69 } 70 } 71 } 72 73 public interface INumberSorter 74 { 75 void Sort(int[] numbers); 76 } 77 78 public class NumberSorter : INumberSorter 79 { 80 private IShellSorter<int> _shellSorter; 81 private IBubbleSorter<int> _bubbleSorter; 82 83 public NumberSorter( 84 IShellSorter<int> shellSorter, 85 IBubbleSorter<int> bubbleSorter) 86 { 87 _shellSorter = shellSorter; 88 _bubbleSorter = bubbleSorter; 89 } 90 91 public void Sort(int[] numbers) 92 { 93 _bubbleSorter.Sort(numbers); 94 } 95 } 96 97 [TestMethod] 98 public void TestSortNumberResult() 99 { 100 IShellSorter<int> shellSorter = new ShellSorter<int>(); 101 IBubbleSorter<int> bubbleSorter = new BubbleSorter<int>(); 102 103 NumberSorter numberSorter = new NumberSorter(shellSorter, bubbleSorter); 104 int[] numbers = new int[] { 3, 1, 2 }; 105 numberSorter.Sort(numbers); 106 107 // 验证返回值是否已经被正确排序。 108 // 只要返回值正确即可,并不关心使用了哪个算法。 109 CollectionAssert.AreEqual(new int[] { 1, 2, 3 }, numbers); 110 } 111 112 [TestMethod] 113 public void TestUseCorrectSortingAlgorithm() 114 { 115 IShellSorter<int> mockShellSorter = Substitute.For<IShellSorter<int>>(); 116 IBubbleSorter<int> mockBubbleSorter = Substitute.For<IBubbleSorter<int>>(); 117 118 NumberSorter numberSorter = new NumberSorter(mockShellSorter, mockBubbleSorter); 119 int[] numbers = new int[] { 3, 1, 2 }; 120 numberSorter.Sort(numbers); 121 122 // 验证排序器是否使用冒泡排序算法。 123 // 如果排序器未使用冒泡排序算法,或者使用了该算法但传递了错误的参数,则验证失败。 124 mockBubbleSorter.Received().Sort(Arg.Is<int[]>(numbers)); 125 } 126 }
关于单元测试 mocking 技术,请参考《NSubstitute完全手册》。