64位下的相對指令地址-X86指令格式(操作碼列和指令列解釋)


尋找64位系統某符號特征碼時發現他的MOV指令用的是相對地址,之前32位下從來沒聽說MOV還能用相對地址,故查閱了下Intel指令手冊。
在MOV指令介紹下找到如下介紹:

In 64-bit mode, the instruction’s default operation size is 32 bits. Use of the REX.R prefix permits access to additional
registers (R8-R15). Use of the REX.W prefix promotes operation to 64 bits. See the summary chart at the
beginning of this section for encoding data and limits.

在64位下仍使用32位操作數,REX.R擴展寄存器,REX.W擴展指令。
REX前綴結構:

這里寫圖片描述

關於RIP的介紹:

2.2.1.6 RIP-Relative Addressing
A new addressing form, RIP-relative (relative instruction-pointer) addressing, is implemented in 64-bit mode. An
effective address is formed by adding displacement to the 64-bit RIP of the next instruction.
In IA-32 architecture and compatibility mode, addressing relative to the instruction pointer is available only with
control-transfer instructions. In 64-bit mode, instructions that use ModR/M addressing can use RIP-relative
addressing. Without RIP-relative addressing, all ModR/M modes address memory relative to zero.
RIP-relative addressing allows specific ModR/M modes to address memory relative to the 64-bit RIP using a signed
32-bit displacement. This provides an offset range of ±2GB from the RIP. Table 2-7 shows the ModR/M and SIB
encodings for RIP-relative addressing. Redundant forms of 32-bit displacement-addressing exist in the current
ModR/M and SIB encodings. There is one ModR/M encoding and there are several SIB encodings. RIP-relative
addressing is encoded using a redundant form.
In 64-bit mode, the ModR/M Disp32 (32-bit displacement) encoding is re-defined to be RIP+Disp32 rather than
displacement-only. See Table 2-7.
這里寫圖片描述
The ModR/M encoding for RIP-relative addressing does not depend on using a prefix. Specifically, the r/m bit field
encoding of 101B (used to select RIP-relative addressing) is not affected by the REX prefix. For example, selecting
R13 (REX.B = 1, r/m = 101B) with mod = 00B still results in RIP-relative addressing. The 4-bit r/m field of REX.B
combined with ModR/M is not fully decoded. In order to address R13 with no displacement, software must encode
R13 + 0 using a 1-byte displacement of zero.
RIP-relative addressing is enabled by 64-bit mode, not by a 64-bit address-size. The use of the address-size prefix
does not disable RIP-relative addressing. The effect of the address-size prefix is to truncate and zero-extend the
computed effective address to 32 bits.

RIP是64位的新特性,在64位下,指令使用特定的Mod\rm來使用RIP,RIP的偏移是32位故尋址范圍為上下2GB。RIP的計算時相對於當前指令的下一條指令的地址來計算的,既目標地址=下一條指令地址+偏移。RIP中ModR\M不取決於指令前綴,比如指令前綴與R\M指定了R13寄存器,但mod是00,指令仍然使用RIP而不是r13寄存器。

舉個例子,原始指令:4c8b2dedd9eaff
其中4c是REX,打開了W和R,即R和reg聯合制定了r13寄存器,但不用SIB,2d則是00101101,就是使用RIP,后面是32位偏移。
在計算MOV指令的地址時可以這樣算:

    //算出ObpLookupObjectByName的地址 ULONG_PTR ObpLookupObjectByName = (ULONG_PTR)((PUCHAR)tg1_addr + 0x301 + 5 + offset); //在ObpLookupObjectByName的偏移62C處是指令MOV R13,ObRootDirectoryObject //而加7則定位到下一條指令 ULONG_PTR next_code = (ULONG_PTR)((PUCHAR)ObpLookupObjectByName + 0x62C + 7); //取出偏移值 UINT32 rip = *(PINT32)((PUCHAR)ObpLookupObjectByName + 0x62C + 3); //用下一條指令地址+偏移值即可得到目標地址 POBJECT_DIRECTORY ObRootDirectoryObject= (POBJECT_DIRECTORY)((ULONG_PTR)next_code+rip);


 

 

IMMEDIATE  立即數

REGISTER    寄存器操作數

MEMORY    內存操作數

REGISTER_RIP 寄存器相對指令地址

 

 

AT&T匯編語言語法與Intel的類似,你可以參考gas手冊。

區別在下面幾點(摘自gas manual):

AT&T Syntax versus Intel Syntax  //AT&T語法與Intel語法的對比

-------------------------------

 

orignal:

In order to maintain compatibility with the output of gcc, as supports AT&T System V/386 assembler syntax. 

This is quite different from Intel syntax. 

We mention these differences because almost all 80386 documents used only Intel syntax. 

Notable differences between the two syntaxes are: 

翻譯:

 

為了與gcc的output(gcc -s source_file)保持兼容,因為gcc支持AT&T System V/386匯編語法格式。

這種AT&T的匯編語法格式與Intel的匯編語法格式有顯著的不同。

我們之所以提到這些不同,是因為幾乎所有的80386 documents 文檔都使用Intel的語法格式。

這兩種語法格式的顯著區別如下:

1>立即數:(immediate operand)

