.previous
摘自百度文库:
linux源代码中汇编语言部分总是有.previous、.section、.long,这是一个ELF段堆栈操作命令。其他的段堆栈操作命令还有.section、.subsection、.pushsection、.popsection,本命令交换当前段(及其子段)和最近访问过的段(及其子段)。多个连续的.previous命令将使当前位置两个段(及其子段)之间反复切换。用段堆栈的术语来说,本命令使当前段和堆顶段交换位置。
previous表示恢复到当前.section定义之前的那个段作为当前段。也就是说这个previous与它前面的那个section一般成对出现.
只不过是一段汇编,主要还是对目标地址进行读写,不过在代码中添加了修正代码(.fixup),其目的就是当发生异常时执行该修正代码,防止内核被BUG掉。
1. __copy_user
宏__copy_user在include/asm-i386/uaccess.h中定义,是作为从用户空间和内核空间进行内存复制的关键。这个宏扩展为汇编后如下:
000 #define __copy_user(to,from,size)
001 do {
002 int __d0, __d1;
003 __asm__ __volatile__(
004 "0: rep; movsl\n"
005 " movl %3,%0\n"
006 "1: rep; movsb\n"
007 "2:\n"
008 ".section .fixup,\"ax\"\n"
009 "3: lea 0(%3,%0,4),%0\n"
010 " jmp 2b\n"
011 ".previous\n"
012 ".section __ex_table,\"a\"\n"
013 " .align 4\n"
014 " .long 0b,3b\n"
015 " .long 1b,2b\n"
016 ".previous"
017 : "=&c"(size), "=&D" (__d0), "=&S" (__d1)
018 : "r"(size & 3), "0"(size / 4), "1"(to), "2"(from)
019 : "memory");
020 } while (0)
这段代码的主要操作就是004-007行,它的主要功能是将from处长度为size的数据复制到to处。
看这段代码之前,先看看它的约束条件:
017 : "=&c"(size), "=&D" (__d0), "=&S" (__d1)
018 : "r"(size & 3), "0"(size / 4), "1"(to), "2"(from)
019 : "memory");
017是输出部,根据描述可知size保存在ecx中,__d0保存在DI中,__d1保存在SI中。
018是输入部,根据描述可知size/4(即size除以4后的整数部分)保存在ecx中,size&3(即size除以4的余数部分)随便保存在某一个寄存器中,to保存在DI中,from保存在SI中。
然后再反过头来看004-007行,就明白了:
004行:将size/4个4字节从from复制到to。为了提高速度,这里使用的是movsl,所以对size也要处理一下。
005行:将size&3,即size/4后余下的余数,复制到ecx中。
006行:根据ecx中的数量,从from复制数据到to,这里使用的是movsb。
007行:代码结束。
到这里,复制就结束了。
但是实际上没有这么简单,因为还可能发生复制不成功的现象,所以008-016行的代码都是进行此类处理的。
内核提供了一个意外表,它的每一项的结构是(x,y),即如果在地址x上发生了错误,那么就跳转到地址y处,这里行012-015就是利用了这个机制在编译时声明了两个表项。将这几行代码说明如下:
012行:声明以下内容属于段__ex_table。
013行:声明此处内容4字节对齐。
014行:声明第一个意外表项,即如果在标志0处出错,就跳转到标志3处(.section .fixup段中)。
015行:声明第二个意外表项,即如果在标志1处出错,就跳转到标志2处(.section .text段中)。
上面之所以要在标志后面加上b,是因为引用之前的代码,如果要引用之后的代码就加f。
这里对size的操作约定是:如果复制失败,则size中保留的是还没有复制完的数据字节数。
由于复制数据的代码只有4行,其中可能出现问题的就是004和006行。从上面的异常表可以看出,内核的处理策略是:
(1) 如果在0处出错,那么这时没有复制完的字节数就是ecx中剩余的数字乘以4加上先前size除以4以后的那个余数。009行代码即完成此任务,“lea 0(%3,%0,4),%0”即计算“%ecx = (size % 4) + %ecx * 4”,并将这个数值赋值给返回C代码的size中。
(2)如果在1处出现错误,那么由于之前ecx中的size/4个字节都已经复制成功了,所以只需要将保存在任意一个寄存器中的size/4的余数赋值给size返回。
从汇编代码中可以看到,009行的异常处理代码被编译到一个叫做fixup的段中。
可见这段代码的本质就是从from复制数据到to,并对两处可能出现错误的地方进行简单的异常处理——返回未复制的字节数。
注意:
(1).section .fixup,"ax";.section __ex_table,"a";
将这两个.section和.previous中间的代码汇编到各自定义的段中,然后跳回去,将这之后的的代码汇编到.text段中,也就是自定义段之前的段。.section和.previous必须配套使用。
(2)例子中__ex_table异常表的安排在用户空间是不会得到执行的,它只在内核中有效。
自己写了个测试下:
文件sramboot.S
1 #include"regdef.h"
2 .set noreorder
3 .text
4 .globl start
5 start:
6 .set noat
7 la at,value
8 lw a1, 4(at)
9 nop
10 add a2, a0, a1
11 sw a2, 8(at)
12
13 .data
14 .section soochow ,"a"
15 .align 2
16 value:
17 .long 0x0001
18 .section seu,"a"
19 .long 0x0004
20 .previous
21 .long 0x0002
22 .long 0x0003
23 .section seu,"a"
24 .long 0x0005
Makefile文件
1CROSS_COMPILE=mipsellinux-
2 CC = $(CROSS_COMPILE)gcc
3 AS = $(CROSS_COMPILE)as
4 LD = $(CROSS_COMPILE)ld
5 OBJDUMP = $(CROSS_COMPILE)objdump
6 OBJCOPY = $(CROSS_COMPILE)objcopy
7 TEXTBASE =# -Ttext
8 DATABASE =
9 LDFLAGS = $(TEXTBASE) $(DATABASE) -T my.ld
10
11 test.bin:test.exe
12 $(OBJCOPY) -O binary test.exe test.bin
13
14 test.exe: sramboot.o
15 $(LD) sramboot.o $(LDFLAGS) -o test.exe
16 $(OBJDUMP) -D test.exe > test.dump
17
18 sramboot.o:sramboot.S
19 $(CC) -E sramboot.S -o sramboot.s
20 $(AS) sramboot.s -o sramboot.o
21 @rm sramboot.s
22
23 clean:
24 @rm *.o *.exe *.bin *.dump
链接脚本my.ld
1 OUTPUT_FORMAT("elf32-tradlittlemips","elf32-tradbigmips","elf32-tradlittlemips")
2 OUTPUT_ARCH(mips)
3 ENTRY(start)
4 SECTIONS
5 {
6 . =0;
7 .text :
8 {
9 *(.text)
10 }
11 .data :
12 {
13 *(.data)
14 }
15 .bss :
16 {
17 *(.bss)
18 }
19 }
bin文件反汇编脚本test.sh
1#!/bin/sh
2 A=0
3 if [ ! -e test.bin ]
4 then
5 make
6 A=1
7 fi
8
9 mipsel-linux-objdump -D -m mips -b binary -EL -M no-aliases -z test.bin >my.dump
10
11 if [ $A = 1 ]
12 then
13 rm sramboot.o *bin *exe
14 fi
结果: