1. 为什么整型可以赋给char型?
- 因为字符说到底还是用一个无符号整型来表示的,具体每个字符用什么值来表示请见ascii码表。。
former=0就相当于把ascii码表里用0表示的字符赋值给了former,你既可以把这个字符以char型打印出来,也可以把它当无符号整数0来操作。在c语言里,甚至可以对char型加加减减。 - 字符型本就是以ASCII存的,就是整型
- 系统自动进行了隐式转换,但int——>char可能造成数据丢失
eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。
比方说:add eax,-2 ; //可以认为是给变量eax加上-2这样的一个值。
这些32位寄存器有多种用途,但每一个都有“专长”,有各自的特别之处。
EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
EDX 则总是被用来放整数除法产生的余数。
ESI/EDI分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
EBP是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer). 在破解的时候,经常可以看见一个标准的函数起始代码:
push ebp ;保存当前ebp
mov ebp,esp ;EBP设为当前堆栈指针
sub esp, xxx ;预留xxx字节给函数临时变量.
...
这样一来,EBP 构成了该函数的一个框架, 在EBP上方分别是原来的EBP, 返回地址和参数. EBP下方则是临时变量. 函数返回时作 mov esp,ebp/pop ebp/ret 即可.
ESP 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。
386部分寄存器:
mov
传送指令MOV
1.立即数送寄存器或主存 MOV reg/mem , im
mov al , 4 ; al←4,字节传送
mov cx , 0ffh ; cx←00ffh,字传送
mov si , 200h ; si←0200h,字传送
mov byte ptr [si] , 0ah ; byte ptr 说明是字节操作
mov word ptr [si+2 ], 0bh ; word ptr 说明是字操作
# 注意立即数是字节量还是字量
# 明确指令是字节操作还是字操作
2.寄存器送(段)寄存器或主存 MOV reg/mem/seg , reg;
mov ax , bx ; ax←bx,字传送
mov ah , al ; ah←al,字节传送
mov ds , ax ; ds←ax,字传送
mov [bx] , al ; [bx]←al,字节传送
3.主存送(段)寄存器 MOV reg/seg , mem;
mov al , [bx]
mov dx , [bp] ; dx←ss:[bp]
mov es , [si] ; es←ds:[si]
# 不存在存储器向存储器的传送指令
4.段寄存器送寄存器或主存 MOV reg/mem , seg;
mov si , ds
mov ax , es ; ax←es
mov ds , ax ; ds←ax←es
# 对段寄存器的操作有一些限制MOV指令传送功能
push/pop
堆栈是存储器中专用的缓冲区,用于暂存寄存器数据或地址指针,push/pop就用于堆栈的操作,这两个指令一般用在:
1、子程序调用,为了保护现场,把所要用的寄存器中的内容先暂时保存起来,在子程序调用结束之前,按照先进后出的原则,把数据恢复。
2、有时候,需要临时用一下某些寄存器,也可用一下,凭个人喜好;
这两个指令必须成对使用(特殊用途除外),你只要压入了那些东西,并且知道他们的顺序就行了,所操作的是字符还是数据,就不用知道了。
堆栈操作指令
堆栈是一个先进后出的主存区域,位于堆栈段中,使用SS段寄存器记录器段地址。栈只有一个出口,即当前栈顶。栈顶是地址较小的一端(低端),它用堆栈指针寄存器SP指定。堆栈的两种基本操作,对应两条基本指令:
(1)、进栈指令push
push reg/mem/seg;sp<-sp-2,ss<-reg/mem/seg
进栈指令先使堆栈指令sp减2,然后把一个字操作数存入堆栈顶部。堆栈操作的对象只能是字操作数,进栈时底字节存放于低地址,高字节存放于高地址,sp相应向低地址移动两个字节单元。
push AX
PUSH [2000H]
PUSH CS
(2)、出栈指令pop
pop reg/seg/mem;reg/seg/mem<-ss:[sp],sp<-sp+2
出栈指令把栈顶的一个字传送至指定的目的操作数,然后堆栈指针sp加2。目的操作数应为字操作数,字从栈顶弹出时,低地址字节送低字节,高地址字节送高字节。
pop AX
POP [2000H]
POP SS堆栈可以用来临时存放数据,以便随时恢复它们。也常用于子程序见传递参数。
注意几点:
(1)、因为堆栈指针sp总是指向已经存入数据的栈顶(不是空单元),所以PUSH指令是将(SP)减2,后将内容压栈(即先修改SP是指指向空单元,后压入数据),而POP是先从栈顶弹出一个字,后将堆栈指针SP加2.
(2)、PUSH CS是合法的,但是POP CS是不合法的。
(3)、因为SP总是指向栈顶,而用PUSH和POP指令存取数时都是在栈顶进行的,所以堆栈是先进后出或叫后进先出的。栈底在高地址,堆栈是从高地址向低地址延伸的,所有栈底就是最初的栈顶。
(4)、用PUSH指令和POP指令时只能按字访问堆栈,不能按字节访问堆栈。
(5)、PUSH和POP指令都不影响标志
jmp
转移指令的原理(jmp)
1.可以修改IP,或同时修改CS和IP的指令统称转移指令。概括地讲,转移指令就是可以控制CPU执行内存中某处代码的指令。
2.8086CPU的转移行为有以下几种:
(1)只修改IP时,称为段内转移。例如:jmp ax
(2)同时修改CS和IP时,成为段间转移。例如:jmp 1000:0
3.由于转移指令对IP值修改范围的不同,段内转移又分为:短转移和近转移。
(1)短转移IP的修改范围是-128~127
(2)近转移IP的修改范围是-32768~32767
4.8086CPU的转移指令分为以下几类:
(1)无条件转移指令(如:jmp)
(2)条件转移指令
(3)循环指令(如:Loop)
(4)过程
(5)中断
这些转移指令的前提条件可能不同,但转移的基本原理是相同的。
5.操作符offset是一个伪指令,它的功能是取得标号的偏移地址。
案例:将s处的一条指令复制到s0处
assume cs:codesg
codesg segment
s: mov ax,bx ;mov ax,bx占两个字节
mov si,offset s ;得到标号s所在的偏移地址赋值给si
mov di,offset s0 ;得到标号s0所在的偏移地址赋值给di
mov ax,cs:[si]
mov cs:[di],ax
s0: nop ;nop占一个字节
nop
codesg ends
end
6.jmp为无条件转移,可以只修改IP,也可以同时修改CS和IP的值
jmp指令要给出两个信息:
(1)转移的目的地址。
(2)转移的距离(段间转移,段内转移的短转移和近转移)
7.jmp short 标号(转到标号处执行)
这种指令实现的是段内短转移。jmp指令中的"标号"是代码段中的标号,指明了指令要转移的目的地,转移指令结束,CS:IP指向标号处的指令。
8.CPU在执行jmp指令时并不需要指明转移的目标地址。
9.在“jmp short 标号“指令所对应的机器码中,并不包含转移的目的地址,而包含的是转移的位移。
10."jmp short 标号" 的功能是:(IP)=(IP)+8位位移
(1)8位位移=标号处的地址-jmp指令后的第一个字节的地址。
(2)short 指明此处的位移是8位
(3)8位位移的的范围为-128~127,用补码表示。
(4)8位位移由编译程序编译时算出。
11.jmp near ptr 标号 表示段内近转移。
12."jmp near ptr 标号"的功能是:(IP)=(IP)+16位位移
(1)16位位移=标号处的地址-jmp指令后的第一个字节的地址。
(2)near ptr 指明此处的位移是16位,进行段内近转移。
(3)16位位移范围是-32768~32767,用补码表示。
(4)16位位移由编译程序在编译时算出。
13."jmp far ptr 标号"实现的是段间转移,又称远转移。功能如下:
(CS)=标号所在段的段地址;(IP)=标号所在段的偏移地址。
far ptr 指明了指令用标号的段地址和偏移地址修改CS和IP。
14.转移地址在内存中的jmp指令有以下几种:
(1)jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址开始处存放着一个字,是转移的目的的偏移地址。
内存单元地址可以用寻址方式的任意格式给出。
比如,下列指令:
mov ax,0013h
mov ds:[0],ax
jmp word ptr ds:[0]
(2)jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址开始处放着两个字,高地址处放着转移的目的段地址,低地址处放着转移的目的地址的偏移地址。
(CS)=(内存单元地址+2)
(IP)=(内存单元地址)
内存单元地址可以用寻址方式的任意格式给出。
比如,下列指令:
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]
执行后,(CS)=0,(IP)=0123H,CS:IP指向0000:0123
15.jcxz指令为有条件转移,所有有条件转移指令都是短转移。
指令格式:jcxz 标号(如果(cx)=0,转移到标号出执行)
当cx<>0时,什么也不做(程序下下执行)
16.loop指令是循环指令,所有的循环指令都是短转移。
指令格式:loop 标号((cx)=(cs)-1,如果(cx)<>0,转移到标号出执行)
当cx=0时,什么也不做(程序下下执行)
17.根据位移进行转移是为了方便程序段在内存中的浮动装配
18.根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候编译器将报错。
call/ret
1.call和ret指令都是转移指令,它们都修改IP的值,或同时修改CS和IP的值。它们经常共同用语实现子程序的设计。
2.ret指令用栈中的数据,修改IP的内容,从而实现近转移。
3.retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
4.CPU执行ret指令时,相当于进行:
pop IP
执行retf指令时,相当于进行:
pop IP
pop CS
5.CPU执行call指令时,进行两步操作:
(1)将当前的IP或CS和IP压入栈中;
(2)转移
6.call指令不能实现短转移,call指令实现转移的方法和jmp指令的原理相同。
7.call 标号(将当前的IP压栈后,转到标号处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:
(1)(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(2)(IP)=(IP)+16位位移
call执行"call标号"时,相当于进行:
push IP
jmp near ptr 标号
8."call far ptr 标号"实现的是段间转移.
CPU执行此种格式的call指令时,进行如下的操作:
(1)(sp)=(sp)-2
((ss)*16+(sp))=(CS)
(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(2)(cs)=标号所在段的段地址
(IP)=标号在段中的偏移地址
CPU执行"call far ptr 标号"时,相当于进行:
push CS
push IP
jmp far ptr 标号
9.指令格式:call 16位 reg
功能:
(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(IP)=(16位 reg)
CPU执行"call 16位 reg"时,相当于:
push IP
jmp 16 位 reg
10.转移地址在内存中的call指令有两种格式.
(1)call word ptr 内存单元地址
CPU执行"cakk word ptr 内存单元地址"时,相当于:
push IP
jmp word ptr 内存单元地址
(2)call dword ptr 内存单元地址
CPU执行"calldword ptr 内存单元地址"时,相当于:
push CS
push IP
jmp dword ptr 内存单元地址
11.利用call和ret可以实现子程序的机制,框架如下:
标号:
指令
ret
12.mul是乘法指令,使用mul做乘法的时候需注意一下两点.
(1)两个相乘的数:两个相乘的数,要么都是8位,要么都是16位.如果是8位,默认放在AL中,另一个放在8位的reg或内存字节单元中;如果是16位,一个默认放在AX中,另一个放在16位reg过内存字单元中.
(2)结果:如果是8位乘法,默认放在AX中;如果是16位乘法,结果高位放在DX中,低位放在AX中.
格式如下:
mul reg
mul 内存单元
例如:
(1)计算100*10
mov al,100
mov bl,10
mul bl
结果:(AX)=1000(03E8H)
(2)计算100*10000
mov ax,100
mov bx,10000
mul bx
结果:(AX)=4240H,(DX)=000FH (F4240H=1000000)
13.call和ret指令共同支持了汇编语言编程中的模块化设计.
14.用寄存器来存储参数和结果是最常用的方法.
15.对于批量数据的传递,我们将它放在内存中,然后将它们所在内存控件的首地址放在寄存器中,传递给需要的子程序.(联想:C的指针和数组关系)
16.当出现寄存器冲突时(如多次使用CX),解决方法是在子程序的开始将子程序中所有用到的寄存器中的值都保存起来,在子程序返回前再恢复(栈的使用)
- kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
- kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,malloc不保证任何东西(猜测的,不一定正确)
- kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大
- 内存只有在要被DMA访问的时候才需要物理上连续
- vmalloc比kmalloc要慢
kmalloc()是内核中最常见的内存分配方式,它最终调用伙伴系统的__get_free_pages()函数分配,根据传递给这个函数的flags参数,决定这个函数的分配适合什么场合,如果标志是GFP_KERNEL则仅仅可以用于进程上下文中,如果标志GFP_ATOMIC则可以用于中断上下文或者持有锁的代码段中。
kmalloc返回的线形地址是直接映射的,而且用连续物理页满足分配请求,且内置了最大请求数(2**5=32页)。
vmalloc优先使用高端物理内存,但性能上会打些折扣。
vmalloc分配的物理页不会被交换出去;
vmalloc返回的虚地址大于(PAGE_OFFSET + SIZEOF(phys memory) + GAP),为VMALLOC_START----VMALLOC_END之间的线形地址;
vmalloc使用的是vmlist链表,与管理用户进程的vm_area_struct要区别,而后者会swapped;kmap()是主要用在高端存储器页框的内核映射中,一般是这么使用的:
使用alloc_pages()在高端存储器区得到struct page结构,然后调用kmap(struct *page)在内核地址空间PAGE_OFFSET+896M之后的地址空间中(PKMAP_BASE到FIXADDR_STAR)建立永久映射(如果page结构对应的是低端物理内存的页,该函数仅仅返回该页对应的虚拟地址)
kmap()也可能引起睡眠,所以不能用在中断和持有锁的代码中不过kmap 只能对一个物理页进行分配,所以尽量少用。
对于高端物理内存(896M之后),并没有和内核地址空间建立一一对应的关系(即虚拟地址=物理地址+PAGE_OFFSET这样的关系),所以不能使用get_free_pages()这样的页分配器进行内存的分配,而必须使用alloc_pages()这样的伙伴系统算法的接口得到struct *page结构,然后将其映射到内核地址空间,注意这个时候映射后的地址并非和物理地址相差PAGE_OFFSET.
kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系。而vmalloc申请的内存则位于vmalloc_start~vmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。
X86是一个对所有*86系统的简单的通配符定义,是一个intel通用计算机系列的编号,也标识一套通用的计算机指令集合。由于早期intel的CPU编号都是如8086,80286来编号,由于这整个系列的CPU都是指令兼容的,所以都用X86来标识所使用的指令集合。如今的英特尔的处理器,都是支持X86指令系统的,所以都属于X86家族。
8086是16位处理器;直到1985年32位的80386的开发,这个架构都维持是16位。接着一系列的处理器表示了32位架构的细微改进,推出了数种的扩充,直到2003年AMD对于这个架构发展了64位的扩充,并命名为AMD64。后来Intel也推出了与之兼容的处理器,并命名为Intel 64。两者一般被统称为x86-64或x64,开创了x86的64位时代。
32位的X86CPU,支持最大4GB内存,但通过其它技术其实可以扩展这个容量,像微软的服务器系统win2003就可以使用超过4G的内存;而目前64位的x86的CPU,可以理论上上动手动脚16EB内存,微软的64位系统可以支持到192GB。
字符与字节它们完全不是一个位面的概念,所以两者之间没有“区别”这一说法。在不同编码里,字符和字节的对应关系是不同的。一般来说,半角英文状态下一个字母或数字(称之为字符)占用一个字节,一个汉字用两个字节表示。在不同的编码方式下一个字符占的字节数是不同的,所以两者是不能划等号的。
1个字节(Byte)等于8个bit位,每个bit位是0/1两种状态,也就是说一个字节可以表示256个状态,计算机里用字节来作为最基本的存储单位。
字符,在计算机和电信技术中,一个字符是一个单位的字形、类字形单位或符号的基本信息。
字符是指计算机中使用的字母、数字、字和符号,包括:1、2、3、A、B、C、~!·#¥%……—*()——+等等。
①ASCII码中,一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数,换算为十进制。最小值0,最大值255。
②UTF-8编码中,一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。
③Unicode编码中,一个英文等于两个字节,一个中文(含繁体)等于两个字节。
文本符号:英文标点占一个字节,中文标点占两个字节。举例:英文句号“.”占1个字节的大小,中文句号“。”占2个字节的大小。
④UTF-16编码中,一个英文字母字符或一个汉字字符存储都需要2个字节(Unicode扩展区的一些汉字存储需要4个字节)。
⑤UTF-32编码中,世界上任何字符的存储都需要4个字节。
10. 计算机里为什么以补码的形式存储?
引子
你知道计算机中以什么形式存储整数吗?是符号位加值位吗?值位是按照正常的二进制方式存储吗?
如果后两个问题你都回答是,那就意味着当用3位二进制进行存储、且符号位0表示正1表示负时,1会存储成001
,-1会存储成101
。可惜事实不是这样,计算机中是用补码的形式而不是刚刚那种看上去很自然的形式存储整数,补码虽然也是用符号位加值位来表示,但表示的规则不太一样:1会存成001
,-1会存成111
。
如果三个问题你都回答对了,你知道计算机中整数以补码的形式存储,但你知道为什么要用这种形式吗?以及「正数的补码等于原码;负数的补码等于反码加1,而反码等于原码符号位不变,其余各位取反」这样的补码到底意味着什么?(假设你不知道,请接着往下看吧 XD)
先看使用补码的目的,然后忘掉上面那个补码定义,跟我从这个目的开始,一步步探索补码的本质。
目的:为了简化计算机基本运算电路,使加减法都只需要通过加法电路实现,也就是让减去一个正数或加上一个负数这样的运算可以用加上一个正数来代替。于是改变负数存储的形式,存储成一种可以直接当成正数来相加的形式,这种形式就是补码。(正数不用变,所以接下来的讨论中一般略去正数)
补码是怎么把减法变成加法的?
用时钟理解减法变加法
这是一个身边的例子,当你校对时钟的时候,假设发现钟是6点,但实际上现在才2点,也就是它走快了4个小时,你可以有两种方法进行校正,一种是逆时针拨回4个小时到2点,另一种是顺时针拨6个小时到12点然后再拨2小时,也就是顺时针拨8个小时到2点。所以对于时钟的表盘来说,设-N
表示逆时针拨N个小时,N
表示顺时针拨动N个小时,那么-4 = +8
,同样还会有 -1 = +11
、-5 = +7
,甚至也可以 -4 = +8 = +20 = +32 = -16
…
这里边隐藏了什么规律?其实在数学中,-4、+8、+20、+32、-16
可以归为符合某个条件的同一类数字 —— 对于模12同余。
中文维基上对于模和同余的定义是:两个整数a、b,若它们除以正整数m所得的余数相等,则称a、b对于模m同余。
而在一个可溢出计数系统中,把计数系统容量作为模,那么所有对此模同余的数在此计数系统中都会有同样的表示,而且运算等价。
比如上面例子中的时钟表盘就是一个可溢出计数系统,模为12,所以-4、+8、+20、+32、-16
这些对模12同余的数在时钟表盘上的表示是一样的,而且对时针做这些操作的结果也是一样的,都会拨到同样的位置。
一个n位二进制构成的计数系统,因为会舍弃溢出的高位,所以也是一个可溢出的计数系统,它的模为2n2n 。(从0数到2n−12n−1,再多就溢出)
由此可以推理,在一个3位二进制构成的模为8的计数系统中,-2,-10,6,14
可以用同样的二进制数来表示,同时减10和加14会得到一样的结果。
引出 补码
所以,只要 补码 是一个负数的正同余数,那么就能实现加这个正同余补码跟加另一个负数是一样结果的效果。对一个负数来说,有无数个正同余数满足条件,为了减少不必要的运算,可以规定补码就取其中最小的正数。
可能因为通过原码求 补码 是一个补模运算,所以它被称为 补码 。
注意,这里的 补码 都被我用特殊标识,因为这还不是计算机里真正的存储的补码形式,它应该叫补数,不过相信我,已经很接近了
完善 补码
这种 补码 表示还有点问题
通过转换成 补码 ,减一个数确实变成加一个数了,看似很不错,但却有一个明显的问题,那就是数本身的符号丢失了。
比如3位二进制,正常表示0~7
,使用补码法它能代替-8~-1
的运算,但它不能真正表示-8~-1
,因为你不知道它到底是正数还是负数。
我们把负数转换成了一种在运算中更让计算机喜欢的形式,但它却丢失了自己本身作为数的信息。
怎么解决这个问题,可能有人很快就拍脑袋:那就加一位来表示正负得了。但这样的话运算时怎么办,从第二位开始算么?那进位去位的时候是不是也需要特别注意一下不要影响到符号位?你会发现这个问题并不是那么简单。
怎么完善 补码
不知道大牛是怎么想到的,问题解决得非常完美:
- 在保持补码特性的前提下 (也就是减一个数还是照样变成加一个数)
- 增加正负的表示 (能真正表示
-8~-1
了,只用看符号位是0还是1) - 还能让运算时不用另外区分符号位,直接把符号位当成值位进行运算,而结果的正负号自然会符合这个正负表示法(也就是符号位的进位和值位的进位都会自然地合理)
而且解决方式真的皮,简单到出人意料,就是前面你拍脑袋想到的办法:加一位来表示正负。
具体做法是:在左侧高位增加一个符号位,这个符号位连同前面我们推演出的 伪补码 一同构成真正完善的补码。
实现的效果:通过读取符号位能得知数的正负,同时符号位在加法运算中跟值位一样参与运算、进位、退位。
最后
总结一下
- 使用补码的目的:简化计算机基本运算电路,使加减法都只需要用加法电路实现,用加法替代减法。
- 补码为什么能达到这个目的:n位二进制可以构成一个可溢出计数系统,在这样的系统中,把计数系统容量作为模,所有对此模同余的数在此计数系统中都会有同样的表示,而且运算等价。而补码就是负数的最小正同余数,所以加一个负数和减一个正数都可以用加一个补码来表示。
- 怎么计算补码:正数的补码是它本身;对负数求最小正同余数(模为值位的容量)放入值位,符号位置为1,得到负数的补码。
到这里「整数为什么以补码的形式存储」这个问题基本就解答清楚了,你会发现里边都没有反码的影子,对,就是这样,用反码以及教材里那套计算补码的方法来理解补码都是缘木求鱼,那它们是用来干什么的?值位取反加一这种算法是怎么冒出来的?(求补码的简便算法) 以及大牛对补码的完善为什么可行?(补码正确性的证明) 感兴趣的同学可以点击超链接继续看补充内容。