AT&T immediate operands are preceded by `$';  //AT&T的立即數前有前導的'$‘;

Intel immediate operands are undelimited (Intel `push 4' is AT&T `pushl $4').  //Intel的立即數沒有限定符

 

2>寄存器操作數:(register operand)

AT&T register operands are preceded by `%';  //AT&T的寄存器操作數前有前導的%限定

Intel register operands are undelimited.         //Intel的寄存器操作數沒有限定。

 

3>絕對跳轉指令:(absolute jump/call)

AT&T absolute (as opposed to PC relative) jump/call operands are prefixed by `*';  //AT&T的絕對跳轉指令前有前綴*;

they are undelimited in Intel syntax.  //Intel的絕對跳轉指令前沒有限定。

 

4> 源操作數和目的操作數的位置:(source and destination location)

AT&T and Intel syntax use the opposite order for source and destination operands.  //兩者源操作數與目的操作數的位置相反

Intel `add eax, 4' is `addl $4, %eax'.  // Intel的格式: op-code 目的操作數,源操作數;

                                                          // AT&T的格式:op-code源操作數,目的操作數;

The `source, dest' convention is maintained for compatibility with previous Unix assemblers.

//AT&T的這種’源操作數,目的操作數‘的約定(規約)是為了與先前的Unix Assemblers保持兼容性。

 

 5>內存操作數的size:(b, w, l, )

//在AT&T語法格式中,操作數的存儲尺寸是由op-code最后一個字符決定的:

//b (byte, 8), w(word,16),  l(long, 32)。

In AT&T syntax the size of memory operands is determined from the last character of the opcode name.

 Opcode suffixes of `b', `w', and `l' specify byte (8-bit), word (16-bit), and long (32-bit) memory references. 

 

//Intel 語法實現操作數的size通過,operand的前綴,如,byte ptr (byte,  8), word ptr(word,16), dword ptr(double word, 32)

Intel syntax accomplishes this by prefixes memory operands (not the opcodes themselves) 

with `byte ptr', `word ptr', and `dword ptr'.

//兩者間的等價舉例: 

Thus, Intel `mov al, byte ptr foo' is `movb foo, %al' in AT&T syntax. 

 

6>長jump/call 和長ret (long jumps/calls and long ret)

Immediate form long jumps and calls are `lcall/ljmp $section, $offset' in AT&T syntax; 

//AT&T語法格式:lcall/ljmp $section, $offset

 the Intel syntax is `call/jmp far section:offset'.

//Intel語法格式:call/jmp far section:offset

 

//同樣 long return,指令也相似: 

Also, the far return instruction is `lret $stack-adjust' in AT&T syntax; //AT&T語法格式 

Intel syntax is `ret far stack-adjust'.  //Intel語法格式

 

7>其他: multiple sections

The AT&T assembler does not provide support for multiple section programs. 

//AT&T assebmler 不提供對多段程序的支持。

Unix style systems expect all programs to be single sections. 

Unix風格的系統認為所有的程序都是一個段。

 

8> references: 參考書目:

 <1>.參考sun的x86匯編手冊:http://oldlinux.org/download/805-4693.pdf

 

補充:

 

Brennan's Guide to Inline Assembly

by Brennan "Bas" Underwood

Document version 1.1.2.2

 

Ok. This is meant to be an introduction to inline assembly under DJGPP. DJGPP is based on GCC, so it uses the AT&T/UNIX syntax and has a somewhat unique method of inline assembly. I spent many hours figuring some of this stuff out and told Info that I hate it, many times.

Hopefully if you already know Intel syntax, the examples will be helpful to you. I've put variable names, register names and other literals in bold type.

 

The Syntax

