公版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都不知道上回测到哪了....