内核汇编.docx
- 文档编号:11040532
- 上传时间:2023-02-24
- 格式:DOCX
- 页数:14
- 大小:24.37KB
内核汇编.docx
《内核汇编.docx》由会员分享,可在线阅读,更多相关《内核汇编.docx(14页珍藏版)》请在冰豆网上搜索。
内核汇编
linux源代码中汇编语言部分总是有.previous、.section、.long,这是一个ELF段堆栈操作命令。
其他的段堆栈操作命令还有.section、.subsection、.pushsection、.popsection,本命令交换当前段(及其子段)和最近访问过的段(及其子段)。
多个连续的.previous命令将使当前位置两个段(及其子段)之间反复切换。
用段堆栈的术语来说,本命令使当前段和堆顶段交换位置。
我的理解:
section是elf文件中的一个节,ELF文件中代码、连接信息和注释是以节(section)为单位存放的,并由一个节头表(sectionheader)来控制组织。
节头表可以看作是一个ELF32_Shdr结构的数组,其中每一项都对应文件中的一个节。
程序运行读入内存时,是以程序段(programsegment)为单位读入的。
程序段的内容由若干节(section)组成,节的内容组合在一起连成一片构成程序段的内容。
所以.section是给gcc或者ld等工具用来生产可执行文件或者共享库或者重定向文件用的。
previous表示恢复到当前.section定义之前的那个段作为当前段。
也就是说这个previous与它前面的那个section一般成对出现.
.long?
在哪里
一个elf文件(例如动态库)一般来说在文本段有以下几个section:
.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_d
.gnu.version_r
.rel.data
.rel.got
.rel.plt
.init
.plt
.text
.fini
.rodata
数据段包括:
.data
.eh_frame
.ctors
.dtors
.got
.dynamic
.bss:
其他的还有:
.comment
.note
.shstrtab
.symtab
.strtab
等等[/Color]
用户进程的地址映射有的时候还没建立(如堆栈),在pg_dir里页没相应的设置。
如果在用户态对这个地址进行访问,就会产生页面异常,在异常处理函数中会根据实际情况(如检查VM_GROWSDOWN等标志)对页面重新建立映射。
而在内核进程访问这些地址时,如果不用get_user等函数的话,一旦发生缺页异常整个内核就会BUG掉。
我曾经在内核态下对用户进程的地址空间直接进行读写,结果就panic了,只有用get_user才行。
只不过是一段汇编,主要还是对目标地址进行读写,不过在代码中添加了修正代码(.fixup),其目的就是当发生异常时执行该修正代码,防止内核被BUG掉。
1.__copy_user
宏__copy_user在include/asm-i386/uaccess.h中定义,是作为从用户空间和内核空间进行内存复制的关键。
这个宏扩展为汇编后如下:
000#define__copy_user(to,from,size)
001do{
002int__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:
lea0(%3,%0,4),%0\n"
010"jmp2b\n"
011".previous\n"
012".section__ex_table,\"a\"\n"
013".align4\n"
014".long0b,3b\n"
015".long1b,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行代码即完成此任务,“lea0(%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异常表的安排在用户空间是不会得到执行的,它只在内核中有效。
(3)将.fixup段和.text段独立开来的目的是为了提高CPU流水线的利用率。
熟悉体系结构的读者应该知道,当前的CPU引入了流水线技术来加快指令的执行,即在执行当前指令的同时,要将下面的一条甚至多条指令预取到流水线中。
这种技术在面对程序执行分支的时候遇到了问题:
如果预取的指令并不是程序下一步要执行的分支,那么流水线中的所有指令都要被排空,这对系统的性能会产生一定的影响。
在我们的这个程序中,如果将.fixup段的指令安排在正常执行的.text段中,当程序执行到前面的指令时,这几条很少执行的指令会被预取到流水线中,正常的执行必然会引起流水线的排空操作,这显然会降低整个系统的性能。
2.在用户程序中使用__copy_user
/*hello.c*/
#include
#include
#define__copy_user(to,from,size)\
do{\
int__d0,__d1;\
__asm____volatile__(\
"0:
rep;movsl\n"\
"movl%3,%0\n"\
"1:
rep;movsb\n"\
"2:
\n"\
".section.fixup,\"ax\"\n"\
"3:
lea0(%3,%0,4),%0\n"\
"jmp2b\n"\
".previous\n"\
".section__ex_table,\"a\"\n"\
".align4\n"\
".long0b,3b\n"\
".long1b,2b\n"\
".previous"\
:
"=&c"(size),"=&D"(__d0),"=&S"(__d1)\
:
"r"(size&3),"0"(size/4),"1"(to),"2"(from)\
:
"memory");\
}while(0)
intmain(void)
{
constchar*string="Hello,world!
";
charbuf[20];
unsignedlongn,m;
m=n=strlen(string);
__copy_user(buf,string,n);
buf[m]='\0';
printf("%s\n",buf);
exit(0);
}
$gcchello.c-ohello
$objdump--disassemble--section=.texthello
hello:
fileformatelf32-i386
Disassemblyofsection.text:
8048498:
8b45c4mov0xffffffc4(%ebp),%eax
804849b:
83e003and$0x3,%eax
804849e:
8b55c4mov0xffffffc4(%ebp),%edx
80484a1:
89d1mov%edx,%ecx
80484a3:
c1e902shr$0x2,%ecx
80484a6:
8d7dc8lea0xffffffc8(%ebp),%edi
80484a9:
8b75f4mov0xfffffff4(%ebp),%esi
80484ac:
f3a5repzmovsl%ds:
(%esi),%es:
(%edi)
80484ae:
89c1mov%eax,%ecx
80484b0:
f3a4repzmovsb%ds:
(%esi),%es:
(%edi)
80484b2:
89c8mov%ecx,%eax
$objdump--disassemble--section=.fixuphello
hello:
fileformatelf32-i386
Disassemblyofsection.fixup:
08048530<.fixup>:
8048530:
8d4c8800lea0x0(%eax,%ecx,4),%ecx
8048534:
e979ffffffjmp80484b2
$objdump--full-contents--section=__ex_tablehello
hello:
fileformatelf32-i386
Contentsofsection__ex_table:
8048578ac84040830850408b0840408b2840408....0...........
由于x86使用小尾端的编址方式,上面的这段数据比较凌乱。
把上面的__ex_table中的内容转变成大家通常看到的样子:
804857880484ac804853080484b080484b2....0...........
(1).long0b,3b对应80484ac8048530,其中80484ac对应的指令为.text中的repzmovsl%ds:
(%esi),%es:
(%edi),8048530对应的指令为.fixup中的lea0x0(%eax,%ecx,4),%ecx。
(2).long1b,2b对应80484b080484b2,其中80484b0对应的指令为.text中的repzmovsb%ds:
(%esi),%es:
(%edi),80484b2对应的指令为.text中的mov%ecx,%eax。
AT&Tx86asm语法
linux下gcc的汇编格式是at&t格式的,和我们平时用的intel格式的汇编语法不一样,所以很多熟悉windows汇编的人到linux下有点无所适从,所以我贴了我以前写的这篇文档,帮助大家理解at&t汇编,做个参考手册
AT&Tx86asm语法
DJGPP使用AT&T格式的汇编语法。
和一般的intel格式的语法有点不同。
主要不同点如下:
AT&T语法颠倒了源和目的操作数的位置,目的操作数在源操作数之后。
寄存器操作数要有个%的前缀,立即数操作数要有个$符号的前缀。
存储器操作数的大小取决于操作码的最后一个字符。
它们是b(8-bit),w(16-bit),和l(32-bit).
这里有一些例子。
左边部分是intel指令格式,右边是at&t格式。
movw%bx,%ax//movax,bx
xorl%eax,%eax//xoreax,eax
movw$1,%ax//movax,1
movbX,%ah//movah,byteptrX
movwX,%ax//movax,wordptrX
movlX,%eax//moveax,X
大部分操作指令,at%t和intel都是差不多的,除了这些:
movsSD//movsx
movzSD//movz
S和D分辨代表源和目的操作数后缀。
movswl%ax,%ecx//movsxecx,ax
cbtw //cbw
cwtl //cwde
cwtd //cwd
cltd //cdq
lcall$S,$O//callfarS:
O
ljmp$S,$O //jumpfarS:
O
lret$V //retfarV
操作嘛前缀不能与他们作用的指令写在同一行。
例如,rep和stosd应该是两个相互独立的指令,存储器
的情况也有一点不同。
通常intel格式的如下:
section:
[base+index*scale+disp]
被写成:
section:
disp(base,index,scale)
这里有些例子:
movl4(%ebp),%eax //moveax,[ebp+4])
addl(%eax,%eax,4),%ecx //addecx,[eax+eax*4])
movb$4,%fs
%eax) //movfs:
eax,4)
movl_array(,%eax,4),%eax //moveax,[4*eax+array])
movw_array(%ebx,%eax,4),%cx//movcx,[ebx+4*eax+array])
Jump指令通常是个短跳转。
可是,下面这些指令都是只能在一个字节的范围内跳转:
jcxz,jecxz,loop,loopz,loope,loopnz和loopne。
象在线文档所说的那样,一个jcxzfoo可以扩展成以下工作:
jcxzcx_zero
jmpcx_nonzero
cx_zero:
jmpfoo
cx_nonzero:
文档也注意到了mul和imul指令。
扩展的乘法指令只用一个操作数,例如,imul$ebx,$ebx将不会把结
果放入edx:
eax。
使用imul%ebx中的单操作数来获得扩展结果。
-------------------------------------------------------------------------------
InlineAsm
我将首先开始inlineasm,因为似乎关于这方面的疑问非常多。
这是最基本的语法了,就象在线帮助信息
中描述的:
__asm__(asmstatements:
outputs:
inputs:
registers-modified);
这四个字段的含义是:
asmstatements-AT&T的结构,每新行都是分开的。
outputs-修饰符一定要用引号引起来,用逗号分隔
inputs-修饰符一定要用引号引起来,用逗号分隔
registers-modified-名字用逗号分隔
一个小小的例子:
__asm__("
pushl%eax\n
movl$1,%eax\n
popl%eax"
);
假如你不用到特别的输入输出变量或者修改任何寄存器的值,一般来说是不会使用到其他的三个字段的,
让我们来分析一下输入变量。
inti=0;
__asm__("
pushl%%eax\n
movl%0,%%eax\n
addl$1,%%eax\n
movl%%eax,%0\n
popl%%eax"
:
:
"g"(i)
);//incrementi
不要为上面的代码所困扰!
我将尽力来解释它。
我们想让输入变量i加1,没有任何输出变量,也没有改变寄存器值(我们保存了eax值)。
因此,第二个和最后一个字段是空的。
因为指定了输入字段,我们仍需要保留一个空的输出字段,但是没有最后一个字段,因为它没被使用。
在两个空冒号之间留下一个新行或者至少一个空格。
下面让我们来看看输入字段。
附加描述符可以修正指令来让你给定的编译器来正确处理这些变量。
他们一般被附上双引号。
那么这个"g"是用来做什么的呢?
只要是合法的汇编指令,"g"就让编译器决定该在哪里加载i的值。
一般来说,你的大部分输入变量都可以被赋予"g",让编译器决定如何去加载它们(gcc甚至可以优化它们!
)。
其他描述符使用"r"(加载到任何可用的寄存器去),"a"(ax/eax),"b"(bx/ebx),"c"(cx/ecx),"d"(dx/edx),"D"(di/edi),"S"(si/esi),等等。
我们将要提到一个在asm代码里面的如%0的输入变量。
如果我们有两个输入,他们会一个是%0一个是%1,在输入段里按顺序排列(如下一个例子)。
假如N个输入变量且没有输出变量,从%0到%N-1将和输入字段里的变量相对应,按顺序排列。
如果任何的输入,输出,寄存器修改字段被使用,汇编代码里的寄存器名必须用两个%来代替一个%。
对应于第一个没有使用最后三个字段的例子。
让我们看看两个输入变量且引入了"volatile"的例子:
inti=0,j=1;
__asm____volatile__("
pushl%%eax\n
movl%0,%%eax\n
addl%1,%%eax\n
movl%%eax,%0\n
popl%%eax"
:
:
"g"(i),"g"(j)
);//incrementibyj
Okay,现在我们已经有了两个输入变量了。
没问题了,我们只需要记住%0对应第一个输入变量(在这个例子中是i),%1对应在i后面的列出的j。
Ohyeah,这个volatile到底是什么意思呢?
它防止你的编译器修改你的汇编代码,就是不进行优化(纪录,删除,结合,等等优化手段。
),不改变代码原样来汇编它们。
建议一般情况下使用volatile选项。
让我们来看看输出字段:
inti=0;
__asm____volatile__("
pushl%%eax\n
movl$1,%%eax\n
movl%%eax,%0\n
popl%%eax"
:
"=g"(i)
);//assign1toi
这看起来非常象我们前面提到的输入字段的例子;确实也没有很大的不同。
所有的输出修饰符前面都应该加上=字符,他们同样在汇编代码里面用%0到%N-1来表示,在输出字段按顺序排列。
你一定会问如果同时有输入和输出字段会怎么排序的呢?
好,下面一个例子就是让大家知道如何同时处理输入输出字段的。
inti=0,j=1,k=0;
__asm____volatile__("
pushl%%eax\n
movl%1,%%eax\n
addl%2,%%eax\n
movl%%eax,%0\n
popl%%eax"
:
"=g"(k)
:
"g"(i),"g"(j)
);//k=i+j
Okay,唯一个不清楚的地方就是汇编代码中的变量的个数。
我马上来解释一下。
当同时使用输入字段和输出字段的时候:
%0...%K是输出变量
%K+1...%N是输入变量
在我们的例子中,%0对应k,%1对应i,%2对应j。
很简单,是吧?
到现在为止我们都没有使用最后一个字段(registers-modified)。
如果我们要在我们的汇编代码里使用
任何寄存器,我们要明确的用push和pop指令来保存它们,或者列到最后一个字段里面让gcc来处理它们。
这是前面的一个例子,没有明确的保留和存贮eax。
inti=0,j=1,k=0;
__asm____volatile__("
pushl%%eax\n /*译者注:
好像原文说的有点问题,明明是保存了eax的值,
*/
movl%1,%%eax\n
addl%2,%%eax\n
movl%%eax,%0\n
popl%%eax"
:
"=g"(k)
:
"g"(i),"g"(j)
:
"ax","memory"
);//k=i
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 内核 汇编