公版ubuntu自带memtest86+内存测试工具,出于工作需要,分析了其工作流程记录于此。
分析一个陌生的程序,当然得先找入口入口函数,很可惜main()/_start之类的都找到,唯一看着像入口点的main.c文件也没找到可能的入口点。看来只能从makefile文件分析了。
OBJS= head.o reloc.o main.o test.o init.o lib.o patn.o screen_buffer.o \ config.o linuxbios.o memsize.o pci.o controller.o random.o spd.o \ error.o dmi.o cpuid.o all: memtest.bin memtest # Link it statically once so I know I don't have undefined # symbols and then link it dynamically so I have full # relocation information memtest_shared: $(OBJS) memtest_shared.lds Makefile $(LD) --warn-constructors --warn-common -static -T memtest_shared.lds \ -o $@ $(OBJS) && \ $(LD) -shared -Bsymbolic -T memtest_shared.lds -o $@ $(OBJS) memtest_shared.bin: memtest_shared objcopy -O binary $< memtest_shared.bin memtest: memtest_shared.bin memtest.lds $(LD) -s -T memtest.lds -b binary memtest_shared.bin -o $@ head.s: head.S config.h defs.h test.h $(CC) -E -traditional $< -o $@ bootsect.s: bootsect.S config.h defs.h $(CC) -E -traditional $< -o $@ setup.s: setup.S config.h defs.h $(CC) -E -traditional $< -o $@ memtest.bin: memtest_shared.bin bootsect.o setup.o memtest.bin.lds $(LD) -T memtest.bin.lds bootsect.o setup.o -b binary \ memtest_shared.bin -o memtest.bin这几个规则指明了源码目录中的obj文件如何链接成memtest86+.bin,而链接过程又由3个lds文件提供:
#memtest_shared.lds链接脚本:规则memtest_shared依赖的lds脚本 OUTPUT_FORMAT("elf32-i386"); OUTPUT_ARCH(i386); ENTRY(startup_32); #指定$(OBJS)的入口点为head.S!startup_32 SECTIONS { . = 0; .text : ... #重要的节,后面重定位时,会用got表中的信息对memtest_share进行重定向 .got : { *(.got.plt) *(.got) _edata = . ; } . = ALIGN(4); ... _end = .; } /DISCARD/ : { *(*) } }
ubuntu:~/Desktop/memtest86+-4.20$ file memtest_shared memtest_shared: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, not stripped
ubuntu:~/Desktop/memtest86+-4.20$ readelf -a memtest_shared|grep startup 71: 00000000 0 NOTYPE GLOBAL DEFAULT 1 startup_32 #这个符号在head.S中 545: 00000000 0 NOTYPE GLOBAL DEFAULT 1 startup_32看下head.S的头几行
.code32 .globl startup_32 startup_32: cld cli代码段开始就申明了标号startup_32,理论上memtest_share的入口点就是这个了
#memtest.lds链接脚本:规则memtest的依赖项 OUTPUT_FORMAT("elf32-i386"); OUTPUT_ARCH(i386); ENTRY(_start); SECTIONS { . = 0x5000; _start = . ; .data : { *(.data) } }
#memtest.bin链接脚本:规则memtest.bin的依赖项 OUTPUT_FORMAT("binary") OUTPUT_ARCH("i386") ENTRY(_main);#指明入口点为_main SECTIONS { . = 0; #左边是输出:右边是输入 .bootsect : { *(.bootsect) } .setup : { *(.setup) } .memtest : { _start = . ; *(.data) _end = . ; } _syssize = (_end - _start + 15) >> 4; }
既然知道了整个memtest86+文件分布,也知道了程序入口是bootsect.S!_main,那就可以分析整个程序的流程了,打开bootsect.S
#include "defs.h" .code16 #告诉汇编器,这里要生成16bit代码 .section ".bootsect", "ax", @progbits _boot: # ld86 requires an entry symbol. This may as well be the usual one. .globl _main _main: movw $BOOTSEG, %ax movw %ax, %ds #ds=0x7c00 movw $INITSEG, %ax movw %ax, %es movw $256, %cx subw %si, %si subw %di, %di一堆汇编代码,还有一些奇奇怪怪的立即数,真像直接不干了!查找$BOOTSEG的定义,其值为0x07c0。汇编代码前2句是把0x7c0赋值给段寄存器ds
#define LOW_TEST_ADR 0x00002000 /* Final adrs for test code */ #define BOOTSEG 0x07c0 /* Segment adrs for inital boot */ #define INITSEG 0x9000 /* Segment adrs for relocated boot */ #define SETUPSEG (INITSEG+0x20) /* Segment adrs for relocated setup */ #define TSTLOAD 0x1000 /* Segment adrs for load of test */ #define KERNEL_CS 0x10 /* 32 bit segment adrs for code */ #define KERNEL_DS 0x18 /* 32 bit segment adrs for data */ #define REAL_CS 0x20 /* 16 bit segment adrs for code */ #define REAL_DS 0x28 /* 16 bit segment adrs for data */如果你做过i386处理器,马上会想到bootsect.S和setup.S是一个启动cpu进入32位保护模式的bootloader,同时从磁盘上加载剩余的程序到内存设置程序运行环境!因为bootloader的功能大同小异,这里略过处理功能这些的代码。
setup.S最终会调用memtest.share!。前面说过memtest.share是linux elf文件格式,而memtest86+这个程序显然没有运行linux内核,因此为了运行elf文件,它需要自己实现loader的功能,把memtest.share加载到内存并读取got重定位表对其中的重定位信息进行重定位,如下:
#head.S 0: /* Load the GOT pointer */ #call-pop 获得程序运行时,当前指令在内存中的地址,以后ebx就作为重定位的参考地址 call 0f 0: popl %ebx addl $_GLOBAL_OFFSET_TABLE_+[.-0b], %ebx ... leal gdt@GOTOFF(%ebx), %eax movl %eax, 2 + gdt_descr@GOTOFF(%ebx) lgdt gdt_descr@GOTOFF(%ebx) #gdt_descr是一个需要重定位的变量 leal flush@GOTOFF(%ebx), %eaxhead.S就是通过这个办法对memtest.share中导出的重定位符号进行重定位。可以通过readelf -a查看重定位信息。
待到重定位结束,head.S通过call do_test进入main.c开始内存测试。
//下面是do_test的伪代码 void do_test(void) { /*如果memtest是由grub启动的,grub会把grub.cfg中的启动参数存放到boot_param中,parse_command_line获得boot_param*/ parse_command_line(); switch(tseq[v->test].pat) { case N://具体的测试项 break; } window++; //前面setup.S通过e820获得的内存图,把内存分布存放到数组windows中,而windows是windows中的一项数组元素 //还有部分e820数组中的内存块没有在这一项test中测试过,通过run_at进行下一次测试 if (window != 0) { run_at(LOW_TEST_ADR); } else { //e820数组中所有内存块都通过了这一项test测试,进入下一项测试 v->test++; run_at(LOW_TEST_ADR); } }上面的伪代码多次出现run_at,这是整个memtest86+中最奇葩的操作:
static void __run_at(unsigned long addr) { /* Copy memtest86+ code */ memmove((void *)addr, &_start, _end - _start); /* Jump to the start address */ p = (ulong *)(addr + startup_32 - _start); goto *p; } static unsigned long run_at_addr = 0xffffffff; static void run_at(unsigned long addr) { unsigned long start; unsigned long len; run_at_addr = addr; start = (unsigned long) &_start; len = _end - _start; if ( ((start < addr) && ((start + len) >= addr)) || ((addr < start) && ((addr + len) >= start))) { /* Handle overlap by doing an extra relocation */ if (addr + len < high_test_adr) { __run_at(high_test_adr); } else if (start + len < addr) { __run_at(LOW_TEST_ADR); } } __run_at(run_at_addr); }获得_start标号的位置,然后重新跳到_start去运行。那么,标号_start定义在哪?反汇编看一下
objdump -d memtest_share 00000000 <_start>: 0: fc cld 1: fa cli 2: 85 e4 test %esp,%esp 4: 75 0c jne 12 <_start+0x12> 6: bc 5a c7 02 00 mov $0x2c75a,%esp b: 8d a4 24 20 20 00 00 lea 0x2020(%esp),%esp 12: e8 00 00 00 00 call 17 <_start+0x17> 17: 5b pop %ebx 18: 81 c3 49 a7 02 00 add $0x2a749,%ebx 1e: 8d a3 20 20 00 00 lea 0x2020(%ebx),%esp 24: 8d 83 c8 5e fd ff lea -0x2a138(%ebx),%eax 2a: 89 83 c2 5e fd ff mov %eax,-0x2a13e(%ebx) 30: 0f 01 93 c0 5e fd ff lgdtl -0x2a140(%ebx) 37: 8d 83 e1 58 fd ff lea -0x2a71f(%ebx),%eax 3d: 6a 10 push $0x10 3f: 50 push %eax 40: cb lret 00000041 <flush>: 41: b8 18 00 00 00 mov $0x18,%eax_start定义在head.S中,并且就在head.S的开头。也就是,为了实现跳转,memtest_share跳到程序开头重新再跑一次!!!!尼玛,我都震惊了,好在windows数组和window是全局变量,要不然下次进到do_test都不知道上回测到哪了....