So, DJGPP uses the AT&T assembly syntax. What does that mean to you?

  • Register naming:
    Register names are prefixed with "%". To reference eax:
    AT&T:  %eax
    Intel: eax
    
  • Source/Destination Ordering:
    In AT&T syntax (which is the UNIX standard, BTW) the source is always on the left, and the destination is always on the right.
    So let's load ebx with the value in eax:
    AT&T:  movl %eax, %ebx
    Intel: mov ebx, eax
    
  • Constant value/immediate value format:
    You must prefix all constant/immediate values with "$".
    Let's load eax with the address of the "C" variable booga, which is static.
    AT&T:  movl $_booga, %eax
    Intel: mov eax, _booga
    
    Now let's load ebx with 0xd00d:
    AT&T:  movl $0xd00d, %ebx
    Intel: mov ebx, d00dh
    
  • Operator size specification:
    You must suffix the instruction with one of b, w, or l to specify the width of the destination register as a byte, word or longword. If you omit this, GAS (GNU assembler) will attempt to guess. You don't want GAS to guess, and guess wrong! Don't forget it.
    AT&T:  movw %ax, %bx
    Intel: mov bx, ax
    
    The equivalent forms for Intel is byte ptr, word ptr, and dword ptr, but that is for when you are...
  • Referencing memory:
    DJGPP uses 386-protected mode, so you can forget all that real-mode addressing junk, including the restrictions on which register has what default segment, which registers can be base or index pointers. Now, we just get 6 general purpose registers. (7 if you use ebp, but be sure to restore it yourself or compile with -fomit-frame-pointer.)
    Here is the canonical format for 32-bit addressing:
    AT&T:  immed32(basepointer,indexpointer,indexscale)
    Intel: [basepointer + indexpointer*indexscale + immed32]
    
    You could think of the formula to calculate the address as:
      immed32 + basepointer + indexpointer * indexscale
    
    You don't have to use all those fields, but you do have to have at least 1 of immed32, basepointer and you MUST add the size suffix to the operator!
    Let's see some simple forms of memory addressing:

     

    • Addressing a particular C variable:
      AT&T:  _booga
      Intel: [_booga]
      
      Note: the underscore ("_") is how you get at static (global) C variables from assembler. This only works with global variables. Otherwise, you can use extended asm to have variables preloaded into registers for you. I address that farther down.

       

    • Addressing what a register points to:
      AT&T:  (%eax)
      Intel: [eax]
      
    • Addressing a variable offset by a value in a register:
      AT&T: _variable(%eax)
      Intel: [eax + _variable]
      
    • Addressing a value in an array of integers (scaling up by 4):
      AT&T:  _array(,%eax,4)
      Intel: [eax*4 + array]
      
    • You can also do offsets with the immediate value:
      C code: *(p+1) where p is a char *
      AT&T:  1(%eax) where eax has the value of p
      Intel: [eax + 1]
      
    • You can do some simple math on the immediate value:
      AT&T: _struct_pointer+8
      
      I assume you can do that with Intel format as well.

       

    • Addressing a particular char in an array of 8-character records:
      eax holds the number of the record desired. ebx has the wanted char's offset within the record.
      AT&T:  _array(%ebx,%eax,8)
      Intel: [ebx + eax*8 + _array]
      
    Whew. Hopefully that covers all the addressing you'll need to do. As a note, you can put esp into the address, but only as the base register.

Basic inline assembly

The format for basic inline assembly is very simple, and much like Borland's method.

asm ("statements");

Pretty simple, no? So

asm ("nop");

will do nothing of course, and

asm ("cli");

will stop interrupts, with

asm ("sti");

of course enabling them. You can use  __asm__  instead of  asm  if the keyword  asm  conflicts with something in your program.

When it comes to simple stuff like this, basic inline assembly is fine. You can even push your registers onto the stack, use them, and put them back.

asm ("pushl %eax\n\t"
     "movl $0, %eax\n\t"
     "popl %eax");

