memtest86+4.20流程分析

    公版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/ : { *(*) }	
}

memtest_share规则生成memset_share文件,是由源码目录下所有.c文件和head.S链接后生成的,这个文件是标准的linux ELF文件

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仅仅把前一个规则生成的memtest_share作为输入链接到.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+中最重要的一个脚本,makefile参考它生成了整个memtest86+二进制文件。同时还规定了源码目录下所有obj文件在bin文件中的布局:bootsect在最前端,紧接着是setup,最后是memtest_share。

    既然知道了整个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), %eax
head.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都不知道上回测到哪了....

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值