程序的机器级表示
数据传送指令
将数据从一个位置复制到另一个位置的指令是最频繁使用的指令。
算术逻辑指令
控制指令
目的:
条件语句、循环语句和分支语句,要求有条件的进行,根据数据测试的结果来决定操作执行的顺序。机器代码提供两种基本的低级机制来实现有条件的行为:测试数据值,然后根据测试的结果来改变控制流或者数据流
控制指令-条件码
CPU维护着一组单个位的条件码寄存器,他们描述了最近的算术或者逻辑操作的属性。可以检测这些寄存器来执行条件分支指令
最常用的条件码:
CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号操作数的溢出
ZF:零标志。最近的操作得出的结果为0.
SF:符号标志。最经的操作得到的结果为负数。
OF:溢出标志。最近的操作导致一个补码溢出——正溢出或者负溢出
比较和测试指令
有两类指令只设置条件码而不改变任何其他寄存器。
CMP指令根据两个操作数之间的差来设置条件码。
TEST指令的行为同and一样。典型的用法是,testl %eax,%eax用来检查%eax是负数、零还是正数
控制指令
Do-while循环
源代码
do{
body-statement
}while(test-expr)
翻译成汇编的伪代码
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
Do-while循环-举例
真实代码:
int fact_do(int n)
{
int result = 1;
do
{
result *= n;
n = n-1;
}while(n > 1);
return result;
}
汇编代码:
_fact_do:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//读取n
movl $1, %eax//写入1
L5:
imull %edx, %eax// result *= n
decl %edx// n = n-1
cmpl $1, %edx//判断n大于1
jg L5//是的话继续
popl %ebp
ret
While循环
源代码
while(test-expr)
body-statement
翻译成汇编的伪代码
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
done:
While循环-举例
源代码:
int fact_while(int n)
{
int val = 0;
while(x)
{
val ^= x;
x >>=1;
}
return val & 0x1;
}
汇编代码:
_fact_while:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//读取n
movl $0, %eax//val = 0
testl %edx, %edx//x = 0?
je L25//是的话,return
L23:
xorl %edx, %eax
shrl %edx
jne L23//循环
L25:
andl $1, %eax
popl %ebp
ret
For循环
源代码:
for(init-expr;
test-expr;
update-expr)
body-statement;
翻译成汇编的伪代码:
init-expr;
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
update-expr;
t = test-expr;
if(t)
goto loop;
done:
For循环-举例
源代码:
int sum(int x)
{
int sum = 0;
int i;
for(i = 0; i< x; i++)
{
if(i & 1)
continue;
sum += i;
}
return sum;
}
汇编代码:
_sum:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx//读取x
movl $0, %eax//sum=0
movl $0, %edx//i = 0
cmpl %ecx, %eax//判断 0>= x?
jge L33//如果成立,结束
L31:
testb $1, %dl//测试 1 and i
jne L29//如果不为0,跳转L29
addl %edx, %eax//否则sum +=i
L29:
incl %edx//i++
cmpl %ecx, %edx//判断 i < x?
jl L31//如果成立,继续循环
L33:
popl %ebp
ret
Switch语句
int swichtest(int a, int b, int c)
{
int answer;
switch(a)
{
case 5:
c = b^15;
case 0:
answer = c +115;
break;
case 2:
case 7:
answer = (c + b) << 2;
break;
case 4:
answer = a;
break;
default:
answer = b;
}
return answer;
}
int swichtest_impl(int a, int b, int c)
{
static void *jt[8] = {
&&loc_A, &&loc_def, &&loc_B, &&loc_def,
&&loc_C , &&loc_D, &&loc_def , &&loc_B
};
int answer;
if(a > 7)
goto loc_def;
goto *jt[a];
loc_def :
answer = b;
goto done;
loc_D :
c = b^15;
loc_A :
answer = c +115;
goto done;
loc_B :
answer = (c + b) << 2;
goto done;
loc_C :
answer = a;
goto done;
done:
return answer;
}
_swichtest:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//读取a
movl 12(%ebp), %ecx//读取b
movl 16(%ebp), %eax//读取c
cmpl $7, %edx//如果 a大于 7,default
ja L35//跳转default
jmp *L36(,%edx,4)//按照数组跳转
L30: //case 5
movl %ecx, %eax
xorl $15, %eax//顺延到下一个条件
L31: //case 0
addl $115, %eax
jmp L29//返回
L33: //case 2,7
addl %ecx, %eax
sall $2, %eax
jmp L29//返回
L34: //case 4
movl %edx, %eax
jmp L29//返回
L35: //default
movl %ecx, %eax
L29:
popl %ebp
ret
L36:
.long L31
.long L35
.long L33
.long L35
.long L34
.long L30
.long L35
.long L33
.text
If-else语句
模板
if(test-expr)
then-statement
else
else-statement
一般情况
if(!test-expr)
goto false;
v = then-expr;
goto done;
false:
v = else-expr;
done:
使用条件传送语句的格式
vt = then-expr;
v = else-expr;
t = test-expr;
if(t) v = vt;
If-else-一般情况举例
int absdiff(int x, int y)
{
if(x < y)
return y-x;
else
return x-y;
}
_absdiff:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//读取x
movl 12(%ebp), %eax//读取y
cmpl %eax, %edx//判断x>=y?
jge L2//是的话,跳转
subl %edx, %eax//y - x
jmp L3
L2:
subl %eax, %edx//x - y
movl %edx, %eax//将结果存到结果寄存器
L3:
popl %ebp
ret
If-else-使用条件传送语句
int absdiff(int x, int y)
{
if(x < y)
return y-x;
else
return x-y;
}
_absdiff:
pushl %ebp
movl %esp, %ebp
pushl %ebx
movl 8(%ebp), %ecx //读取x
movl 12(%ebp), %edx //读取y
movl %edx, %ebx
subl %ecx, %ebx //y - x
movl %ecx, %eax //
subl %edx, %eax //x - y
cmpl %edx, %ecx //判断x < y
cmovl %ebx, %eax//是的话返回y - x
popl %ebx
popl %ebp
ret
编译器在什么情况下选择什么方式
在分支计算量小的情况下,基于条件数据传送的代码比基于条件控制转移的代码性能要好。
这本书后面会介绍到处理器通过使用流水线来获得高性能,在碰到条件控制转移的时候,会采用精密的分支预测逻辑来猜测跳转指令的执行与否,但是有时候这个猜测是不可靠的,从而浪费了程序执行的时间。但是使用条件数据传送的方式只需要检查条件码,要么更新目的寄存器,要么保持不变。
编译器会根据分支预测所需要的时间和条件传送浪费的计算上进行权衡,编译成哪种逻辑才是合适的。
If-else-例外
int cread(int *xp)
{
return (xp? *xp : 0);
}
_cread:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl $0, %eax
testl %edx, %edx
je L9
movl (%edx), %eax
L9:
popl %ebp
ret
我们看到编译器依然选择了使用分支跳转的方式进行编译的。原因是即使当xp测试为空的时候,cmovne对xp的间接引用还是发生了,导致一个间接引用空指针的错误
.net程序集加载流程
对于一个已编译好的.NET程序集,Windows操作系统是如何启动执行的呢?日常使用中我们发现对于托管的和非托管的程序集编译器都会吧程序集编译成以.exe或.dll等为扩展名的文件,可见Windows加载器并没有区分是托管还是非托管的程序集,而且我们也知道对非托管的程序集是在编译器直接编译成了机器码,自然可以由CPU直接执行,而托管的.NET 程序集是包含复杂结构的MSIL代码,执行时会使用JIT即时编译器将IL代码编译成机器码,再由CPU执行,当然这期间还需要执行其它许多的工作,如加载CLR、执行初始化等工作,那么这些是怎么自动实现的呢?
首先我们要清楚的是对于托管还是非托管程序集,他们在编译器执行编译时都会编译成一个特殊的文件格式,即PE文件(可移植可执行文件格式),操作系统加载器通过加载这样的PE文件来执行程序集的。可以这么说吧,无论是托管程序还是非托管程序他们实际上都是编译成这样的PE文件(只是有部分内容不一样而已)。
然后这个PE文件会指示如何执行托管程序集和非托管程序集,加载器首先会查找到PE头中的AddressOfEntryPoint域,这个域指示PE文件的入口点位置,在.NET程序集中是指向.text段中的CLR头--〉包含一个结构IMAGE_COR20_HEADER—包含许多信息如托管代码应用程序的入口点,目标CLR的主版本号和从版本号,以及程序集的强名称签名等--〉Windows加载器根据这个数据结构决定加载哪个版本的CLR以及一些基本的程序集信息。在.text段中还包含了程序集的元数据表,MSIL以及非托管启动存根代码,而非托管启动存根代码包好了由Windows加载器执行役启动PE文件执行的代码,结构如图所示。
1、用户执行一个.NET程序集;
2、Windows加载器查看AddressOfEntryPoint域,并找到PE映像文件的.text段;
3、位于AddressOfEntryPoint位置上的字节只是一个JMP(跳转)指令,这个指令跳转到mscoree.dll中的一个导入函数;
4、将执行控制转移到mscoree.dll中的_CorExeMain中,这个函数将启动CLR并把执行控制转移到程序集的入口点。
程序的机器级表示
数据传送指令
将数据从一个位置复制到另一个位置的指令是最频繁使用的指令。
算术逻辑指令
控制指令
目的:
条件语句、循环语句和分支语句,要求有条件的进行,根据数据测试的结果来决定操作执行的顺序。机器代码提供两种基本的低级机制来实现有条件的行为:测试数据值,然后根据测试的结果来改变控制流或者数据流
控制指令-条件码
CPU维护着一组单个位的条件码寄存器,他们描述了最近的算术或者逻辑操作的属性。可以检测这些寄存器来执行条件分支指令
最常用的条件码:
CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号操作数的溢出
ZF:零标志。最近的操作得出的结果为0.
SF:符号标志。最经的操作得到的结果为负数。
OF:溢出标志。最近的操作导致一个补码溢出——正溢出或者负溢出
比较和测试指令
有两类指令只设置条件码而不改变任何其他寄存器。
CMP指令根据两个操作数之间的差来设置条件码。
TEST指令的行为同and一样。典型的用法是,testl %eax,%eax用来检查%eax是负数、零还是正数
控制指令
Do-while循环
源代码
do{
body-statement
}while(test-expr)
翻译成汇编的伪代码
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
Do-while循环-举例
真实代码:
int fact_do(int n)
{
int result = 1;
do
{
result *= n;
n = n-1;
}while(n > 1);
return result;
}
汇编代码:
_fact_do:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//读取n
movl $1, %eax//写入1
L5:
imull %edx, %eax// result *= n
decl %edx// n = n-1
cmpl $1, %edx//判断n大于1
jg L5//是的话继续
popl %ebp
ret
While循环
源代码
while(test-expr)
body-statement
翻译成汇编的伪代码
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
done:
While循环-举例
源代码:
int fact_while(int n)
{
int val = 0;
while(x)
{
val ^= x;
x >>=1;
}
return val & 0x1;
}
汇编代码:
_fact_while:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//读取n
movl $0, %eax//val = 0
testl %edx, %edx//x = 0?
je L25//是的话,return
L23:
xorl %edx, %eax
shrl %edx
jne L23//循环
L25:
andl $1, %eax
popl %ebp
ret
For循环
源代码:
for(init-expr;
test-expr;
update-expr)
body-statement;
翻译成汇编的伪代码:
init-expr;
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
update-expr;
t = test-expr;
if(t)
goto loop;
done:
For循环-举例
源代码:
int sum(int x)
{
int sum = 0;
int i;
for(i = 0; i< x; i++)
{
if(i & 1)
continue;
sum += i;
}
return sum;
}
汇编代码:
_sum:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx//读取x
movl $0, %eax//sum=0
movl $0, %edx//i = 0
cmpl %ecx, %eax//判断 0>= x?
jge L33//如果成立,结束
L31:
testb $1, %dl//测试 1 and i
jne L29//如果不为0,跳转L29
addl %edx, %eax//否则sum +=i
L29:
incl %edx//i++
cmpl %ecx, %edx//判断 i < x?
jl L31//如果成立,继续循环
L33:
popl %ebp
ret
Switch语句
int swichtest(int a, int b, int c)
{
int answer;
switch(a)
{
case 5:
c = b^15;
case 0:
answer = c +115;
break;
case 2:
case 7:
answer = (c + b) << 2;
break;
case 4:
answer = a;
break;
default:
answer = b;
}
return answer;
}
int swichtest_impl(int a, int b, int c)
{
static void *jt[8] = {
&&loc_A, &&loc_def, &&loc_B, &&loc_def,
&&loc_C , &&loc_D, &&loc_def , &&loc_B
};
int answer;
if(a > 7)
goto loc_def;
goto *jt[a];
loc_def :
answer = b;
goto done;
loc_D :
c = b^15;
loc_A :
answer = c +115;
goto done;
loc_B :
answer = (c + b) << 2;
goto done;
loc_C :
answer = a;
goto done;
done:
return answer;
}
_swichtest:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//读取a
movl 12(%ebp), %ecx//读取b
movl 16(%ebp), %eax//读取c
cmpl $7, %edx//如果 a大于 7,default
ja L35//跳转default
jmp *L36(,%edx,4)//按照数组跳转
L30: //case 5
movl %ecx, %eax
xorl $15, %eax//顺延到下一个条件
L31: //case 0
addl $115, %eax
jmp L29//返回
L33: //case 2,7
addl %ecx, %eax
sall $2, %eax
jmp L29//返回
L34: //case 4
movl %edx, %eax
jmp L29//返回
L35: //default
movl %ecx, %eax
L29:
popl %ebp
ret
L36:
.long L31
.long L35
.long L33
.long L35
.long L34
.long L30
.long L35
.long L33
.text
If-else语句
模板
if(test-expr)
then-statement
else
else-statement
一般情况
if(!test-expr)
goto false;
v = then-expr;
goto done;
false:
v = else-expr;
done:
使用条件传送语句的格式
vt = then-expr;
v = else-expr;
t = test-expr;
if(t) v = vt;
If-else-一般情况举例
int absdiff(int x, int y)
{
if(x < y)
return y-x;
else
return x-y;
}
_absdiff:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx//读取x
movl 12(%ebp), %eax//读取y
cmpl %eax, %edx//判断x>=y?
jge L2//是的话,跳转
subl %edx, %eax//y - x
jmp L3
L2:
subl %eax, %edx//x - y
movl %edx, %eax//将结果存到结果寄存器
L3:
popl %ebp
ret
If-else-使用条件传送语句
int absdiff(int x, int y)
{
if(x < y)
return y-x;
else
return x-y;
}
_absdiff:
pushl %ebp
movl %esp, %ebp
pushl %ebx
movl 8(%ebp), %ecx //读取x
movl 12(%ebp), %edx //读取y
movl %edx, %ebx
subl %ecx, %ebx //y - x
movl %ecx, %eax //
subl %edx, %eax //x - y
cmpl %edx, %ecx //判断x < y
cmovl %ebx, %eax//是的话返回y - x
popl %ebx
popl %ebp
ret
编译器在什么情况下选择什么方式
在分支计算量小的情况下,基于条件数据传送的代码比基于条件控制转移的代码性能要好。
这本书后面会介绍到处理器通过使用流水线来获得高性能,在碰到条件控制转移的时候,会采用精密的分支预测逻辑来猜测跳转指令的执行与否,但是有时候这个猜测是不可靠的,从而浪费了程序执行的时间。但是使用条件数据传送的方式只需要检查条件码,要么更新目的寄存器,要么保持不变。
编译器会根据分支预测所需要的时间和条件传送浪费的计算上进行权衡,编译成哪种逻辑才是合适的。
If-else-例外
int cread(int *xp)
{
return (xp? *xp : 0);
}
_cread:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl $0, %eax
testl %edx, %edx
je L9
movl (%edx), %eax
L9:
popl %ebp
ret
我们看到编译器依然选择了使用分支跳转的方式进行编译的。原因是即使当xp测试为空的时候,cmovne对xp的间接引用还是发生了,导致一个间接引用空指针的错误
.net程序集加载流程
对于一个已编译好的.NET程序集,Windows操作系统是如何启动执行的呢?日常使用中我们发现对于托管的和非托管的程序集编译器都会吧程序集编译成以.exe或.dll等为扩展名的文件,可见Windows加载器并没有区分是托管还是非托管的程序集,而且我们也知道对非托管的程序集是在编译器直接编译成了机器码,自然可以由CPU直接执行,而托管的.NET 程序集是包含复杂结构的MSIL代码,执行时会使用JIT即时编译器将IL代码编译成机器码,再由CPU执行,当然这期间还需要执行其它许多的工作,如加载CLR、执行初始化等工作,那么这些是怎么自动实现的呢?
首先我们要清楚的是对于托管还是非托管程序集,他们在编译器执行编译时都会编译成一个特殊的文件格式,即PE文件(可移植可执行文件格式),操作系统加载器通过加载这样的PE文件来执行程序集的。可以这么说吧,无论是托管程序还是非托管程序他们实际上都是编译成这样的PE文件(只是有部分内容不一样而已)。
然后这个PE文件会指示如何执行托管程序集和非托管程序集,加载器首先会查找到PE头中的AddressOfEntryPoint域,这个域指示PE文件的入口点位置,在.NET程序集中是指向.text段中的CLR头--〉包含一个结构IMAGE_COR20_HEADER—包含许多信息如托管代码应用程序的入口点,目标CLR的主版本号和从版本号,以及程序集的强名称签名等--〉Windows加载器根据这个数据结构决定加载哪个版本的CLR以及一些基本的程序集信息。在.text段中还包含了程序集的元数据表,MSIL以及非托管启动存根代码,而非托管启动存根代码包好了由Windows加载器执行役启动PE文件执行的代码,结构如图所示。
1、用户执行一个.NET程序集;
2、Windows加载器查看AddressOfEntryPoint域,并找到PE映像文件的.text段;
3、位于AddressOfEntryPoint位置上的字节只是一个JMP(跳转)指令,这个指令跳转到mscoree.dll中的一个导入函数;
4、将执行控制转移到mscoree.dll中的_CorExeMain中,这个函数将启动CLR并把执行控制转移到程序集的入口点。