(The \n's and \t's are there so the  .s  file that GCC generates and hands to GAS comes out right when you've got multiple statements per  asm .)
It's really meant for issuing instructions for which there is no equivalent in C and don't touch the registers.

But if you do touch the registers, and don't fix things at the end of your asm statement, like so:

asm ("movl %eax, %ebx");
asm ("xorl %ebx, %edx");
asm ("movl $0, _booga");

then your program will probably blow things to hell. This is because GCC hasn't been told that your  asm  statement clobbered  ebx  and  edx  and  booga , which it might have been keeping in a register, and might plan on using later. For that, you need:

Extended inline assembly

The basic format of the inline assembly stays much the same, but now gets Watcom-like extensions to allow input arguments and output arguments.

Here is the basic format:

asm ( "statements" : output_registers : input_registers : clobbered_registers);

Let's just jump straight to a nifty example, which I'll then explain:

asm ("cld\n\t"
     "rep\n\t"
     "stosl"
     : /* no output registers */
     : "c" (count), "a" (fill_value), "D" (dest)
     : "%ecx", "%edi" );

The above stores the value in  fill_value   count  times to the pointer  dest .

Let's look at this bit by bit.

asm ("cld\n\t"

We are clearing the direction bit of the  flags  register. You never know what this is going to be left at, and it costs you all of 1 or 2 cycles.

     "rep\n\t"
     "stosl"

Notice that GAS requires the  rep  prefix to occupy a line of it's own. Notice also that  stos  has the  l  suffix to make it move  longwords .

     : /* no output registers */

Well, there aren't any in this function.

     : "c" (count), "a" (fill_value), "D" (dest)

Here we load  ecx  with  count ,  eax  with  fill_value , and  edi  with  dest . Why make GCC do it instead of doing it ourselves? Because GCC, in its register allocating, might be able to arrange for, say,  fill_value  to already be in  eax . If this is in a loop, it might be able to preserve  eax  thru the loop, and save a  movl  once per loop.

     : "%ecx", "%edi" );

And here's where we specify to GCC, "you can no longer count on the values you loaded into  ecx  or  edi  to be valid." This doesn't mean they will be reloaded for certain. This is the clobberlist.

Seem funky? Well, it really helps when optimizing, when GCC can know exactly what you're doing with the registers before and after. It folds your assembly code into the code it's generates (whose rules for generation look remarkably like the above) and then optimizes. It's even smart enough to know that if you tell it to put (x+1) in a register, then if you don't clobber it, and later C code refers to (x+1), and it was able to keep that register free, it will reuse the computation. Whew.

Here's the list of register loading codes that you'll be likely to use:

a        eax
b        ebx
c        ecx
d        edx
S        esi
D        edi
I        constant value (0 to 31)
q,r      dynamically allocated register (see below)
g        eax, ebx, ecx, edx or variable in memory
A        eax and edx combined into a 64-bit integer (use long longs)

Note that you can't directly refer to the byte registers ( ah ,  al , etc.) or the word registers ( ax ,  bx , etc.) when you're loading this way. Once you've got it in there, though, you can specify  ax  or whatever all you like.

The codes have to be in quotes, and the expressions to load in have to be in parentheses.

When you do the clobber list, you specify the registers as above with the %. If you write to a variable, you must include "memory" as one of The Clobbered. This is in case you wrote to a variable that GCC thought it had in a register. This is the same as clobbering all registers. While I've never run into a problem with it, you might also want to add "cc" as a clobber if you change the condition codes (the bits in the flags register the jnz, je, etc. operators look at.)

Now, that's all fine and good for loading specific registers. But what if you specify, say, ebx, and ecx, and GCC can't arrange for the values to be in those registers without having to stash the previous values. It's possible to let GCC pick the register(s). You do this:

asm ("leal (%1,%1,4), %0"
     : "=r" (x)
     : "0" (x) );

The above example multiplies x by 5 really quickly (1 cycle on the Pentium). Now, we could have specified, say  eax . But unless we really need a specific register (like when using  rep movsl  or  rep stosl , which are hardcoded to use  ecx ,  edi , and  esi ), why not let GCC pick an available one? So when GCC generates the output code for GAS, %0 will be replaced by the register it picked.

And where did "q" and "r" come from? Well, "q" causes GCC to allocate from eax, ebx, ecx, and edx. "r" lets GCC also consider esi and edi. So make sure, if you use "r"that it would be possible to use esi or edi in that instruction. If not, use "q".

Now, you might wonder, how to determine how the %n tokens get allocated to the arguments. It's a straightforward first-come-first-served, left-to-right thing, mapping to the"q"'s and "r"'s. But if you want to reuse a register allocated with a "q" or "r", you use "0", "1", "2"... etc.

You don't need to put a GCC-allocated register on the clobberlist as GCC knows that you're messing with it.

Now for output registers.

asm ("leal (%1,%1,4), %0"
     : "=r" (x_times_5)
     : "r" (x) );

Note the use of  =  to specify an output register. You just have to do it that way. If you want 1 variable to stay in 1 register for both in and out, you have to respecify the register allocated to it on the way in with the  "0"  type codes as mentioned above.

asm ("leal (%0,%0,4), %0"
     : "=r" (x)
     : "0" (x) );

This also works, by the way:

asm ("leal (%%ebx,%%ebx,4), %%ebx"
     : "=b" (x)
     : "b" (x) );

2 things here:

  • Note that we don't have to put ebx on the clobberlist, GCC knows it goes into x. Therefore, since it can know the value of ebx, it isn't considered clobbered.
  • Notice that in extended asm, you must prefix registers with %% instead of just %. Why, you ask? Because as GCC parses along for %0's and %1's and so on, it would interpret %edx as a %e parameter, see that that's non-existent, and ignore it. Then it would bitch about finding a symbol named dx, which isn't valid because it's not prefixed with % and it's not the one you meant anyway.

Important note:  If your assembly statement  must  execute where you put it, (i.e. must not be moved out of a loop as an optimization), put the keyword  volatile  after  asm  and before the ()'s. To be ultra-careful, use

__asm__ __volatile__ (...whatever...);

However, I would like to point out that if your assembly's only purpose is to calculate the output registers, with no other side effects, you should leave off the  volatile keyword so your statement will be processed into GCC's common subexpression elimination optimization.

Some useful examples

#define disable() __asm__ __volatile__ ("cli");

#define enable() __asm__ __volatile__ ("sti");

Of course,  libc  has these defined too.

#define times3(arg1, arg2) \
__asm__ ( \
  "leal (%0,%0,2),%0" \
  : "=r" (arg2) \
  : "0" (arg1) );

#define times5(arg1, arg2) \
__asm__ ( \
  "leal (%0,%0,4),%0" \
  : "=r" (arg2) \
  : "0" (arg1) );

#define times9(arg1, arg2) \
__asm__ ( \
  "leal (%0,%0,8),%0" \
  : "=r" (arg2) \
  : "0" (arg1) );

These multiply arg1 by 3, 5, or 9 and put them in arg2. You should be ok to do:

times5(x,x);

as well.

#define rep_movsl(src, dest, numwords) \
__asm__ __volatile__ ( \
  "cld\n\t" \
  "rep\n\t" \
  "movsl" \
  : : "S" (src), "D" (dest), "c" (numwords) \
  : "%ecx", "%esi", "%edi" )

Helpful Hint: If you say  memcpy()  with a constant length parameter, GCC will inline it to a  rep movsl  like above. But if you need a variable length version that inlines and you're always moving dwords, there ya go.

#define rep_stosl(value, dest, numwords) \
__asm__ __volatile__ ( \
  "cld\n\t" \
  "rep\n\t" \
  "stosl" \
  : : "a" (value), "D" (dest), "c" (numwords) \
  : "%ecx", "%edi" )

Same as above but for  memset() , which doesn't get inlined no matter what (for now.)

 

#define RDTSC(llptr) ({ \
__asm__ __volatile__ ( \
        ".byte 0x0f; .byte 0x31" \
        : "=A" (llptr) \
        : : "eax", "edx"); })

Reads the TimeStampCounter on the Pentium and puts the 64 bit result into llptr.

 

The End

"The End"?! Yah, I guess so.

If you're wondering, I personally am a big fan of AT&T/UNIX syntax now. (It might have helped that I cut my teeth on SPARC assembly. Of course, that machine actually had a decent number of general registers.) It might seem weird to you at first, but it's really more logical than Intel format, and has no ambiguities.

If I still haven't answered a question of yours, look in the Info pages for more information, particularly on the input/output registers. You can do some funky stuff like use"A" to allocate two registers at once for 64-bit math or "m" for static memory locations, and a bunch more that aren't really used as much as "q" and "r".

Alternately, mail me, and I'll see what I can do. (If you find any errors in the above, please, e-mail me and tell me about it! It's frustrating enough to learn without buggy docs!) Or heck, mail me to say "boogabooga."

It's the least you can do.

 


Related Usenet posts:


Thanks to Eric J. Korpela <korpela@ssl.Berkeley.EDU> for some corrections.


Have you seen the DJGPP2+Games Page? Probably.
Page written and provided by Brennan Underwood.
Copyright © 1996 Brennan Underwood. Share and enjoy!
Page created with  vi , God's own editor.  

 

上文的original link:http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html

 

 

操作碼列

1.主操作碼是 1、2 或 3 字節.其中2字節操作碼和三字節操作碼都在0F開頭,但是二字節的SIMD opcode是一個強制前綴+0fh+一字節的操作碼:

一字節操作碼示例:

操作碼

指令

說明

98

CBW

AX  AL 的符號擴展

FF /4

JMP r/m32

絕對間接近跳轉,地址由 r/m32 給出

2.只要可能,便會按照內存中的出現順序以十六進制字節的形式給出這些代碼,非十六進制字節的其它定義如下:

操作碼

指令

說明

E8 cw

CALL rel16

相對近調用,位移量相對於下一條指令

E8 cd

CALL rel32

相對近調用,位移量相對於下一條指令

FF /2

CALL r/m16

絕對間接近調用,地址由 r/m16 給出

FF /2

CALL r/m32

絕對間接近調用,地址由 r/m32 給出

9A cd

CALL ptr16:16

絕對遠調用,地址由操作數給出

9A cp

CALL ptr16:32

絕對遠調用,地址由操作數給出

FF /3

CALL m16:16

絕對間接遠調用,地址由 m16:16 給出

FF /3

CALL m16:32

絕對間接遠調用,地址由 m16:32 給出

/digit

 為0到7之間的數字,表示指令的 ModR/M byte 只使用 r/m字段作為操作數,而其reg字段作為opcode的一部分,使用digit(下表的/digit(Opcode列))指定的數字

這里再解釋下ModR/M byte,圖請看http://hgy413.com/3288.html

mod(模式)域:連同r/m(寄存器/內存)域共同構成了32個可能的值:8個寄存器和24個尋址模式。 reg/opcode(寄存器/操作數)域:指定了8個寄存器或者額外的3個字節的opcode。究竟這三個字節用來做什么由主opcode指定。 r/m(寄存器/內存)域:可以指定一個寄存器作為操作數,或者可以和mod域聯合用來指定尋址模式。有時候,它和mod域一起用來為某些指令指定額外的信息。

一個指令往往需要引用一個在內存當中的值,典型的如mov:

MOV eax, dword ptr [123456]///一個立即數表示的地址 MOV eax, dword ptr [esi]///一個存放在寄存器當中的地址 MOV eax, ebx///寄存器本身

這其中的 123456 或者 esi 就是 MOV 指令引用的內存地址,而MOV關心的是這個地址當中的內容。這個時候,需要某種方式來為指令指定這個操作數的類型:是一個立即數表示的地址,還是一個存放在寄存器當中的地址,或者,就是寄存器本身。

這個用來區分操作數類型的指令字節就是 ModR/M,確切的說是其中的5個位,即mod和r/m域。剩下的三個位,可能用來做額外的指令字節。因為,IA32的指令個數已經遠超過一個字節所能表示的256個了。因此,有的指令就要復用第一個字節,然后依據ModR/M當中的reg/opcode域進行區分。

CALL指令的表示法:FF /2,是 0xFF 后面跟着一個 /digit 表示的東西。就是說,0xFF后面需要跟一個ModR/M字節,ModR/M字節使用reg/opcode域 = 2 。那么,reg/opcode = 2 的字節有32個,正如ModR/M的解釋,這32個值代表了32種不同的尋址方式。是哪32種呢?手冊上面有張表:

非常復雜的一張表。現在就看看這張表怎么讀。對於SIB的介紹,我們先忽略

   首先是列的定義。由於reg/opcode域可以用來表示opcode,也可以用來表示reg,因此同一個值在不同的指令當中可能代表不同的含義。在表當中,就表現為每一列的表頭都有很多個不同的表示。我們需要關心的就是 opcode這一個。注意看我用紅圈圈出來的部分,這一列就是opcode=2的一列。而我們需要的CALL指令,也就是在這一列當中,0xFF后面需要跟着的內容。

   行的定義就是不同的尋址模式。正如手冊所說,mod + R/M域,共5個字節,定義了32種尋址模式。

   0x10--0x17 對應於寄存器尋址。例如指令 CALL dword ptr [eax] :[eax]尋址對應的是0x10,因此,該指令對應的二進制就是 FF 10。同理, CALL dword ptr [ebx] 是 FF 13,CALL dword ptr [esi] 是 FF 16,這些指令都是2個字節。有人也許問 CALL word ptr [eax] 是什么?抱歉,這不是一個合法的32位指令。

注意到這一列中有個disp32,說明是ff 15 + 32位數據:

00020000 ff1510203040    call    dword ptr ds:[40302010h]

   0x50-0x57部分需要帶一個disp8,即 8bit立即數,也就是一個字節。這個是基地址+8位偏移量的尋址模式。例如 CALL dword ptr [eax+10] 就是 FF 50 10 。注意雖然表當中寫的是 [eax] + disp8 這種形式,但是並不表示是取得 eax 指向的地址當中的值再加上 disp8,而是在eax上加上disp8再進行尋址。因此寫成 [eax+disp8] 更不容易引起誤解。后面的disp32也是一樣的。這個類型指令是3個字節。

00020000 ff5130          call    dword ptr [ecx+30h]

  0x90-0x97部分需要帶 disp32,即4字節立即數。這個是基地址+32位偏移量。例如 CALL dword ptr [eax+12345] 就是 FF 90 00 01 23 45。有趣的是, CALL dword ptr [eax+10] 也可以寫成 FF 90 00 00 00 10。至於匯編成哪個二進制形式,這是匯編器的選擇。這個類型的指令是6個字節。

00020000 ff9210203040    call    dword ptr [edx+40302010h]

  0xD0-0xD7部分則直接是寄存器。這邊引用的寄存器的類型有很多,但是在CALL指令當中只能引用通用寄存器,因此 CALL eax 就是 FF D0,臭名昭著的 CALL esp 就是 FF D4。注意 CALL eax 和 CALL [eax] 是不一樣的。

00020000 ffd0            call    eax
00020002 ff10 call dword ptr [eax]

 這時應該大家注意到了0x14,0x54,0x94,0x14,0x54,0x94部分是最復雜的,因為這個時候,ModR/M不足以指定尋址方式,而是需要一個額外的字節,這個字節就是指令當中的第4個字節SIB,SIB字節包括下列信息:

某些特定的ModR/M字節需要一個后續字節,稱為SIB字節。32位指令的基地址+偏移量,以及 比例*偏移量 的形式的尋址方式需要SIB字節。\ scale(比例)域指定了放大的比例。 index(偏移)域指定了用來存放偏移量 的寄存器。 base (基地址)域用來標識存放基地址的寄存器。

0x14, 0x54, 0x94就是這里所說的“特定的ModR/M字節。這個字節后面跟着的SIB表示了一個復雜的尋址方式,典型的見於虛函數調用:

CALL dword ptr [ecx+4*eax]

就是調用ecx指向的虛表當中的第eax個虛函數。這個指令當中,因為沒有立即數,因此FF后面的字節就是0x14,而 [ecx+4*eax] 就需要用SIB字節來表示。SIB確定的尋址方式是[base+Index* Scale +disp]

在這個指令當中,ecx就是 Base,4是Scale,eax是Index。

那么,Base, Scale和Index是如何確定的呢?手冊上同樣有一張表(又是巨大的表):

列是Base,行是Index*Scale,例如[ecx+4*eax] 就是0x81。

根據這張表,CALL dword ptr [ecx+4*eax] 就是 FF 14 81 。由此可見,對於 0x14系列的來說,CALL指令就是 3個字節。

00020000 ff1481          call    dword ptr [ecx+eax*4]

而 0x54 帶 8bit 立即數,就是對應於 CALL指令:CALL dword ptr [ecx+4*eax+xx],這個指令就是 FF 54 81 xx,是4個字節。

00020000 ff548120        call    dword ptr [ecx+eax*4+20h]

同理,0x94帶32位立即數,對應於CALL指令:CALL dword ptr [ecx+4*eax+xxxxxxxx],這個指令就是 FF 94 81 xx xx xx xx,是7個字節。

00020000 ff948120304000  call    dword ptr [ecx+eax*4+403020h]

/r

表示指令的 ModR/M 字節同時包含寄存器操作數與 r/m 操作數

89 /r

MOV r/m32,r32

將 r32 移到 r/m32

比如 

00020000 8933 mov dword ptr [ebx],esi

cb、cw、cd、cp

1 字節 (cb)、2 字節 (cw)、4 字節 (cd) 或 6 字節 (cp) 值,跟在操作碼的后面,用於指定代碼偏移量,並可能用於給代碼段寄存器指定新的值,一般用於我們在匯編中寫call lable

E8 cw 的含義是:字節 0xE8 后面跟着一個2字節操作數表示要跳轉到的地址與當前地址的偏移量。 E8 cd 的含義是:字節 0xE8 后面跟着一個4字節的操作數表示要跳轉的地址與當前地址的偏移量。 9A cp 的含義是:字節 0x9A 后面跟着一個6字節的操作數表示要跳轉的地址和代碼段寄存器的值。

ib、iw、id 

指令的 1 字節 (ib)、2 字節 (iw) 或 4 字節 (id) 立即數操作數,跟在操作碼、ModR/M字節或基數索引字節的后面。操作碼確定操作數是否為有符號值。所有的字與雙字都是按照低位字節在先的形式給出。

操作碼

指令

說明

14 ib

ADC AL,imm8

帶進位將 imm8 加到 AL 上

15 iw

ADC AX,imm16

帶進位將 imm16 加到 AX 上

15 id

ADC EAX,imm32

帶進位將 imm32 加到 EAX 上

00020000 1510203040 adc eax,40302010h

+rb、+rw、+rd 

從0到7的寄存器代碼,它添加到加號左側給出的十六進制字節,以形成單個操作碼字節。寄存器如下:

操作碼

指令

FF /6

PUSH r/m32

50+rw

PUSH r16

50+rd

PUSH r32

 

rb

   

rw

   

rd

 

AL

=

0

AX

=

0

EAX

=

0

CL

=

1

CX

=

1

ECX

=

1

DL

=

2

DX

=

2

EDX

=

2

BL

=

3

BX

=

3

EBX

=

3

 

rb

   

rw

   

rd

 

AH

=

4

SP

=

4

ESP

=

4

CH

=

5

BP

=

5

EBP

=

5

DH

=

6

SI

=

6

ESI

=

6

BH

=

7

DI

=

7

EDI

=

7

00020000 50 push eax 00020001 51 push ecx 00020002 52 push edx 00020003 53 push ebx 00020004 54 push esp 00020005 55 push ebp 00020006 56 push esi 00020007 57 push edi

+i

操作數之一是來自 FPU 寄存器堆棧的 ST(i) 時浮點指令中使用的數字。數字 i(范圍從 0 到 7)添加到加號左側給出的十六進制字節,以形成單個操作碼字節

指令列

rel:relative(rel8,rel16,rel32)

rel8:指令前128個字節到指令后127個字節范圍內的相對地址。

rel16與rel32匯編后的指令所在的代碼段內的相對地址。rel16 符號適用於操作數大小屬性等於 16 位的指令;rel32 符號適用於操作數大小屬性等於 32 位的指令。

77 cb

JA rel8

高於(CF=0 且 ZF=0)時短跳轉

0F 8C cw/cd

JL rel16/32

小於 (SF<>OF) 時近跳轉

00020000 7710 ja 00020012//20002+10 = 20012 00020002 0f8c10200057 jl 57022018//20008+57002010=57022018

ptr16:16 與 ptr16:32 

遠指針,通常與指令不在同一個代碼段中。16:16 記法表示指針值包含兩個部分。冒號左側的值是一個16位選擇器,或是代碼段寄存器的目標值。冒號右側的值對應目標段中的偏移量。指令的操作數大小屬性是16位時,使用 ptr16:16 符號;操作數大小屬性是32位時,使用 ptr16:32 符號  

EA cd

JMP ptr16:16

絕對遠跳轉,地址由操作數給出

EA cp

JMP ptr16:32

絕對遠跳轉,地址由操作數給出

FF /5

JMP m16:16

絕對間接遠跳轉,地址由 m16:16 給出

FF /5

JMP m16:32

絕對間接遠跳轉,地址由 m16:32 給出

00020000 ff25d4924100    jmp     dword ptr ds:[4192D4h]

r(register)

r8 - 字節通用寄存器 AL、CL、DL、BL、AH、CH、DH 或 BH 之一。

r16 - 字通用寄存器 AX、CX、DX、BX、SP、BP、SI 或 DI 之一。

r32 - 雙字通用寄存器 EAX、ECX、EDX、EBX、ESP、EBP、ESI 或 EDI 之一。

imm(立即數)

imm8 - 立即數字節。imm8 符號是 -128 到 +127(含)之間的一個有符號數字。對於結合使用 imm8 與字或雙字操作數的指令,立即數會進行符號擴展,以形成一個字或雙字。字的高位字節使用立即數的最高位填充。

imm16 - 操作數大小屬性等於 16 位的指令使用的立即數字。這是 -32,768 到 +32,767(含)之間的一個數值。

imm32 - 操作數大小屬性等於 32 位的指令使用的立即數雙字。它允許使用 -2,147,483,648 到 +2,147,483,647(含)之間的數值。

r/m

r/m8 - 字節操作數,可以是字節通用寄存器(AL、BL、CL、DL、AH、BH、CH 及 DH)的內容,或是內存中的一個字節。

r/m16 - 操作數大小屬性等於 16 位的指令使用的字通用寄存器或內存操作數。字通用寄存器有:AX、BX、CX、DX、SP、BP、SI 及 DI。內存的內容位於有效地址計算提供的地址。

r/m32 - 操作數大小屬性等於 32 位的指令使用的雙字通用寄存器或內存操作數。雙字通用寄存器有:EAX、EBX、ECX、EDX、ESP、EBP、ESI 及 EDI。內存的內容位於有效地址計算提供的地址。

這里要特別注意:

89 /r

MOV r/m32,r32

將 r32 移到 r/m32

8B /r

MOV r32,r/m32

將 r/m32 移到 r32

 如:

MOV ecx,edx

這里就有兩種解釋:

如果是89,則r32為源操作數,r32 = edx, r/m32 = ecx

如果是8b,則r32為目標操作數,r32 = ecx, r/m32 = edx

所以可以構建出如下匯編:

00020000 89d1 mov ecx,edx 00020002 8bca mov ecx,edx

m(內存操作數)

m - 內存中的 16 或 32 位操作數。

m8 - 內存中的字節操作數,通常表示為變量或數組名稱,但由 DS:(E)SI 或 ES:(E)DI 寄存器指向它。此術語僅用於字符串指令與 XLAT 指令。

m16 - 內存中的字操作數,通常表示為變量或數組名稱,但由 DS:(E)SI 或 ES:(E)DI 寄存器指向它。此術語僅用於字符串指令。

m32 - 內存中的雙字操作數,通常表示為變量或數組名稱,但由 DS:(E)SI 或 ES:(E)DI 寄存器指向它。此術語僅用於字符串指令。

m64 - 內存中的內存四字操作數。此術語僅用於 CMPXCHG8B 指令。

m128 - 內存中的內存雙四字操作數。此術語僅用於“數據流單指令多數據擴展指令集”。

m16:16、m16:32 - 包含兩個數字組成的遠指針的內存操作數。冒號左側的數字對應指針的段選擇器。右側的數字對應它的偏移量。

m16&32、m16&16、m32&32 - 由成對的數據項組成的內存操作數,其大小分別在和號 (&) 的左右兩側指出。允許使用所有的內存尋址模式。m16&16 與 m32&32 操作數由 BOUND 指令使用,以便提供包含數組下標的上、下邊界的操作數。m16&32 操作數由 LIDT 與 LGDT 指令使用,以便提供用於加載限制字段的字,以及用於加載對應的 GDTR 與 IDTR 寄存器基址字段的雙字。

moffs8、moffs16、moffs32 - 字節、字或雙字類型的簡單內存變量(內存偏移量),供 MOV 指令的一些變體使用。實際地址按照相對於段基址的簡單偏移量的形式給出。指令中不使用 ModR/M 字節。隨 moffs 顯示的數字表示其大小,這由指令的地址大小屬性確定。

sreg(段寄存器)

段寄存器的位分配情況是:ES=0、CS=1、SS=2、DS=3、FS=4 及 GS=5

其余

m32real、m64real、m80real - 分別是內存中的單精度、雙精度及擴展型實數浮點操作數。

m16int、m32int、m64int - 分別是內存中的字、短整型及長整型浮點操作數。

ST 或 ST(0) - FPU 寄存器堆棧的棧頂元素。

ST(i) - 從 FPU 寄存器堆棧的棧頂元素數算起的第 i 個元素。(i0 到 7)

mm - MMX™ 技術寄存器。64 位 MMX 寄存器有:MM0 到 MM7。

mm/m32 - MMX 寄存器的低 32 位,或是 32 位內存操作數。64 位 MMX 寄存器有:MM0 到 MM7。內存的內容位於有效地址計算提供的地址。

mm/m64 - MMX 寄存器,或是 64 位內存操作數。64 位 MMX 寄存器有:MM0 到 MM7。內存的內容位於有效地址計算提供的地址。

xmm - XMM 寄存器。128 位 XMM 寄存器有:XMM0 到 XMM7。

xmm/m32 - XMM 寄存器,或是 32 位內存操作數。128 位 XMM 寄存器有:XMM0 到 XMM7。內存的內容位於有效地址計算提供的地址。

xmm/m64 - XMM 寄存器,或是 64 位內存操作數。128 位 SIMD 浮點寄存器有:XMM0 到 XMM7。內存的內容位於有效地址計算提供的地址。

xmm/m128 - XMM 寄存器,或是 128 位內存操作數。128 位 XMM 寄存器有:XMM0 到 XMM7。內存的內容位於有效地址計算提供的地址。

參考

CALL指令有多少種寫法

https://cloud.tencent.com/developer/article/1647526

vc++中.ncb .clw .aps文件的作用

.clw文件記錄了類的信息,如果classView中某個類不見了,重新生成該文件就可以了,方法:刪除此文件,點擊“建立類向導”,根據提示輸入工程名稱就可以了;


.ncb文件記錄了類的提示信息,如果類的成員函數和變量的提示不見了,重新生成該文件即可,方法同上; 

.aps文件記錄了資源信息,要利用現成的資源,需要修改3個文件,.rc文件,Resource.h文件和.aps文件,.aps直接刪除后,進入程序,VC會自動生成。

VS中*.clw *.ncb *.opt *.aps這些文件是做什么用的?

通常,VS在建立一個工程之后,會出現*.clw *.ncb *.opt *.aps為后綴的文件

.CLW 文件是VC Class Wizard信息文件。存放了Class Wizard的信息。
.NCB 文件是分析器信息文件,是由系統自動產生的。
.OPT 文件是IDE的Option文件。
.APS 文件是資源文件的二進制版本。

還有其他的幾個
.bsc 瀏覽器信息文件
.dsp 項目文件
.dsw 工作空間文件
.mak 外部的創建文件
.plg 建立日志文件

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM