Intel CPU security features

List of Intel CPU security features along with short descriptions taken from the Intel manuals.

WP (Write Protect) (PDF)

Quoting Volume 3A, 4-3, Paragraph 4.1.3:

CR0.WP allows pages to be protected from supervisor-mode writes. If CR0.WP = 0, supervisor-mode write accesses are allowed to linear addresses with read-only access rights; if CR0.WP = 1, they are not (User-mode write accesses are never allowed to linear addresses with read-only access rights, regardless of the value of CR0.WP).

Interesting links:

NXE/XD (No-Execute Enable/Execute Disable) (PDF)

Regarding IA32_EFER MSR and NXE (Volume 3A, 4-3, Paragraph 4.1.3):

IA32_EFER.NXE enables execute-disable access rights for PAE paging and IA-32e paging. If IA32_EFER.NXE = 1, instructions fetches can be prevented from specified linear addresses (even if data reads from the addresses are allowed).

IA32_EFER.NXE has no effect with 32-bit paging. Software that wants to use this feature to limit instruction fetches from readable pages must use either PAE paging or IA-32e paging.

Regarding XD (Volume 3A, 4-17, Table 4-11):

If IA32_EFER.NXE = 1, execute-disable (if 1, instruction fetches are not allowed from the 4-KByte page controlled by this entry; see Section 4.6); otherwise, reserved (must be 0).

SMAP (Supervisor Mode Access Protection) (PDF)

Quoting Volume 3A, 4-3, Paragraph 4.1.3:

CR4.SMAP allows pages to be protected from supervisor-mode data accesses. If CR4.SMAP = 1, software operating in supervisor mode cannot access data at linear addresses that are accessible in user mode. Software can override this protection by setting EFLAGS.AC.

SMEP (Supervisor Mode Execution Protection) (PDF)

Quoting Volume 3A, 4-3, Paragraph 4.1.3:

CR4.SMEP allows pages to be protected from supervisor-mode instruction fetches. If CR4.SMEP = 1, software operating in supervisor mode cannot fetch instructions from linear addresses that are accessible in user mode.

MPX (Memory Protection Extensions) (PDF)

Intel MPX introduces new bounds registers and new instructions that operate on bounds registers. Intel MPX allows an OS to support user mode software (operating at CPL = 3) and supervisor mode software (CPL < 3) to add memory protection capability against buffer overrun. It provides controls to enable Intel MPX extensions for user mode and supervisor mode independently. Intel MPX extensions are designed to allow software to associate bounds with pointers, and allow software to check memory references against the bounds associated with the pointer to prevent out of bound memory access (thus preventing buffer overflow).

Interesting links:

SGX (Software Guard Extensions) (PDF)

These extensions allow an application to instantiate a protected container, referred to as an enclave. An enclave is a protected area in the application’s address space (see Figure 1-1), which provides confidentiality and integrity even in the presence of privileged malware. Accesses to the enclave memory area from any software not resident in the enclave are prevented.

Interesting links:

Protection keys (PDF)

Quoting Volume 3A, 4-31, Paragraph 4.6.2:

The protection-key feature provides an additional mechanism by which IA-32e paging controls access to user-mode addresses. When CR4.PKE = 1, every linear address is associated with the 4-bit protection key located in bits 62:59 of the paging-structure entry that mapped the page containing the linear address. The PKRU register determines, for each protection key, whether user-mode addresses with that protection key may be read or written.

The following paragraphs, taken from LWN, shed some light on the purpose of memory protection keys:

One might well wonder why this feature is needed when everything it does can be achieved with the memory-protection bits that already exist. The problem with the current bits is that they can be expensive to manipulate. A change requires invalidating translation lookaside buffer (TLB) entries across the entire system, which is bad enough, but changing the protections on a region of memory can require individually changing the page-table entries for thousands (or more) pages. Instead, once the protection keys are set, a region of memory can be enabled or disabled with a single register write. For any application that frequently changes the protections on regions of its address space, the performance improvement will be large.

There is still the question (as asked by Ingo Molnar) of just why a process would want to make this kind of frequent memory-protection change. There would appear to be a few use cases driving this development. One is the handling of sensitive cryptographic data. A network-facing daemon could use a cryptographic key to encrypt data to be sent over the wire, then disable access to the memory holding the key (and the plain-text data) before writing the data out. At that point, there is no way that the daemon can leak the key or the plain text over the wire; protecting sensitive data in this way might also make applications a bit more resistant to attack.

Another commonly mentioned use case is to protect regions of data from being corrupted by «stray» write operations. An in-memory database could prevent writes to the actual data most of the time, enabling them only briefly when an actual change needs to be made. In this way, database corruption due to bugs could be fended off, at least some of the time. Ingo was unconvinced by this use case; he suggested that a 64-bit address space should be big enough to hide data in and protect it from corruption. He also suggested that a version of mprotect() that optionally skipped TLB invalidation could address many of the performance issues, especially if huge pages were used. Alan Cox responded, though, that there is real-world demand for the ability to change protection on gigabytes of memory at a time, and that mprotect() is simply too slow.

CET (Control-flow Enforcement Technology) (PDF)

Control-flow Enforcement Technology (CET) provides the following capabilities to defend against ROP/JOP style control-flow subversion attacks:

  • Shadow Stack – return address protection to defend against Return Oriented Programming,
  • Indirect branch tracking – free branch protection to defend against Jump/Call Oriented Programming.
Реклама

Basic Practices in Assembly Language Programming

Contents

 

Introduction

Assembly language is a low-level programming language for niche platforms such as IoTs, device drivers, and embedded systems. Usually, it’s the sort of language that Computer Science students should cover in their coursework and rarely use in their future jobs. From TIOBE Programming Community Index, assembly language has enjoyed a steady rise in the rankings of the most popular programming languages recently.

In the early days, when an application was written in assembly language, it had to fit in a small amount of memory and run as efficiently as possible on slow processors. When memory becomes plentiful and processor speed is dramatically increased, we mainly rely on high level languages with ready made structures and libraries in development. If necessary, assembly language can be used to optimize critical sections for speed or to directly access non-portable hardware. Today assembly language still plays an important role in embedded system design, where performance efficiency is still considered as an important requirement.

In this article, we’ll talk about some basic criteria and code skills specific to assembly language programming. Also, considerations would be emphasized on execution speed and memory consumption. I’ll analyze some examples, related to the concepts of register, memory, and stack, operators and constants, loops and procedures, system calls, etc.. For simplicity, all samples are in 32-bit, but most ideas will be easily applied to 64-bit.

All the materials presented here came from my teaching [1] for years. Thus, to read this article, a general understanding of Intel x86-64 assembly language is necessary, and being familiar with Visual Studio 2010 or above is assumed. Preferred, having read Kip Irvine’s textbook [2] and the MASM Programmer’s Guide [3] are recommended. If you are taking an Assembly Language Programming class, this could be a supplemental reading for studies.

About instruction

The first two rules are general. If you can use less, don’t use more.

1. Using less instructions

Suppose that we have a 32-bit DWORD variable:

.data
   var1 DWORD 123

The example is to add var1 to EAX. This is correct with MOV and ADD:

mov ebx, var1
add eax, ebx

But as ADD can accept one memory operand, you can just

add eax, var1

2. Using an instruction with less bytes

Suppose that we have an array:

.data
   array DWORD 1,2,3

If want to rearrange the values to be 3,1,2, you could

mov eax,array           ;        eax =1
xchg eax,[array+4]      ; 1,1,3, eax =2
xchg eax,[array+8]      ; 1,1,2, eax =3
xchg array,eax          ; 3,1,2, eax =1

But notice that the last instruction should be MOV instead of XCHG. Although both can assign 3 in EAX to the first array element, the other way around in exchange XCHG is logically unnecessary.

Be aware of code size, MOV takes 5-byte machine code but XCHG takes 6, as another reason to choose MOV here:

00000011  87 05 00000000 R      xchg array,eax
00000017  A3 00000000 R         mov array,eax

To check machine code, you can generate a listing file in assembling or open the Disassembly window at runtime in Visual Studio. Also, you can look up from the Intel instruction manual.

About register and memory

In this section, we’ll use a popular example, the nth Fibonacci number, to illustrate multiple solutions in assembly language. The C function would be like:

unsigned int Fibonacci(unsigned int n)
{
    unsigned int previous = 1, current = 1, next = 0;
    for (unsigned int i = 3; i <= n; ++i) 
    {
        next = current + previous;
        previous = current;
        current = next;
    }
    return next;
}

3. Implementing with memory variables

At first, let’s copy the same idea from above with two variables previous and current created here

.data
   previous DWORD ?
   current  DWORD ?

We can use EAX store the result without the next variable. Since MOV cannot move from memory to memory, a register like EDX must be involved for assignment previous = current. The following is the procedure FibonacciByMemory. It receives n from ECX and returns EAX as the nth Fibonacci number calculated:

;------------------------------------------------------------
FibonacciByMemory PROC 
; Receives: ECX as input n 
; Returns: EAX as nth Fibonacci number calculated
;------------------------------------------------------------
   mov   eax,1         
   mov   previous,0         
   mov   current,0         
L1:
   add eax,previous       ; eax = current + previous      
   mov edx, current       ; previous = current
   mov previous, edx
   mov current, eax
loop   L1
   ret
FibonacciByMemory ENDP

4. If you can use registers, don’t use memory

A basic rule in assembly language programming is that if you can use a register, don’t use a variable. The register operation is much faster than that of memory. The general purpose registers available in 32-bit are EAX, EBX, ECX, EDX, ESI, and EDI. Don’t touch ESP and EBP that are for system use.

Now let EBX replace the previous variable and EDX replace current. The following is FibonacciByRegMOV, simply with three instructions needed in the loop:

;------------------------------------------------------------
FibonacciByRegMOV PROC 
; Receives: ECX as input n 
; Returns: EAX, nth Fibonacci number
;------------------------------------------------------------
   mov   eax,1         
   xor   ebx,ebx      
   xor   edx,edx      
L1:
   add  eax,ebx      ; eax += ebx
   mov  ebx,edx
   mov  edx,eax
loop   L1
   ret
FibonacciByRegMOV ENDP

A further simplified version is to make use of XCHG which steps up the sequence without need of EDX. The following shows FibonacciByRegXCHG machine code in its listing, where only two instructions of three machine-code bytes in the loop body:

           ;------------------------------------------------------------
000000DF    FibonacciByRegXCHG PROC
           ; Receives: ECX as input n
           ; Returns: EAX, nth Fibonacci number
           ;------------------------------------------------------------
000000DF  33 C0         xor   eax,eax
000000E1  BB 00000001   mov   ebx,1
000000E6             L1:
000000E6  93            xchg eax,ebx      ; step up the sequence
000000E7  03 C3         add  eax,ebx      ; eax += ebx
000000E9  E2 FB      loop   L1
000000EB  C3            ret
000000EC    FibonacciByRegXCHG ENDP

In concurrent programming

The x86-64 instruction set provides many atomic instructions with the ability to temporarily inhibit interrupts, ensuring that the currently running process cannot be context switched, and suffices on a uniprocessor. In someway, it also would avoid the race condition in multi-tasking. These instructions can be directly used by compiler and operating system writers.

5. Using atomic instructions

As seen above used XCHG, so called as atomic swap, is more powerful than some high level language with just one statement:

xchg  eax, var1

A classical way to swap a register with a memory var1 could be

mov ebx, eax
mov eax, var1
mov var1, ebx

Moreover, if you use the Intel486 instruction set with the .486 directive or above, simply using the atomic XADD is more concise in the Fibonacci procedure. XADD exchanges the first operand (destination) with the second operand (source), then loads the sum of the two values into the destination operand. Thus we have

           ;------------------------------------------------------------
000000EC    FibonacciByRegXADD PROC
           ; Receives: ECX as input n
           ; Returns: EAX, nth Fibonacci number
           ;------------------------------------------------------------
000000EC  33 C0         xor   eax,eax
000000EE  BB 00000001   mov   ebx,1
000000F3             L1:
000000F3  0F C1 D8      xadd eax,ebx   ; first exchange and then add
000000F6  E2 FB      loop   L1
000000F8  C3            ret
000000F9    FibonacciByRegXADD ENDP

Two atomic move extensions are MOVZX and MOVSX. Another worth mentioning is bit test instructions, BT, BTC, BTR, and BTS. For the following example

.data
  Semaphore WORD 10001000b
.code
  btc Semaphore, 6  ; CF=0, Semaphore WORD 11001000b

Imagine the instruction set without BTC, one non-atomic implementation for the same logic would be

mov ax, Semaphore
shr ax, 7
xor Semaphore,01000000b

Little-endian

An x86 processor stores and retrieves data from memory using little-endian order (low to high). The least significant byte is stored at the first memory address allocated for the data. The remaining bytes are stored in the next consecutive memory positions.

6. Memory representations

Consider the following data definitions:

.data
dw1 DWORD 12345678h
dw2 DWORD 'AB', '123', 123h
;dw3 DWORD 'ABCDE'  ; error A2084: constant value too large
by3 BYTE 'ABCDE', 0FFh, 'A', 0Dh, 0Ah, 0
w1 WORD 123h, 'AB', 'A'

For simplicity, the hexadecimal constants are used as initializer. The memory representation is as follows:

As for multiple-byte DWORD and WORD date, they are represented by the little-endian order. Based on this, the second DWORD initialized with 'AB' should be 00004142h and next '123' is 00313233h in their original order. You can’t initialize dw3 as 'ABCDE' that contains five bytes 4142434445h, while you really can initialize by3 in a byte memory since no little-endian for byte data. Similarly, see w1 for a WORD memory.

7. A code error hidden by little-endian

From the last section of using XADD, we try to fill in a byte array with first 7 Fibonacci numbers, as 01, 01, 02, 03, 05, 08, 0D. The following is such a simple implementation but with a bug. The bug does not show up an error immediately because it has been hidden by little-endian.

FibCount = 7
.data
FibArray BYTE FibCount DUP(0ffh)
BYTE 'ABCDEF' 

.code
   mov  edi, OFFSET FibArray       
   mov  eax,1             
   xor  ebx,ebx          
   mov  ecx, FibCount        
 L1:
   mov  [edi], eax                
   xadd eax, ebx                      
   inc  edi                  
 loop L1

To debug, I purposely make a memory 'ABCDEF' at the end of the byte array FibArray with seven 0ffhinitialized. The initial memory looks like this:

Let’s set a breakpoint in the loop. When the first number 01 filled, it is followed by three zeros as this:

But OK, the second number 01 comes to fill the second byte to overwrite three zeros left by the first. So on and so forth, until the seventh 0D, it just fits the last byte here:

All fine with an expected result in FibArray because of little-endian. Only when you define some memory immediately after this FibArray, your first three byte will be overwritten by zeros, as here 'ABCDEF' becomes 'DEF'. How to make an easy fix?

About runtime stack

The runtime stack is a memory array directly managed by the CPU, with the stack pointer register ESP holding a 32-bit offset on the stack. ESP is modified by instructions CALL, RET, PUSH, POP, etc.. When use PUSH and POP or alike, you explicitly change the stack contents. You should be very cautious without affecting other implicit use, like CALL and RET, because you programmer and the system share the same runtime stack.

8. Assignment with PUSH and POP is not efficient

In assembly code, you definitely can make use of the stack to do assignment previous = current, as in FibonacciByMemory. The following is FibonacciByStack where only difference is using PUSH and POPinstead of two MOV instructions with EDX.

;------------------------------------------------------------
FibonacciByStack 
; Receives: ECX as input n 
; Returns: EAX, nth Fibonacci number
;------------------------------------------------------------
   mov   eax,1         
   mov   previous,0         
   mov   current,0         
L1:
   add  eax,previous      ; eax = current + previous     
   push current           ; previous = current
   pop  previous
   mov  current, eax
loop   L1
   ret
FibonacciByStack ENDP

As you can imagine, the runtime stack built on memory is much slower than registers. If you create a test benchmark to compare above procedures in a long loop, you’ll find that FibonacciByStack is the most inefficient. My suggestion is that if you can use a register or memory, don’t use PUSH and POP.

9. Using INC to avoid PUSHFD and POPFD

When you use the instruction ADC or SBB to add or subtract an integer with the previous carry, you reasonably want to reserve the previous carry flag (CF) with PUSHFD and POPFD, since an address update with ADD will overwrite the CF. The following Extended_Add example borrowed from the textbook [2] is to calculate the sum of two extended long integers BYTE by BYTE:

;--------------------------------------------------------
Extended_Add PROC
; Receives: ESI and EDI point to the two long integers
;           EBX points to an address that will hold sum
;           ECX indicates the number of BYTEs to be added
; Returns:  EBX points to an address of the result sum
;--------------------------------------------------------
   clc                      ; clear the Carry flag
   L1:
      mov   al,[esi]        ; get the first integer
      adc   al,[edi]        ; add the second integer
      pushfd                ; save the Carry flag

      mov   [ebx],al        ; store partial sum
      add   esi, 1          ; point to next byte   
      add   edi, 1
      add   ebx, 1          ; point to next sum byte   
      popfd                 ; restore the Carry flag
   loop   L1                ; repeat the loop

   mov   dword ptr [ebx],0  ; clear high dword of sum
   adc   dword ptr [ebx],0  ; add any leftover carry
   ret
Extended_Add ENDP

As we know, the INC instruction makes an increment by 1 without affecting the CF. Obviously we can replace above ADD with INC to avoid PUSHFD and POPFD. Thus the loop is simplified like this:

L1:
   mov   al,[esi]        ; get the first integer
   adc   al,[edi]        ; add the second integer

   mov   [ebx],al        ; store partial sum
   inc   esi             ; add one without affecting CF
   inc   edi
   inc   ebx
loop   L1                ; repeat the loop

Now you might ask what if to calculate the sum of two long integers DWORD by DWORD where each iteration must update the addresses by 4 bytes, as TYPE DWORD. We still can make use of INC to have such an implementation:

clc
xor   ebx, ebx

L1:
    mov eax, [esi +ebx*TYPE DWORD]
    adc eax, [edi +ebx*TYPE DWORD]
    mov [edx +ebx*TYPE DWORD], eax
    inc ebx
loop  L1

Applying a scaling factor here would be more general and preferred. Similarly, wherever necessary, you also can use the DEC instruction that makes a decrement by 1 without affecting the carry flag.

10. Another good reason to avoid PUSH and POP

Since you and the system share the same stack, you should be very careful without disturbing the system use. If you forget to make PUSH and POP in pair, an error could happen, especially in a conditional jump when the procedure returns.

The following Search2DAry searches a 2-dimensional array for a value passed in EAX. If it is found, simply jump to the FOUND label returning one in EAX as true, else set EAX zero as false.

;------------------------------------------------------------
Search2DAry PROC
; Receives: EAX, a byte value to search a 2-dimensional array
;           ESI, an address to the 2-dimensional array
; Returns: EAX, 1 if found, 0 if not found
;------------------------------------------------------------
   mov  ecx,NUM_ROW        ; outer loop count

ROW:   
   push ecx                ; save outer loop counter
   mov  ecx,NUM_COL        ; inner loop counter

   COL:   
      cmp al, [esi+ecx-1]
      je FOUND   
   loop COL

   add esi, NUM_COL
   pop  ecx                ; restore outer loop counter
loop ROW                   ; repeat outer loop

   mov eax, 0
   jmp QUIT
FOUND: 
   mov eax, 1
QUIT:
   ret
Search2DAry ENDP

Let’s call it in main by preparing the argument ESI pointing to the array address and the search value EAX to be 31h or 30h respectively for not-found or found test case:

.data
ary2D   BYTE  10h,  20h,  30h,  40h,  50h
        BYTE  60h,  70h,  80h,  90h,  0A0h
NUM_COL = 5
NUM_ROW = 2

.code
main PROC
   mov esi, OFFSET ary2D
   mov eax, 31h            ; crash if set 30h 
   call Search2DAry
; See eax for search result
   exit
main ENDP

Unfortunately, it’s only working in not-found for 31h. A crash occurs for a successful searching like 30h, because of the stack leftover from an outer loop counter pushed. Sadly enough, that leftover being popped by RETbecomes a return address to the caller.

Therefore, it’s better to use a register or variable to save the outer loop counter here. Although the logic error is still, a crash would not happen without interfering with the system. As a good exercise, you can try to fix.

Assembling time vs. runtime

I would like to talk more about this assembly language feature. Preferred, if you can do something at assembling time, don’t do it at runtime. Organizing logic in assembling indicates doing a job at static (compilation) time, not consuming runtime. Differently from high level languages, all operators in assembly language are processed in assembling such as +, -, *, and /, while only instructions work at runtime like ADD, SUB, MUL, and DIV.

11. Implementing with plus (+) instead of ADD

Let’s redo Fibonacci calculating to implement eax = ebx + edx in assembling with the plus operator by help of the LEA instruction. The following is FibonacciByRegLEA with only one line changed from FibonacciByRegMOV.

;------------------------------------------------------------
FibonacciByRegLEA 
; Receives: ECX as input n 
; Returns: EAX, nth Fibonacci number
;------------------------------------------------------------
   xor   eax,eax         
   xor   ebx,ebx      
   mov   edx,1      
L1:
   lea  eax, DWORD PTR [ebx+edx]  ; eax = ebx + edx
   mov  edx,ebx
   mov  ebx,eax
loop   L1

   ret
FibonacciByRegLEA ENDP

This statement is encoded as three bytes implemented in machine code without an addition operation explicitly at runtime:

000000CE  8D 04 1A      lea eax, DWORD PTR [ebx+edx]  ; eax = ebx + edx

This example doesn’t make too much performance difference, compared to FibonacciByRegMOV. But is enough as an implementation demo.

12. If you can use an operator, don’t use an instruction

For an array defined as:

.data
   Ary1 DWORD 20 DUP(?)

If you want to traverse it from the second element to the middle one, you might think of this like in other language:

mov esi, OFFSET Ary1
add esi, TYPE DWORD    ; start at the second value 
mov ecx LENGTHOF Ary1  ; total number of values
sub ecx, 1
div ecx, 2             ; set loop counter in half
L1:
   ; do traversing
Loop L1

Remember that ADD, SUB, and DIV are dynamic behavior at runtime. If you know values in advance, they are unnecessary to calculate at runtime, instead, apply operators in assembling:

mov esi, OFFSET Ary1 + TYPE DWORD   ; start at the second
mov ecx (LENGTHOF Ary1 -1)/2        ; set loop counter
L1:
   ; do traversing
Loop L1

This saves three instructions in the code segment at runtime. Next, let’s save memory in the data segment.

13. If you can use a symbolic constant, don’t use a variable

Like operators, all directives are processed at assembling time. A variable consumes memory and has to be accessed at runtime. As for the last Ary1, you may want to remember its size in byte and the number of elements like this:

.data
   Ary1 DWORD 20 DUP(?)
   arySizeInByte DWORD ($ - Ary1)  ; 80
   aryLength DWORD LENGTHOF Ary1   ; 20

It is correct but not preferred because of using two variables. Why not simply make them symbolic constants to save the memory of two DWORD?

.data
   Ary1 DWORD 20 DUP(?)
   arySizeInByte = ($ - Ary1)      ; 80
   aryLength EQU LENGTHOF Ary1     ; 20

Using either equal sign or EQU directive is fine. The constant is just a replacement during code preprocessing.

14. Generating the memory block in macro

For an amount of data to initialize, if you already know the logic how to create, you can use macro to generate memory blocks in assembling, instead of at runtime. The following macro creates all 47 Fibonacci numbers in a DWORD array named FibArray:

.data
val1 = 1
val2 = 1
val3 = val1 + val2 

FibArray LABEL DWORD
DWORD val1                ; first two values
DWORD val2
WHILE val3 LT 0FFFFFFFFh  ; less than 4-billion, 32-bit
   DWORD val3             ; generate unnamed memory data
   val1 = val2
   val2 = val3
   val3 = val1 + val2
ENDM

As macro goes to the assembler to be processed statically, this saves considerable initializations at runtime, as opposed to FibonacciByXXX mentioned before.

For more about macro in MASM, see my article Something You May Not Know About the Macro in MASM [4]. I also made a reverse engineering for the switch statement in VC++ compiler implementation. Interestingly, under some condition the switch statement chooses the binary search but without exposing the prerequisite of a sort implementation at runtime. It’s reasonable to think of the preprocessor that does the sorting with all known case values in compilation. The static sorting behavior (as opposed to dynamic behavior at runtime), could be implemented with a macro procedure, directives and operators. For details, please see Something You May Not Know About the Switch Statement in C/C++ [5].

About loop design

Almost every language provides an unconditional jump like GOTO, but most of us rarely use it based on software engineering principles. Instead, we use others like break and continue. While in assembly language, we rely more on jumps either conditional or unconditional to make control workflow more freely. In the following sections, I list some ill-coded patterns.

15. Encapsulating all loop logic in the loop body

To construct a loop, try to make all your loop contents in the loop body. Don’t jump out to do something and then jump back into the loop. The example here is to traverse a one-dimensional integer array. If find an odd number, increment it, else do nothing.

Two unclear solutions with the correct result would be possibly like:

   mov ecx, LENGTHOF array
   xor esi, esi
L1: 
   test array[esi], 1
   jnz ODD
PASS:
   add esi, TYPE DWORD
loop L1
   jmp DONE

ODD: 
  inc array[esi]
jmp PASS
DONE:
   mov ecx, LENGTHOF array
   xor esi, esi
   jmp L1

ODD: 
  inc array[esi]
jmp PASS

L1: 
   test array[esi], 1
   jnz ODD
PASS:
   add esi, TYPE DWORD
loop L1

However, they both do incrementing outside and then jump back. They make a check in the loop but the left does incrementing after the loop and the right does before the loop. For a simple logic, you may not think like this; while for a complicated problem, assembly language could lead astray to produce such a spaghetti pattern. The following is a good one, which encapsulates all logic in the loop body, concise, readable, maintainable, and efficient.

   mov ecx, LENGTHOF array
   xor esi, esi
L1: 
   test array[esi], 1
   jz PASS
   inc array[esi]
PASS:
   add esi, TYPE DWORD
loop L1

16. Loop entrance and exit

Usually preferred is a loop with one entrance and one exit. But if necessary, two or more conditional exits are fine as shown in Search2DAry with found and not-found results.

The following is a bad pattern of two-entrance, where one gets into START via initialization and another directly goes to MIDDLE. Such a code is pretty hard to understand. Need to reorganize or refactor the loop logic.

   ; do something
   je MIDDLE

   ; loop initialization
START: 
   ; do something

MIDDLE:
   ; do something
loop START

The following is a bad pattern of two-loop ends, where some logic gets out of the first loop end while the other exits at the second. Such a code is quite confusing. Try to reconsider with a label jumping to maintain one loop end.

   ; loop initialization
START2: 
   ; do something
   je NEXT
   ; do something
loop START2
   jmp DONE

NEXT:
   ; do something
loop START2
DONE:

17. Don’t change ECX in the loop body

The register ECX acts as a loop counter and its value is implicitly decremented when using the LOOP instruction. You can read ECX and make use of its value in iteration. As see in Search2DAry in the previous section, we compare the indirect operand [ESI+ECX-1] with AL. But never try to change the loop counter within the loop body that makes code hard to understand and hard to debug. A good practice is to think of the loop counter ECX as read-only.

   ; do initialization
   mov ecx, 10
L1: 
   ; do something
   mov eax, ecx                      ; fine
   mov ebx, [esi +ecx *TYPE DWORD]   ; fine
   mov ecx, edx                      ; not good 
   inc ecx                           ; not good
   ; do something
loop L1

18. When jump backward…

Besides the LOOP instruction, assembly language programming can heavily rely on conditional or unconditional jumps to create a loop when the count is not determined before the loop. Theoretically, for a backward jump, the workflow might be considered as a loop. Assume that jx and jy are desired jump or LOOP instructions. The following backward jy L2 nested in the jx L1 is probably thought of as an inner loop.

; loop initialization 
L1: 
   ; do something
 L2: 
   ; do something
 jy L2
   ; do something
jx L1

To have selection logic of if-then-else, it’s reasonable to use a foreword jump like this as branching in the jx L1iteration:

; loop initialization 
L1: 
   ; do something
 jy TrueLogic
   ; do something for false
   jmp DONE
 TrueLogic:
   ; do something for true
DONE:
   ; do something
jx L1

About procedure

Similar to functions in C/C++, we talk about some basics in assembly language’s procedure.

19. Making a clear calling interface

When design a procedure, we hope to make it as reusable as possible. Make it perform only one task without others like I/O. The procedure’s caller should take the responsibility to do input and putout. The caller should communicate with the procedure only by arguments and parameters. The procedure should only use parameters in its logic without referring outside definitions, without any:

  • Global variable and array
  • Global symbolic constant

Because implementing with such a definition makes your procedure un-reusable.

Recalling previous five FibonacciByXXX procedures, we use register ECX as both argument and parameter with the return value in EAX to make a clear calling interface:

;------------------------------------------------------------
FibonacciByXXX 
; Receives: ECX as input n 
; Returns: EAX, nth Fibonacci number
;------------------------------------------------------------

Now the caller can do like

; Read user’s input n and save in ECX
call FibonacciByXXX
; Output or process the nth Fibonacci number in EAX

To illustrate as a second example, let’s take a look again at calling Search2DAry in the previous section. The register arguments ESI and EAX are prepared so that the implementation of Search2DAry doesn’t directly refer to the global array, ary2D.

... ...
NUM_COL = 5
NUM_ROW = 2

.code
main PROC
   mov esi, OFFSET ary2D
   mov eax, 31h 
   call Search2DAry
; See eax for search result
   exit
main ENDP

;------------------------------------------------------------
Search2DAry PROC
; Receives: EAX, a byte value to search a 2-dimensional array
;           ESI, an address to the 2-dimensional array
; Returns: EAX, 1 if found, 0 if not found
;------------------------------------------------------------
   mov  ecx,NUM_ROW        ; outer loop count
... ...
   mov  ecx,NUM_COL        ; inner loop counter
... ...

Unfortunately, the weakness is its implementation still using two global constants NUM_ROW and NUM_COL that makes it not being called elsewhere. To improve, supplying other two register arguments would be an obvious way, or see the next section.

20. INVOKE vs. CALL

Besides the CALL instruction from Intel, MASM provides the 32-bit INVOKE directive to make a procedure call easier. For the CALL instruction, you only can use registers as argument/parameter pair in calling interface as shown above. The problem is that the number of registers is limited. All registers are global and you probably have to save registers before calling and restore after calling. The INVOKE directive gives the form of a procedure with a parameter-list, as you experienced in high level languages.

When consider Search2DAry with a parameter-list without referring the global constants NUM_ROW and NUM_COL, we can have its prototype like this

;---------------------------------------------------------------------
Search2DAry PROTO, pAry2D: PTR BYTE, val: BYTE, nRow: WORD, nCol: WORD 
; Receives: pAry2D, an address to the 2-dimensional array
;           val, a byte value to search a 2-dimensional array 
;           nRow, the number of rows 
;           nCol, the number of columns
; Returns: EAX, 1 if found, 0 if not found
;---------------------------------------------------------------------

Again, as an exercise, you can try to implement this for a fix. Now you just do

INVOKE Search2DAry, ary2D, 31h, NUM_ROW, NUM_COL
; See eax for search result

Likewise, to construct a parameter-list procedure, you still need to follow the rule without referring global variables and constants. Besides, also attention to:

  • The entire calling interface should only go through the parameter list without referring any register values set outside the procedure.

21. Call-by-Value vs. Call-by-Reference

Also be aware of that a parameter-list should not be too long. If so, use an object parameter instead. Suppose that you fully understood the function concept, call-by-value and call-by-reference in high level languages. By learning the stack frame in assembly language, you understand more about the low-level function calling mechanism. Usually for an object argument, we prefer passing a reference, an object address, rather than the whole object copied on the stack memory.

To demonstrate this, let’s create a procedure to write month, day, and year from an object of the Win32 SYSTEMTIME structure.

The following is the version of call-by-value, where we use the dot operator to retrieve individual WORD field members from the DateTime object and extend their 16-bit values to 32-bit EAX:

;--------------------------------------------------------
WriteDateByVal PROC, DateTime:SYSTEMTIME
; Receives: DateTime, an object of SYSTEMTIME
;--------------------------------------------------------
   movzx eax, DateTime.wMonth
   ; output eax as month
   ; output a separator like '/' 
   movzx eax, DateTime.wDay
   ; output eax as day
   ; output a separator like '/' 
   movzx eax, DateTime.wYear
   ; output eax as year
   ; make a newline
   ret
WriteDateByVal ENDP

The version of call-by-reference is not so straight with an object address received. Not like the arrow ->, pointer operator in C/C++, we have to save the pointer (address) value in a 32-bit register like ESI. By using ESI as an indirect operand, we must cast its memory back to the SYSTEMTIME type. Then we can get the object members with the dot:

;--------------------------------------------------------
WriteDateByRef PROC, datetimePtr: PTR SYSTEMTIME
; Receives: DateTime, an address of SYSTEMTIME object
;--------------------------------------------------------
   mov esi, datetimePtr
   movzx eax, (SYSTEMTIME PTR [esi]).wMonth
   ; output eax as month
   ; output a separator like '/'
   movzx eax, (SYSTEMTIME PTR [esi]).wDay
   ; output eax as day
   ; output a separator like '/' 
   movzx eax, (SYSTEMTIME PTR [esi]).wYear
   ; output eax as year
   ; make a newline
   ret
WriteDateByRef ENDP

You can watch the stack frame of argument passed for two versions at runtime. For WriteDateByVal, eight WORD members are copied on the stack and consume sixteen bytes, while for WriteDateByRef, only need four bytes as a 32-bit address. It will make a big difference for a big structure object, though.

22. Avoid multiple RET

To construct a procedure, it’s ideal to make all your logics within the procedure body. Preferred is a procedure with one entrance and one exit. Since in assembly language programming, a procedure name is directly represented by a memory address, as well as any labels. Thus directly jumping to a label or a procedure without using CALL or INVOKE would be possible. Since such an abnormal entry would be quite rare, I am not to going to mention here.

Although multiple returns are sometimes used in other language examples, I don’t encourage such a pattern in assembly code. Multiple RET instructions could make your logic not easy to understand and debug. The following code on the left is such an example in branching. Instead, on the right, we have a label QUIT at the end and jump there making a single exit, where probably do common chaos to avoid repeated code.

MultiRetEx PROC
   ; do something 
   jx NEXTx
   ; do something
   ret

NEXTx: 
   ; do something
   jy NEXTy
   ; do something
   ret

NEXTy: 
   ; do something
   ret
MultiRetEx ENDP
SingleRetEx PROC
   ; do something 
   jx NEXTx
   ; do something
   jmp QUIT
NEXTx: 
   ; do something
   jy NEXTy
   ; do something
   jmp QUIT
NEXTy: 
   ; do something
QUIT:
   ; do common things
   ret
SingleRetEx ENDP

Object data members

Similar to above SYSTEMTIME structure, we can also create our own type or a nested:

Rectangle STRUCT
   UpperLeft COORD <>
   LowerRight COORD <>
Rectangle ENDS

.data
rect Rectangle { {10,20}, {30,50} }

The Rectangle type contains two COORD members, UpperLeft and LowerRight. The Win32 COORD contains two WORD (SHORT), X and Y. Obviously, we can access the object rect’s data members with the dot operator from either direct or indirect operand like this

; directly access
mov rect.UpperLeft.X, 11

; cast indirect operand to access
mov esi,OFFSET rect
mov (Rectangle PTR [esi]).UpperLeft.Y, 22

; use the OFFSET operator for embedded members
mov esi,OFFSET rect.LowerRight
mov (COORD PTR [esi]).X, 33
mov esi,OFFSET rect.LowerRight.Y
mov WORD PTR [esi], 55

By using the OFFSET operator, we access different data member values with different type casts. Recall that any operator is processed in assembling at static time. What if we want to retrieve a data member’s address (not value) at runtime?

23. Indirect operand and LEA

For an indirect operand pointing to an object, you can’t use the OFFSET operator to get the member’s address, because OFFSET only can take an address of a variable defined in the data segment.

There could be a scenario that we have to pass an object reference argument to a procedure like WriteDateByRef in the previous section, but want to retrieve its member’s address (not value). Still use the above rect object for an example. The following second use of OFFSET is not valid in assembling:

mov esi,OFFSET rect
mov edi, OFFSET (Rectangle PTR [esi]).LowerRight

Let’s ask for help from the LEA instruction that you have seen in FibonacciByRegLEA in the previous section. The LEA instruction calculates and loads the effective address of a memory operand. Similar to the OFFSEToperator, except that only LEA can obtain an address calculated at runtime:

mov esi,OFFSET rect
lea edi, (Rectangle PTR [esi]).LowerRight
mov ebx, OFFSET rect.LowerRight

lea edi, (Rectangle PTR [esi]).UpperLeft.Y
mov ebx, OFFSET rect.UpperLeft.Y

mov esi,OFFSET rect.UpperLeft
lea edi, (COORD PTR [esi]).Y

I purposely have EBX here to get an address statically and you can verify the same address in EDI that is loaded dynamically from the indirect operand ESI at runtime.

About system I/O

From Computer Memory Basics, we know that I/O operations from the operating system are quite slow. Input and output are usually in the measurement of milliseconds, compared with register and memory in nanoseconds or microseconds. To be more efficient, trying to reduce system API calls is a nice consideration. Here I mean Win32 API call. For details about the Win32 functions mentioned in the following, please refer to MSDN to understand.

24. Reducing system I/O API calls

An example is to output 20 lines of 50 random characters with random colors as below:

We definitely can generate one character to output a time, by using SetConsoleTextAttribute and WriteConsole. Simply set its color by

INVOKE SetConsoleTextAttribute, consoleOutHandle, wAttributes

Then write that character by

INVOKE WriteConsole,
   consoleOutHandle,    ; console output handle
   OFFSET buffer,       ; points to string
   1,                   ; string length
   OFFSET bytesWritten, ; returns number of bytes written
   0

When write 50 characters, make a new line. So we can create a nested iteration, the outer loop for 20 rows and the inner loop for 50 columns. As 50 by 20, we call these two console output functions 1000 times.

However, another pair of API functions can be more efficient, by writing 50 characters in a row and setting their colors once a time. They are WriteConsoleOutputAttribute and WriteConsoleOutputCharacter. To make use of them, let’s create two procedures:

;-----------------------------------------------------------------------
ChooseColor PROC
; Selects a color with 50% probability of red, 25% green and 25% yellow
; Receives: nothing
; Returns:  AX = randomly selected color

;-----------------------------------------------------------------------
ChooseCharacter PROC
; Randomly selects an ASCII character, from ASCII code 20h to 07Ah
; Receives: nothing
; Returns:  AL = randomly selected character

We call them in a loop to prepare a WORD array bufColor and a BYTE array bufChar for all 50 characters selected. Now we can write the 50 random characters per line with two calls here:

INVOKE WriteConsoleOutputAttribute, 
      outHandle, 
      ADDR bufColor, 
      MAXCOL, 
      xyPos, 
      ADDR cellsWritten

INVOKE WriteConsoleOutputCharacter, 
      outHandle, 
      ADDR bufChar, 
      MAXCOL, 
      xyPos, 
      ADDR cellsWritten

Besides bufColor and bufChar, we define MAXCOL = 50 and the COORD type xyPos so that xyPos.y is incremented each row in a single loop of 20 rows. Totally we only call these two APIs 20 times.

About PTR operator

MASM provides the operator PTR that is similar to the pointer * used in C/C++. The following is the PTRspecification:

  • type PTR expression
    Forces the expression to be treated as having the specified type.
  • [[ distance ]] PTR type
    Specifies a pointer to type.

This means that two usages are available, such as BYTE PTR or PTR BYTE. Let’s discuss how to use them.

25. Defining a pointer, cast and dereference

The following C/C++ code demonstrates which type of Endian is used in your system, little endian or big endian? As an integer type takes four bytes, it makes a pointer type cast from the array name fourBytes, a charaddress, to an unsigned int address. Then it displays the integer result by dereferencing the unsigned intpointer.

int main()
{
   unsigned char fourBytes[] = { 0x12, 0x34, 0x56, 0x78 };
   // Cast the memory pointed by the array name fourBytes, to unsigned int address
   unsigned int *ptr = (unsigned int *)fourBytes;
   printf("1. Directly Cast: n is %Xh\n", *ptr);
   return 0;
}

As expected in x86 Intel based system, this verifies the little endian by showing 78563412 in hexadecimal. We can do the same thing in assembly language with DWORD PTR, which is just similar to an address casting to 4-byte DWORD, the unsigned int type.

.data
fourBytes BYTE 12h,34h,56h,78h

.code
mov eax, DWORD PTR fourBytes		; EAX = 78563412h

There is no explicit dereference here, since DWORD PTR combines four bytes into a DWORD memory and lets MOVretrieve it as a direct operand to EAX. This could be considered equivalent to the (unsigned int *) cast.

Now let’s do another way by using PTR DWORD. Again, with the same logic above, this time we define a DWORDpointer type first with TYPEDEF:

DWORD_POINTER TYPEDEF PTR DWORD

This could be considered equivalent to defining the pointer type as unsigned int *. Then in the following data segment, the address variable dwPtr takes over the fourBytes memory. Finally in code, EBX holds this address as an indirect operand and makes an explicit dereference here to get its DWORD value to EAX.

.data
fourBytes BYTE 12h,34h,56h,78h
dwPtr DWORD_POINTER fourBytes

.code
mov ebx, dwPtr       ; Get DWORD address		
mov eax, [ebx]       ; Dereference, EAX = 78563412h

To summarize, PTR DWORD indicates a DWORD address type to define(declare) a variable like a pointer type. While DWORD PTR indicates the memory pointed by a DWORD address like a type cast.

26. Using PTR in a procedure

To define a procedure with a parameter list, you might want to use PTR in both ways. The following is such an example to increment each element in a DWORD array:

;---------------------------------------------------------
IncrementArray PROC, pAry:PTR DWORD, count:DWORD
; Receives: pAry  - pointer to a DWORD array
;           count - the array count
; Returns:  pAry, every vlues in pAry incremented
;---------------------------------------------------------
   mov edi,pAry
   mov ecx,count                      

 L1:
   inc DWORD PTR [edi]
   add edi, TYPE DWORD
 loop L1
   ret
IncrementArray ENDP

As the first parameter pAry is a DWORD address, so PTR DWORD is used as a parameter type. In the procedure, when incrementing a value pointed by the indirect operand EDI, you must tell the system what the type(size) of that memory is by using DWORD PTR.

Another example is the earlier mentioned WriteDateByRef, where SYSTEMTIME is a Windows defined structure type.

;--------------------------------------------------------
WriteDateByRef PROC, datetimePtr: PTR SYSTEMTIME
; Receives: DateTime, an address of SYSTEMTIME object
;--------------------------------------------------------
   mov esi, datetimePtr
   movzx eax, (SYSTEMTIME PTR [esi]).wMonth
  ... ...
   ret
WriteDateByRef ENDP

Likewise, we use PTR SYSTEMTIME as the parameter type to define datetimePtr. When ESI receives an address from datetimePtr, it has no knowledge about the memory type just like a void pointer in C/C++. We have to cast it as a SYSTEMTIME memory, so as to retrieve its data members.

Signed and Unsigned

In assembly language programming, you can define an integer variable as either signed as SBYTE, SWORD, and SDWORD, or unsigned as BYTE, WORD, and DWORD. The data ranges, for example of 8-bit, are

  • BYTE: 0 to 255 (00h to FFh), totally 256 numbers
  • SBYTE: half negatives, -128 to -1 (80h to FFh), half positives, 0 to 127 (00h to 7Fh)

Based on the hardware point of view, all CPU instructions operate exactly the same on signed and unsigned integers, because the CPU cannot distinguish between signed and unsigned. For example, when define

.data
   bVal   BYTE   255
   sbVal  SBYTR  -1

Both of them have the 8-bit binary FFh saved in memory or moved to a register. You, as a programmer, are solely responsible for using the correct data type with an instruction and are able to explain a results from the flags affected:

  • The carry flag CF for unsigned integers
  • The overflow flag OF for signed integers

The following are usually several tricks or pitfalls.

27. Comparison with conditional jumps

Let’s check the following code to see which label it jumps:

mov   eax, -1
cmp   eax, 1
ja    L1
jmp   L2

As we know, CMP follows the same logic as SUB while non-destructive to the destination operand. Using JAmeans considering unsigned comparison, where the destination EAX is FFh, i.e. 255, while the source is 1. Certainly 255 is bigger than 1, so that makes it jump to L1. Thus, any unsigned comparisons such as JA, JB, JAE, JNA, etc. can be remembered as A(Above) or B(Below). An unsigned comparison is determined by CF and the zero flag ZF as shown in the following examples:

CMP if Destination Source ZF(ZR) CF(CY)
Destination<Source 1 2 0 1
Destination>Source 2 1 0 0
Destination=Source 1 1 1 0

Now let’s take a look at signed comparison with the following code to see where it jumps:

mov   eax, -1
cmp   eax, 1
jg    L1
jmp   L2

Only difference is JG here instead of JA. Using JG means considering signed comparison, where the destination EAX is FFh, i.e. -1, while the source is 1. Certainly -1 is smaller than 1, so that makes JMP to L2. Likewise, any signed comparisons such as JG, JL, JGE, JNG, etc. can be thought of as G(Greater) or L(Less). A signed comparison is determined by OF and the sign flag SF as shown in the following examples:

CMP if Destination Source SF(PL) OF(OV)
Destination<Source: (SF != OF) -2 127 0 1
-2 1 1 0
Destination>Source: (SF == OF) 127 1 0 0
127 -1 1 1
Destination = Source 1 1 ZF=1

28. When CBW, CWD, or CDQ mistakenly meets DIV…

As we know, the DIV instruction is for unsigned to perform 8-bit, 16-bit, or 32-bit integer division with the dividend AX, DX:AX, or EDX:EAX respectively. As for unsigned, you have to clear the upper half by zeroing AH, DX, or EDX before using DIV. But when perform signed division with IDIV, the sign extension CBW, CWD, and CDQ are provided to extend the upper half before using IDIV.

For a positive integer, if its highest bit (sign bit) is zero, there is no difference to manually clear the upper part of a dividend or mistakenly use a sign extension as shown in the following example:

mov eax,1002h
cdq
mov ebx,10h
div ebx  ; Quotient EAX = 00000100h, Remainder EDX = 2

This is fine because 1000h is a small positive and CDQ makes EDX zero, the same as directly clearing EDX. So if your value is positive and its highest bit is zero, using CDQ and

XOR EDX, EDX

are exactly the same.

However, it doesn’t mean that you can always use CDQ/CWD/CBW with DIV when perform a positive division. For an example of 8-bit, 129/2, expecting quotient 64 and remainder 1. But, if you make this

mov  al, 129
cbw             ; Extend AL to AH as negative AX = FF81h
mov  bl,2
div  bl         ; Unsigned DIV, Quotient should be 7FC0 over size of AL

Try above in debug to see how integer division overflow happens as a result. If really want to make it correct as unsigned DIV, you must:

mov  al, 129
XOR  ah, ah     ; extend AL to AH as positive
mov  bl,2
div  bl         ; Quotient AL = 40h,  Remainder AH = 1

On the other side, if really want to use CBW, it means that you perform a signed division. Then you must use IDIV:

mov  al, 129    ; 81h (-127d)
cbw             ; Extend AL to AH as negative AX = FF81h
mov  bl,2
idiv bl         ; Quotient AL = C1h (-63d), Remainder AH = FFh (-1)

As seen here, 81h in signed byte is decimal -127 so that signed IDIV gives the correct quotient and remainder as above

29. Why 255-1 and 255+(-1) affect CF differently?

To talk about the carry flag CF, let’s take the following two arithmetic calculations:

mov al, 255
sub al, 1      ; AL = FE  CF = 0

mov bl, 255
add bl, -1     ; BL = FE  CF = 1

From a human being’s point of view, they do exactly the same operation, 255 minus 1 with the result 254 (FEh). Likewise, based on the hardware point, for either calculation, the CPU does the same operation by representing -1 as a two’s complement FFh and then add it to 255. Now 255 is FFh and the binary format of -1 is also FFh. This is how it has been calculated:

   1111 1111
+  1111 1111
-------------
   1111 1110

Remember? A CPU operates exactly the same on signed and unsigned because it cannot distinguish them. A programmer should be able to explain the behavior by the flag affected. Since we talk about the CF, it means we consider two calculations as unsigned. The key information is that -1 is FFh and then 255 in decimal. So the logic interpretation of CF is

  • For sub al, 1, it means 255 minus 1 to result in 254, without need of a borrow, so CF = 0
  • For add bl, -1, it seems that 255 plus 255 is resulted in 510, but with a carry 1,0000,0000b (256) out, 254 is a remainder left in byte, so CF = 1

From hardware implementation, CF depends on which instruction used, ADD or SUB. Here MSB (Most Significant Bit) is the highest bit.

  • For ADD instruction, add bl, -1, directly use the carry out of the MSB, so CF = 1
  • For SUB instruction, sub al, 1, must INVERT the carry out of the MSB, so CF = 0

30. How to determine OF?

Now let’s see the overflow flag OF, still with above two arithmetic calculations as this:

mov al, 255
sub al, 1      ; AL = FE  OF = 0

mov bl, 255
add bl, -1     ; BL = FE  OF = 0

Both of them are not overflow, so OF = 0. We can have two ways to determine OF, the logic rule and hardware implementation.

Logic viewpoint: The overflow flag is only set, OF = 1, when

  • Two positive operands are added and their sum is negative
  • Two negative operands are added and their sum is positive

For signed, 255 is -1 (FFh). The flag OF doesn’t care about ADD or SUB. Our two examples just do -1 plus -1 with the result -2. Thus, two negatives are added with the sum still negative, so OF = 0.

Hardware implementation: For non-zero operands,

  • OF = (carry out of the MSB) XOR (carry into the MSB)

As seen our calculation again:

   1111 1111
+  1111 1111
-------------
   1111 1110

The carry out of the MSB is 1 and the carry into the MSB is also 1. Then OF = (1 XOR 1) = 0

To practice more, the following table enumerates different test cases for your understanding:

Ambiguous «LOCAL» directive

As mentioned previously, the PTR operator has two usages such as DWORD PTR and PTR DWORD. But MASM provides another confused directive LOCAL, that is ambiguous depending on the context, where to use with exactly the same reserved word. The following is the specification from MSDN:

        LOCAL localname [[, localname]]…
LOCAL label [[ [count ] ]] [[:type]] [[, label [[ [count] ]] [[type]]]]…

  • In the first directive, within a macro, LOCAL defines labels that are unique to each instance of the macro.
  • In the second directive, within a procedure definition (PROC), LOCAL creates stack-based variables that exist for the duration of the procedure. The label may be a simple variable or an array containing count elements.

This specification is not clear enough to understand. In this section, I’ll expose the essential difference in between and show two example using the LOCAL directive, one in a procedure and the other in a macro. As for your familiarity, both examples calculate the nth Fibonacci number as early FibonacciByMemory. The main point delivered here is:

  • The variables declared by LOCAL in a macro are NOT local to the macro. They are system generated global variables on the data segment to resolve redefinition.
  • The variables created by LOCAL in a procedure are really local variables allocated on the stack frame with the lifecycle only during the procedure.

For the basic concepts and implementations of data segment and stack frame, please take a look at some textbook or MASM manual that could be worthy of several chapters without being talked here.

31. When LOCAL used in a procedure

The following is a procedure with a parameter n to calculate nth Fibonacci number returned in EAX. I let the loop counter ECX take over the parameter n. Please compare it with FibonacciByMemory. The logic is the same with only difference of using the local variables pre and cur here, instead of global variables previous and currentin FibonacciByMemory.

;------------------------------------------------------------
FibonacciByLocalVariable PROC USES ecx edx, n:DWORD 
; Receives: Input n
; Returns: EAX, nth Fibonacci number
;------------------------------------------------------------
LOCAL pre, cur :DWORD

   mov   ecx,n
   mov   eax,1         
   mov   pre,0         
   mov   cur,0         
L1:
   add eax, pre      ; eax = current + previous     
   mov edx, cur 
   mov pre, edx
   mov cur, eax
 loop   L1

   ret
FibonacciByLocalVariable ENDP

The following is the code generated from the VS Disassembly window at runtime. As you can see, each line of assembly source is translated into machine code with the parameter n and two local variables created on the stack frame, referenced by EBP:

   231: ;------------------------------------------------------------
   232: FibonacciByLocalVariable PROC USES ecx edx, n:DWORD 
011713F4 55                   push        ebp  
011713F5 8B EC                mov         ebp,esp  
011713F7 83 C4 F8             add         esp,0FFFFFFF8h  
011713FA 51                   push        ecx  
011713FB 52                   push        edx  
   233: ; Receives: Input n
   234: ; Returns: EAX, nth Fibonacci number
   235: ;------------------------------------------------------------
   236: LOCAL pre, cur :DWORD
   237: 
   238:    mov   ecx,n
011713FC 8B 4D 08             mov         ecx,dword ptr [ebp+8]  
   239:    mov   eax,1         
011713FF B8 01 00 00 00       mov         eax,1  
   240:    mov   pre,0         
01171404 C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0  
   241:    mov   cur,0         
0117140B C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0  
   242: L1:
   243:    add eax,pre      ; eax = current + previous     
01171412 03 45 FC             add         eax,dword ptr [ebp-4]  
   244:    mov EDX, cur 
01171415 8B 55 F8             mov         edx,dword ptr [ebp-8]  
   245:    mov pre, EDX
01171418 89 55 FC             mov         dword ptr [ebp-4],edx  
   246:    mov cur, eax
0117141B 89 45 F8             mov         dword ptr [ebp-8],eax  
   247:    loop   L1
0117141E E2 F2                loop        01171412  
   248: 
   249:    ret
01171420 5A                   pop         edx  
01171421 59                   pop         ecx  
01171422 C9                   leave  
01171423 C2 04 00             ret         4  
   250: FibonacciByLocalVariable ENDP

When FibonacciByLocalVariable running, the stack frame can be seen as below:

Obviously, the parameter n is at EBP+8. This

add esp, 0FFFFFFF8h

just means

sub esp, 08h

moving the stack pointer ESP down eight bytes for two DWORD creation of pre and cur. Finally the LEAVEinstruction implicitly does

mov esp, ebp
pop ebp

that moves EBP back to ESP releasing the local variables pre and cur. And this releases n, at EBP+8, for STD calling convention:

ret 4

32. When LOCAL used in a macro

To have a macro implementation, I almost copy the same code from FibonacciByLocalVariable. Since no USES for a macro, I manually use PUSH/POP for ECX and EDX. Also without a stack frame, I have to create global variables mPre and mCur on the data segment. The mFibonacciByMacro can be like this:

;------------------------------------------------------------
mFibonacciByMacro MACRO n
; Receives: Input n 
; Returns: EAX, nth Fibonacci number
;------------------------------------------------------------
LOCAL mPre, mCur, mL
.data
   mPre DWORD ?
   mCur DWORD ?

.code
   push ecx
   push edx

   mov   ecx,n
   mov   eax,1         
   mov   mPre,0         
   mov   mCur,0         
mL:
   add  eax, mPre      ; eax = current + previous     
   mov  edx, mCur 
   mov  mPre, edx
   mov  mCur, eax
   loop   mL

   pop edx
   pop ecx
ENDM

If you just want to call mFibonacciByMacro once, for example

mFibonacciByMacro 12

You don’t need LOCAL here. Let’s simply comment it out:

; LOCAL mPre, mCur, mL

mFibonacciByMacro accepts the argument 12 and replace n with 12. This works fine with the following listing MASM generated:

              mFibonacciByMacro 12
0000018C           1   .data
0000018C 00000000        1      mPre DWORD ?
00000190 00000000        1      mCur DWORD ?
00000000           1   .code
00000000  51           1      push ecx
00000001  52           1      push edx
00000002  B9 0000000C       1      mov   ecx,12
00000007  B8 00000001       1      mov   eax,1
0000000C  C7 05 0000018C R  1      mov   mPre,0
     00000000
00000016  C7 05 00000190 R  1      mov   mCur,0
     00000000
00000020           1   mL:
00000020  03 05 0000018C R  1      add  eax,mPre      ; eax = current + previous
00000026  8B 15 00000190 R  1      mov edx, mCur
0000002C  89 15 0000018C R  1      mov mPre, edx
00000032  A3 00000190 R     1      mov mCur, eax
00000037  E2 E7        1      loop   mL
00000039  5A           1      pop edx
0000003A  59           1      pop ecx

Nothing changed from the original code with just a substitution of 12. The variables mPre and mCur are visible explicitly. Now let’s call it twice, like

mFibonacciByMacro 12
mFibonacciByMacro 13

This is still fine for the first mFibonacciByMacro 12 but secondly, causes three redefinitions in preprocessing mFibonacciByMacro 13. Not only are data labels, i.e., variables mPre and mCur, but also complained is the code label mL. This is because in assembly code, each label is actually a memory address and the second label of any mPre, mCur, or mL should take another memory, rather than defining an already created one:

               mFibonacciByMacro 12
 0000018C           1   .data
 0000018C 00000000        1      mPre DWORD ?
 00000190 00000000        1      mCur DWORD ?
 00000000           1   .code
 00000000  51           1      push ecx
 00000001  52           1      push edx
 00000002  B9 0000000C       1      mov   ecx,12
 00000007  B8 00000001       1      mov   eax,1         
 0000000C  C7 05 0000018C R  1      mov   mPre,0         
      00000000
 00000016  C7 05 00000190 R  1      mov   mCur,0         
      00000000
 00000020           1   mL:
 00000020  03 05 0000018C R  1      add  eax,mPre      ; eax = current + previous     
 00000026  8B 15 00000190 R  1      mov edx, mCur 
 0000002C  89 15 0000018C R  1      mov mPre, edx
 00000032  A3 00000190 R     1      mov mCur, eax
 00000037  E2 E7        1      loop   mL
 00000039  5A           1      pop edx
 0000003A  59           1      pop ecx

               mFibonacciByMacro 13
 00000194           1   .data
              1      mPre DWORD ?
FibTest.32.asm(83) : error A2005:symbol redefinition : mPre
 mFibonacciByMacro(6): Macro Called From
  FibTest.32.asm(83): Main Line Code
              1      mCur DWORD ?
FibTest.32.asm(83) : error A2005:symbol redefinition : mCur
 mFibonacciByMacro(7): Macro Called From
  FibTest.32.asm(83): Main Line Code
 0000003B           1   .code
 0000003B  51           1      push ecx
 0000003C  52           1      push edx
 0000003D  B9 0000000D       1      mov   ecx,13
 00000042  B8 00000001       1      mov   eax,1         
 00000047  C7 05 0000018C R  1      mov   mPre,0         
      00000000
 00000051  C7 05 00000190 R  1      mov   mCur,0         
      00000000
              1   mL:
FibTest.32.asm(83) : error A2005:symbol redefinition : mL
 mFibonacciByMacro(17): Macro Called From
  FibTest.32.asm(83): Main Line Code
 0000005B  03 05 0000018C R  1      add  eax,mPre      ; eax = current + previous     
 00000061  8B 15 00000190 R  1      mov edx, mCur 
 00000067  89 15 0000018C R  1      mov mPre, edx
 0000006D  A3 00000190 R     1      mov mCur, eax
 00000072  E2 AC        1      loop   mL
 00000074  5A           1      pop edx
 00000075  59           1      pop ecx

To rescue, let’s turn on this:

LOCAL mPre, mCur, mL

Again, running mFibonacciByMacro twice with 12 and 13, fine this time, we have:

              mFibonacciByMacro 12
0000018C           1   .data
0000018C 00000000        1      ??0000 DWORD ?
00000190 00000000        1      ??0001 DWORD ?
00000000           1   .code
00000000  51           1      push ecx
00000001  52           1      push edx
00000002  B9 0000000C       1      mov   ecx,12
00000007  B8 00000001       1      mov   eax,1
0000000C  C7 05 0000018C R  1      mov   ??0000,0
     00000000
00000016  C7 05 00000190 R  1      mov   ??0001,0
     00000000
00000020           1   ??0002:
00000020  03 05 0000018C R  1      add  eax,??0000      ; eax = current + previous
00000026  8B 15 00000190 R  1      mov edx, ??0001
0000002C  89 15 0000018C R  1      mov ??0000, edx
00000032  A3 00000190 R     1      mov ??0001, eax
00000037  E2 E7        1      loop   ??0002
00000039  5A           1      pop edx
0000003A  59           1      pop ecx

              mFibonacciByMacro 13
00000194           1   .data
00000194 00000000        1      ??0003 DWORD ?
00000198 00000000        1      ??0004 DWORD ?
0000003B           1   .code
0000003B  51           1      push ecx
0000003C  52           1      push edx
0000003D  B9 0000000D       1      mov   ecx,13
00000042  B8 00000001       1      mov   eax,1
00000047  C7 05 00000194 R  1      mov   ??0003,0
     00000000
00000051  C7 05 00000198 R  1      mov   ??0004,0
     00000000
0000005B           1   ??0005:
0000005B  03 05 00000194 R  1      add  eax,??0003      ; eax = current + previous
00000061  8B 15 00000198 R  1      mov edx, ??0004
00000067  89 15 00000194 R  1      mov ??0003, edx
0000006D  A3 00000198 R     1      mov ??0004, eax
00000072  E2 E7        1      loop   ??0005
00000074  5A           1      pop edx
00000075  59           1      pop ecx

Now the label names, mPre, mCur, and mL, are not visible. Instead, running the first of mFibonacciByMacro 12, the preprocessor generates three system labels ??0000, ??0001, and ??0002 for mPre, mCur, and mL. And for the second mFibonacciByMacro 13, we can find another three system generated labels ??0003, ??0004, and ??0005 for mPre, mCur, and mL. In this way, MASM resolves the redefinition issue in multiple macro executions. You must declare your labels with the LOCAL directive in a macro.

However, by the name LOCAL, the directive sounds misleading, because the system generated ??0000, ??0001, etc. are not limited to a macro’s context. They are really global in scope. To verify, I purposely initialize mPre and mCur as 2 and 3:

LOCAL mPre, mCur, mL
.data
   mPre DWORD 2
   mCur DWORD 3

Then simply try to retrieve the values from ??0000 and ??0001 even before calling two mFibonacciByMacro in code

mov esi, ??0000
mov edi, ??0001

mFibonacciByMacro 12
mFibonacciByMacro 13

To your surprise probably, when set a breakpoint, you can enter &??0000 into the VS debug Address box as a normal variable. As we can see here, the ??0000 memory address is 0x0116518C with DWORD values 2, 3, and so on. Such a ??0000 is allocated on the data segment together with other properly named variables, as shown string ASCII beside:

o summarize, the LOCAL directive declared in a macro is to prevent data/code labels from being globally redefined.

Further, as an interesting test question, think of the following multiple running of mFibonacciByMacro which is working fine without need of a LOCAL directive in mFibonacciByMacro. Why?

mov ecx, 2
L1:
   mFibonacciByMacro 12
loop L1

Summary

I talked so much about miscellaneous features in assembly language programming. Most of them are from our class teaching and assignment discussion [1]. The basic practices are presented here with short code snippets for better understanding without irrelevant details involved. The main purpose is to show assembly language specific ideas and methods with more strength than other languages.

As noticed, I haven’t given a complete test code that requires a programming environment with input and output. For an easy try, you can go [2] to download the Irvine32 library and setup your MASM programming environment with Visual Studio, while you have to learn a lot in advance to prepare yourself first. For example, the statement exit mentioned here in main is not an element in assembly language, but is defined as INVOKE ExitProcess,0 there.

Assembly language is notable for its one-to-one correspondence between an instruction and its machine code as shown in several listings here. Via assembly code, you can get closer to the heart of the machine, such as registers and memory. Assembly language programming often plays an important role in both academic study and industry development. I hope this article could serve as an useful reference for students and professionals as well.

Assembler & Win32

В отличие от программирования под DOS, где программы написанные на языках высокого уровня (ЯВУ) были мало похожи на свои аналоги, написанные на ассемблере, приложения под Win32 имеют гораздо больше общего. В первую очередь, это связано с тем, что обращение к сервису операционной системы в Windows осуществляется посредством вызова функций, а не прерываний, что было характерно для DOS. Здесь нет передачи параметров в регистрах при обращении к сервисным функциям и, соответственно, нет и множества результирующих значений возвращаемых в регистрах общего назначения и регистре флагов. Следовательно проще запомнить и использовать протоколы вызова функций системного сервиса. С другой стороны, в Win32 нельзя непосредственно работать с аппаратным уровнем, чем \»грешили\» программы для DOS. Вообще написание программ под Win32 стало значительно проще и это обусловлено следующими факторами:

отсутствие startup кода, характерного для приложений и динамических библиотек написанных под Windows 3.x;
гибкая система адресации к памяти: возможность обращаться к памяти через любой регистр общего назначения; \»отсутствие\» сегментных регистров;
доступность больших объёмов виртуальной памяти;
развитый сервис операционной системы, обилие функций, облегчающих разработку приложений;
многообразие и доступность средств создания интерфейса с пользователем (диалоги, меню и т.п.).
Современный ассемблер, к которому относится и TASM 5.0 фирмы Borland International Inc., в свою очередь, развивал средства, которые ранее были характерны только для ЯВУ. К таким средствам можно отнести макроопределение вызова процедур, возможность введения шаблонов процедур (описание прототипов) и даже объектно-ориентированные расширения. Однако, ассемблер сохранил и такой прекрасный инструмент, как макроопределения вводимые пользователем, полноценного аналога которому нет ни в одном ЯВУ.

Все эти факторы позволяют рассматривать ассемблер, как самостоятельный инструмент для написания приложений под платформы Win32 (Windows NT и Windows 95). Как иллюстрацию данного положения, рассмотрим простой пример приложения, работающего с диалоговым окном.

Пример 1. Программа работы с диалогом Файл, содержащий текст приложения, dlg.asm
IDEAL
P586
RADIX 16
MODEL FLAT
%NOINCL
%NOLIST
include \»winconst.inc\» ; API Win32 consts
include \»winptype.inc\» ; API Win32 functions prototype
include \»winprocs.inc\» ; API Win32 function
include \»resource.inc\» ; resource consts
MAX_USER_NAME = 20
DataSeg
szAppName db \’Demo 1\’, 0
szHello db \’Hello, \’
szUser db MAX_USER_NAME dup (0)
CodeSeg
Start: call GetModuleHandleA, 0
call DialogBoxParamA, eax, IDD_DIALOG, 0, offset DlgProc, 0
cmp eax,IDOK
jne bye
call MessageBoxA, 0, offset szHello, \\
offset szAppName, \\
MB_OK or MB_ICONINFORMATION
bye: call ExitProcess, 0
public stdcall DlgProc
proc DlgProc stdcall
arg @@hDlg :dword, @@iMsg :dword, @@wPar :dword, @@lPar :dword
mov eax,[@@iMsg] cmp eax,WM_INITDIALOG
je @@init
cmp eax,WM_COMMAND
jne @@ret_false
mov eax,[@@wPar] cmp eax,IDCANCEL
je @@cancel
cmp eax,IDOK
jne @@ret_false
call GetDlgItemTextA, [@@hDlg[, IDR_NAME, \\
offset szUser, MAX_USER_NAME
mov eax,IDOK
@@cancel: call EndDialog, [@@hDlg[, eax
@@ret_false: xor eax,eax
ret
@@init: call GetDlgItem, [@@hDlg], IDR_NAME
call SetFocus, eax
jmp @@ret_false
endp DlgProc
end Start
Файл ресурсов dlg.rc

#include \»resource.h\»
IDD_DIALOG DIALOGEX 0, 0, 187, 95
STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_CLIENTEDGE
CAPTION \»Dialog\»
FONT 8, \»MS Sans Serif\»
BEGIN
DEFPUSHBUTTON \»OK\»,IDOK,134,76,50,14
PUSHBUTTON \»Cancel\»,IDCANCEL,73,76,50,14
LTEXT \»Type your name\»,IDC_STATIC,4,36,52,8
EDITTEXT IDR_NAME,72,32,112,14,ES_AUTOHSCROLL
END
Остальные файлы из данного примера, приведены в приложении 1.

Сразу после метки Start, программа обращается к функции API Win32 GetModuleHandle для получения handle данного модуля (данный параметр чаще именуют как handle of instance). Получив handle, мы вызываем диалог, созданный либо вручную, либо с помощью какой-либо программы построителя ресурсов. Далее программа проверяет результат работы диалогового окна. Если пользователь вышел из диалога посредством нажатия клавиши OK, то приложение запускает MessageBox с текстом приветствия.

Диалоговая процедура обрабатывает следующие сообщения. При инициализации диалога (WM_INITDIALOG) она просит Windows установить фокус на поле ввода имени пользователя. Сообщение WM_COMMAND обрабатывается в таком порядке: делается проверка на код нажатия клавиши. Если была нажата клавиша OK, то пользовательский ввод копируется в переменную szValue, если же была нажата клавиша Cancel, то копирования не производится. Но и в том и другом случае вызывается функция

окончания диалога: EndDialog. Остальные сообщения в группе WM_COMMAND просто игнорируются, предоставляя Windows действовать по умолчанию.

Вы можете сравнить приведённую программу с аналогичной программой, написанной на ЯВУ, разница в написании будет незначительна. Очевидно те, кто писал приложения на ассемблере под Windows 3.x, отметят тот факт, что исчезла необходимость в сложном и громоздком startup коде. Теперь приложение выглядит более просто и естественно.

Пример 2. Динамическая библиотека
Написание динамических библиотек под Win32 также значительно упростилось, по сравнению с тем, как это делалось под Windows 3.x. Исчезла необходимость вставлять startup код, а использование четырёх событий инициализации/деинициализации на уровне процессов и потоков, кажется логичным.

Рассмотрим простой пример динамической библиотеки, в которой всего одна функция, преобразования целого числа в строку в шестнадцатеричной системе счисления. Файл mylib.asm

Ideal
P586
Radix 16
Model flat
DLL_PROCESS_ATTACH = 1

extrn GetVersion: proc

DataSeg
hInst dd 0
OSVer dw 0

CodeSeg
proc libEntry stdcall
arg @@hInst :dword, @@rsn :dword, @@rsrv :dword
cmp [@@rsn],DLL_PROCESS_ATTACH
jne @@1
call GetVersion
mov [OSVer],ax
mov eax,[@@hInst] mov [hInst],eax
@@1: mov eax,1
ret
endP libEntry

public stdcall Hex2Str
proc Hex2Str stdcall
arg @@num :dword, @@str :dword
uses ebx
mov eax,[@@num] mov ebx,[@@str] mov ecx,7
@@1: mov edx,eax
shr eax,4
and edx,0F
cmp edx,0A
jae @@2
add edx,\’0\’
jmp @@3
@@2: add edx,\’A\’ — 0A
@@3: mov [byte ebx + ecx],dl
dec ecx
jns @@1
mov [byte ebx + 8],0
ret
endp Hex2Str

end libEntry
Остальные файлы, которые необходимы для данного примера, можно найти в приложении 2.

Краткие комментарии к динамической библиотеке

Процедура libEntry является точкой входа в динамическую библиотеку, её не надо объявлять как экспортируемую, загрузчик сам определяет её местонахождение. LibEntry может вызываться в четырёх случаях:

при проецировании библиотеки в адресное пространство процесса (DLL_PROCESS_ATTACH);
при первом вызове библиотеки из потока (DLL_THREAD_ATTACH), например, с помощью функции LoadLibrary;
при выгрузке библиотеки потоком (DLL_THREAD_DETACH);
при выгрузке библиотеки из адресного пространства процесса (DLL_PROCESS_DETACH).
В нашем примере обрабатывается только первое из событий DLL_PROCESS_ATTACH. При обработке данного события библиотека запрашивает версию OS сохраняет её, а также свой handle of instance.

Библиотека содержит только одну экспортируемую функцию, которая собственно не требует пояснений. Вы, пожалуй, можете обратить внимание на то, как производится запись преобразованных значений. Интересна система адресации посредством двух регистров общего назначения: ebx + ecx, она позволяет нам использовать регистр ecx одновременно и как счётчик и как составную часть адреса.

Пример 3. Оконное приложение
Файл dmenu.asm

Ideal
P586
Radix 16
Model flat

struc WndClassEx
cbSize dd 0
style dd 0
lpfnWndProc dd 0
cbClsExtra dd 0
cbWndExtra dd 0
hInstance dd 0
hIcon dd 0
hCursor dd 0
hbrBackground dd 0
lpszMenuName dd 0
lpszClassName dd 0
hIconSm dd 0
ends WndClassEx

struc Point
x dd 0
y dd 0
ends Point

struc msgStruc
hwnd dd 0
message dd 0
wParam dd 0
lParam dd 0
time dd 0
pnt Point <>
ends msgStruc

MyMenu = 0065
ID_OPEN = 9C41
ID_SAVE = 9C42
ID_EXIT = 9C43

CS_HREDRAW = 0001
CS_VREDRAW = 0002
IDI_APPLICATION = 7F00
IDC_ARROW = 00007F00
COLOR_WINDOW = 5
WS_EX_WINDOWEDGE = 00000100
WS_EX_CLIENTEDGE = 00000200
WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE OR WS_EX_CLIENTEDGE
WS_OVERLAPPED = 00000000
WS_CAPTION = 00C00000
WS_SYSMENU = 00080000
WS_THICKFRAME = 00040000
WS_MINIMIZEBOX = 00020000
WS_MAXIMIZEBOX = 00010000
WS_OVERLAPPEDWINDOW = WS_OVERLAPPED OR WS_CAPTION OR \\
WS_SYSMENU OR WS_THICKFRAME OR \\
WS_MINIMIZEBOX OR WS_MAXIMIZEBOX
CW_USEDEFAULT = 80000000
SW_SHOW = 5
WM_COMMAND = 0111
WM_DESTROY = 0002
WM_CLOSE = 0010
MB_OK = 0

PROCTYPE ptGetModuleHandle stdcall \\
lpModuleName :dword

PROCTYPE ptLoadIcon stdcall \\
hInstance :dword, \\
lpIconName :dword

PROCTYPE ptLoadCursor stdcall \\
hInstance :dword, \\
lpCursorName :dword

PROCTYPE ptLoadMenu stdcall \\
hInstance :dword, \\
lpMenuName :dword

PROCTYPE ptRegisterClassEx stdcall \\
lpwcx :dword

PROCTYPE ptCreateWindowEx stdcall \\
dwExStyle :dword, \\
lpClassName :dword, \\
lpWindowName :dword, \\
dwStyle :dword, \\
x :dword, \\
y :dword, \\
nWidth :dword, \\
nHeight :dword, \\
hWndParent :dword, \\
hMenu :dword, \\
hInstance :dword, \\
lpParam :dword

PROCTYPE ptShowWindow stdcall \\
hWnd :dword, \\
nCmdShow :dword

PROCTYPE ptUpdateWindow stdcall \\
hWnd :dword

PROCTYPE ptGetMessage stdcall \\
pMsg :dword, \\
hWnd :dword, \\
wMsgFilterMin :dword, \\
wMsgFilterMax :dword

PROCTYPE ptTranslateMessage stdcall \\
lpMsg :dword

PROCTYPE ptDispatchMessage stdcall \\
pmsg :dword

PROCTYPE ptSetMenu stdcall \\
hWnd :dword, \\
hMenu :dword

PROCTYPE ptPostQuitMessage stdcall \\
nExitCode :dword

PROCTYPE ptDefWindowProc stdcall \\
hWnd :dword, \\
Msg :dword, \\
wParam :dword, \\
lParam :dword

PROCTYPE ptSendMessage stdcall \\
hWnd :dword, \\
Msg :dword, \\
wParam :dword, \\
lParam :dword

PROCTYPE ptMessageBox stdcall \\
hWnd :dword, \\
lpText :dword, \\
lpCaption :dword, \\
uType :dword

PROCTYPE ptExitProcess stdcall \\
exitCode :dword

extrn GetModuleHandleA :ptGetModuleHandle
extrn LoadIconA :ptLoadIcon
extrn LoadCursorA :ptLoadCursor
extrn RegisterClassExA :ptRegisterClassEx
extrn LoadMenuA :ptLoadMenu
extrn CreateWindowExA :ptCreateWindowEx
extrn ShowWindow :ptShowWindow
extrn UpdateWindow :ptUpdateWindow
extrn GetMessageA :ptGetMessage
extrn TranslateMessage :ptTranslateMessage
extrn DispatchMessageA :ptDispatchMessage
extrn SetMenu :ptSetMenu
extrn PostQuitMessage :ptPostQuitMessage
extrn DefWindowProcA :ptDefWindowProc
extrn SendMessageA :ptSendMessage
extrn MessageBoxA :ptMessageBox
extrn ExitProcess :ptExitProcess

UDataSeg
hInst dd ?
hWnd dd ?

IFNDEF VER1
hMenu dd ?
ENDIF

DataSeg
msg msgStruc <>
classTitle db \’Menu demo\’, 0
wndTitle db \’Demo program\’, 0
msg_open_txt db \’You selected open\’, 0
msg_open_tlt db \’Open box\’, 0
msg_save_txt db \’You selected save\’, 0
msg_save_tlt db \’Save box\’, 0

CodeSeg
Start: call GetModuleHandleA, 0 ; получаем hInstance
mov [hInst],eax

sub esp,SIZE WndClassEx ; выделяем место в стеке
; заполняем структуру WndClassEx
mov [(WndClassEx esp).cbSize],SIZE WndClassEx
mov [(WndClassEx esp).style],CS_HREDRAW or CS_VREDRAW
mov [(WndClassEx esp).lpfnWndProc],offset WndProc
mov [(WndClassEx esp).cbWndExtra],0
mov [(WndClassEx esp).cbClsExtra],0
mov [(WndClassEx esp).hInstance],eax
call LoadIconA, 0, IDI_APPLICATION
mov [(WndClassEx esp).hIcon],eax
call LoadCursorA, 0, IDC_ARROW
mov [(WndClassEx esp).hCursor],eax
mov [(WndClassEx esp).hbrBackground],COLOR_WINDOW
IFDEF VER1
mov [(WndClassEx esp).lpszMenuName],MyMenu
ELSE
mov [(WndClassEx esp).lpszMenuName],0
ENDIF
mov [(WndClassEx esp).lpszClassName],offset classTitle
mov [(WndClassEx esp).hIconSm],0
call RegisterClassExA, esp ; регистрируем окно

add esp,SIZE WndClassEx ; восстановим стек
; создадим окно

IFNDEF VER2
call CreateWindowExA, WS_EX_OVERLAPPEDWINDOW, \\ extended window style
offset classTitle, \\ pointer to registered class name
offset wndTitle, \\ pointer to window name
WS_OVERLAPPEDWINDOW, \\ window style
CW_USEDEFAULT, \\ horizontal position of window
CW_USEDEFAULT, \\ vertical position of window
CW_USEDEFAULT, \\ window width
CW_USEDEFAULT, \\ window height
0, \\ handle to parent or owner window
0, \\ handle to menu, or child-window
\\ identifier
[hInst], \\ handle to application instance
0 ; pointer to window-creation data
ELSE
call LoadMenu, hInst, MyMenu
mov [hMenu],eax
call CreateWindowExA, WS_EX_OVERLAPPEDWINDOW, \\ extended window style
offset classTitle, \\ pointer to registered class name
offset wndTitle, \\ pointer to window name
WS_OVERLAPPEDWINDOW, \\ window style
CW_USEDEFAULT, \\ horizontal position of window
CW_USEDEFAULT, \\ vertical position of window
CW_USEDEFAULT, \\ window width
CW_USEDEFAULT, \\ window height
0, \\ handle to parent or owner window
eax, \\ handle to menu, or child-window
\\ identifier
[hInst], \\ handle to application instance
0 ; pointer to window-creation data
ENDIF
mov [hWnd],eax
call ShowWindow, eax, SW_SHOW ; show window
call UpdateWindow, [hWnd] ; redraw window

IFDEF VER3
call LoadMenuA, [hInst], MyMenu
mov [hMenu],eax
call SetMenu, [hWnd], eax
ENDIF

msg_loop:
call GetMessageA, offset msg, 0, 0, 0
or ax,ax
jz exit
call TranslateMessage, offset msg
call DispatchMessageA, offset msg
jmp msg_loop
exit: call ExitProcess, 0

public stdcall WndProc
proc WndProc stdcall
arg @@hwnd: dword, @@msg: dword, @@wPar: dword, @@lPar: dword
mov eax,[@@msg] cmp eax,WM_COMMAND
je @@command
cmp eax,WM_DESTROY
jne @@default
call PostQuitMessage, 0
xor eax,eax
jmp @@ret
@@default:
call DefWindowProcA, [@@hwnd], [@@msg], [@@wPar], [@@lPar] @@ret: ret
@@command:
mov eax,[@@wPar] cmp eax,ID_OPEN
je @@open
cmp eax,ID_SAVE
je @@save
call SendMessageA, [@@hwnd], WM_CLOSE, 0, 0
xor eax,eax
jmp @@ret
@@open: mov eax, offset msg_open_txt
mov edx, offset msg_open_tlt
jmp @@mess
@@save: mov eax, offset msg_save_txt
mov edx, offset msg_save_tlt
@@mess: call MessageBoxA, 0, eax, edx, MB_OK
xor eax,eax
jmp @@ret
endp WndProc
end Start
Комментарии к программе
Здесь мне хотелось в первую очередь продемонстрировать использование прототипов функций API Win32. Конечно их (а также описание констант и структур из API Win32) следует вынести в отдельные подключаемые файлы, поскольку, скорее всего Вы будете использовать их и в других программах. Описание прототипов функций обеспечивает строгий контроль со стороны компилятора за количеством и типом параметров, передаваемых в функции. Это существенно облегчает жизнь программисту, позволяя избежать ошибок времени исполнения, тем более, что число параметров в некоторых функциях API Win32 весьма значительно.

Существо данной программы заключается в демонстрации вариантов работы с оконным меню. Программу можно откомпилировать в трёх вариантах (версиях), указывая компилятору ключи VER2 или VER3 (по умолчанию используется ключ VER1). В первом варианте программы меню определяется на уровне класса окна и все окна данного класса будут иметь аналогичное меню. Во втором варианте, меню определяется при создании окна, как параметр функции CreateWindowEx. Класс окна не имеет меню и в данном случае, каждое окно этого класса может иметь своё собственное меню. Наконец, в третьем варианте, меню загружается после создания окна. Данный вариант показывает, как можно связать меню с уже созданным окном.

Директивы условной компиляции позволяют включить все варианты в текст одной и той же программы. Подобная техника удобна не только для демонстрации, но и для отладки. Например, когда Вам требуется включить в программу новый фрагмент кода, то Вы можете применить данную технику, дабы не потерять функционирующий модуль. Ну, и конечно, применение директив условной компиляции — наиболее удобное средство тестирования различных решений (алгоритмов) на одном модуле.

Представляет определённый интерес использование стековых фреймов и заполнение структур в стеке посредством регистра указателя стека (esp). Именно это продемонстрировано при заполнении структуры WndClassEx. Выделение места в стеке (фрейма) делается простым перемещением esp: sub esp,SIZE WndClassEx

Теперь мы можем обращаться к выделенной памяти используя всё тот же регистр указатель стека. При создании 16-битных приложений такой возможностью мы не обладали. Данный приём можно использовать внутри любой процедуры или даже произвольном месте программы. Накладные расходы на подобное выделение памяти минимальны, однако, следует учитывать, что размер стека ограничен и размещать большие объёмы данных в стеке вряд ли целесообразно. Для этих целей лучше использовать \»кучи\» (heap) или виртуальную память (virtual memory).

Остальная часть программы достаточно тривиальна и не требует каких-либо пояснений. Возможно более интересным покажется тема использования макроопределений.

Макроопределения
Мне достаточно редко приходилось серьёзно заниматься разработкой макроопределений при программировании под DOS. В Win32 ситуация принципиально иная. Здесь грамотно написанные макроопределения способны не только облегчить чтение и восприятие программ, но и реально облегчить жизнь программистов. Дело в том, что в Win32 фрагменты кода часто повторяются, имея при этом не принципиальные отличия. Наиболее показательна, в этом смысле, оконная и/или диалоговая процедура. И в том и другом случае мы определяем вид сообщения и передаём управление тому участку кода, который отвечает за обработку полученного сообщения. Если в программе активно используются диалоговые окна, то аналогичные фрагменты кода сильно перегрузят программу, сделав её малопригодной для восприятия. Применение макроопределений в таких ситуациях более чем оправдано. В качестве основы для макроопределения, занимающегося диспетчеризацией поступаю щих сообщений на обработчиков, может послужить следующее описание.

Пример макроопределений

macro MessageVector message1, message2:REST
IFNB
dd message1
dd offset @@&message1

@@VecCount = @@VecCount + 1
MessageVector message2
ENDIF
endm MessageVector

macro WndMessages VecName, message1, message2:REST
@@VecCount = 0
DataSeg
label @@&VecName dword
MessageVector message1, message2
@@&VecName&Cnt = @@VecCount
CodeSeg
mov ecx,@@&VecName&Cnt

mov eax,[@@msg] @@&VecName&_1: dec ecx
js @@default
cmp eax,[dword ecx * 8 + offset @@&VecName] jne @@&VecName&_1
jmp [dword ecx + offset @@&VecName + 4]

@@default: call DefWindowProcA, [@@hWnd], [@@msg], [@@wPar], [@@lPar] @@ret: ret
@@ret_false: xor eax,eax
jmp @@ret
@@ret_true: mov eax,-1
dec eax
jmp @@ret
endm WndMessage
Комментарии к макроопределениям
При написании процедуры окна Вы можете использовать макроопределение WndMessages, указав в списке параметров те сообщения, обработку которых намерены осуществить. Тогда процедура окна примет вид:

proc WndProc stdcall
arg @@hWnd: dword, @@msg: dword, @@wPar: dword, @@lPar: dword
WndMessages WndVector, WM_CREATE, WM_SIZE, WM_PAINT, WM_CLOSE, WM_DESTROY

@@WM_CREATE:
; здесь обрабатываем сообщение WM_CREATE
@@WM_SIZE:
; здесь обрабатываем сообщение WM_SIZE
@@WM_PAINT:
; здесь обрабатываем сообщение WM_PAINT
@@WM_CLOSE:
; здесь обрабатываем сообщение WM_CLOSE
@@WM_DESTROY:
; здесь обрабатываем сообщение WM_DESTROY

endp WndProc
Обработку каждого сообщения можно завершить тремя способами:

вернуть значение TRUE, для этого необходимо использовать переход на метку @@ret_true;
вернуть значение FALSE, для этого необходимо использовать переход на метку @@ret_false;
перейти на обработку по умолчанию, для этого необходимо сделать переход на метку @@default.
Отметьте, что все перечисленные метки определены в макро WndMessages и Вам не следует определять их заново в теле процедуры.

Теперь давайте разберёмся, что происходит при вызове макроопределения WndMessages. Вначале производится обнуление счётчика параметров самого макроопределения (число этих параметров может быть произвольным). Теперь в сегменте данных создадим метку с тем именем, которое передано в макроопределение в качестве первого параметра. Имя метки формируется путём конкатенации символов @@ и названия вектора. Достигается это за счёт использования оператора &. Например, если передать имя TestLabel, то название метки примет вид: @@TestLabel. Сразу за объявлением метки вызывается другое макроопределение MessageVector, в которое передаются все остальные параметры, которые должны быть ничем иным, как списком сообщений, подлежащих обработке в процедуре окна. Структура макроопределения MessageVector проста и бесхитростна. Она извлекает первый параметр и в ячейку памяти формата dword заносит код сообщения. В следующую ячейку памяти формата dword записывается а дрес метки обработчика, имя которой формируется по описанному выше правилу. Счётчик сообщений увеличивается на единицу. Далее следует рекурсивный вызов с передачей ещё не зарегистрированных сообщений, и так продолжается до тех пор, пока список сообщений не будет исчерпан.

Сейчас в макроопределении WndMessage можно начинать обработку. Теперь существо обработки скорее всего будет понятно без дополнительных пояснений.

Обработка сообщений в Windows не является линейной, а, как правило, представляет собой иерархию. Например, сообщение WM_COMMAND может заключать в себе множество сообщений поступающих от меню и/или других управляющих элементов. Следовательно, данную методику можно с успехом применить и для других уровней каскада и даже несколько упростить её. Действительно, не в наших силах исправить код сообщений, поступающих в процедуру окна или диалога, но выбор последовательности констант, назначаемых пунктам меню или управляющим элементам (controls) остаётся за нами. В этом случае нет нужды в дополнительном поле, которое сохраняет код сообщения. Тогда каждый элемент вектора будет содержать только адрес обработчика, а найти нужный элемент весьма просто. Из полученной константы, пришедшей в сообщении, вычитается идентификатор первого пункта меню или первого управляющего элемента, это и будет номер нужного элемента вектора. Остаётся только сделать переход на обработчик.

Вообще тема макроопределений весьма поучительна и обширна. Мне редко доводится видеть грамотное использование макросов и это досадно, поскольку с их помощью можно сделать работу в ассемблере значительно проще и приятнее.

Резюме
Для того, чтобы писать полноценные приложения под Win32 требуется не так много:

собственно компилятор и компоновщик (я использую связку TASM32 и TLINK32 из пакета TASM 5.0). Перед использованием рекомендую \»наложить\» patch, на данный пакет. Patch можно взять на site http://www.borland.com/ или на нашем ftp сервере ftp.uralmet.ru.
редактор и компилятор ресурсов (я использую Developer Studio и brcc32.exe);
выполнить перетрансляцию header файлов с описаниями процедур, структур и констант API Win32 из нотации принятой в языке Си, в нотацию выбранного режима ассемблера: Ideal или MASM.
В результате у Вас появится возможность писать лёгкие и изящные приложения под Win32, с помощью которых Вы сможете создавать и визуальные формы, и работать с базами данных, и обслуживать коммуникации, и работать multimedia инструментами. Как и при написании программ под DOS, у Вас сохраняется возможность наиболее полного использования ресурсов процессора, но при этом сложность написания приложений значительно снижается за счёт более мощного сервиса операционной системы, использования более удобной системы адресации и весьма простого оформления программ.

Приложение 1. Файлы, необходимые для первого примера
Файл констант ресурсов resource.inc

IDD_DIALOG = 65 ; 101
IDR_NAME = 3E8 ; 1000
IDC_STATIC = -1
Файл определений dlg.def

NAME TEST
DESCRIPTION \’Demo dialog\’
EXETYPE WINDOWS
EXPORTS DlgProc @1
Файл компиляции makefile

# Make file for Demo dialog
# make -B
# make -B -DDEBUG for debug information

NAME = dlg
OBJS = $(NAME).obj
DEF = $(NAME).def
RES = $(NAME).res

TASMOPT=/m3 /mx /z /q /DWINVER=0400 /D_WIN32_WINNT=0400

!if $d(DEBUG)
TASMDEBUG=/zi
LINKDEBUG=/v
!else
TASMDEBUG=/l
LINKDEBUG=
!endif

!if $d(MAKEDIR)
IMPORT=$(MAKEDIR)\\..\\lib\\import32
!else
IMPORT=import32
!endif

$(NAME).EXE: $(OBJS) $(DEF) $(RES)
tlink32 /Tpe /aa /c $(LINKDEBUG) $(OBJS),$(NAME),, $(IMPORT), $(DEF), $(RES)

.asm.obj:
tasm32 $(TASMDEBUG) $(TASMOPT) $&.asm

$(RES): $(NAME).RC
BRCC32 -32 $(NAME).RC
Файл заголовков resource.h

//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by dlg.rc
//
#define IDD_DIALOG 101
#define IDR_NAME 1000
#define IDC_STATIC -1

// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
Приложение 2. Файлы, необходимые для второго примера
Файл описания mylib.def

LIBRARY MYLIB
DESCRIPTION \’DLL EXAMPLE, 1997\’
EXPORTS Hex2Str @1
Файл компиляции makefile

# Make file for Demo DLL
# make -B
# make -B -DDEBUG for debug information

NAME = mylib
OBJS = $(NAME).obj
DEF = $(NAME).def
RES = $(NAME).res

TASMOPT=/m3 /mx /z /q /DWINVER=0400 /D_WIN32_WINNT=0400

!if $d(DEBUG)
TASMDEBUG=/zi
LINKDEBUG=/v
!else
TASMDEBUG=/l
LINKDEBUG=
!endif

!if $d(MAKEDIR)
IMPORT=$(MAKEDIR)\\..\\lib\\import32
!else
IMPORT=import32
!endif

$(NAME).EXE: $(OBJS) $(DEF)
tlink32 /Tpd /aa /c $(LINKDEBUG) $(OBJS),$(NAME),, $(IMPORT), $(DEF)

.asm.obj:
tasm32 $(TASMDEBUG) $(TASMOPT) $&.asm

$(RES): $(NAME).RC
BRCC32 -32 $(NAME).RC
Приложение 3. Файлы, необходимые для третьего примера
Файл описания dmenu.def

NAME TEST
DESCRIPTION \’Demo menu\’
EXETYPE WINDOWS
EXPORTS WndProc @1
Файл ресурсов dmenu.rc

#include \»resource.h\»
MyMenu MENU DISCARDABLE
BEGIN POPUP \»Files\»
BEGIN
MENUITEM \»Open\», ID_OPEN
MENUITEM \»Save\», ID_SAVE
MENUITEM SEPARATOR
MENUITEM \»Exit\», ID_EXIT
END
MENUITEM \»Other\», 65535
END
Файл заголовков resource.h

//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by dmenu.rc
//
#define MyMenu 101
#define ID_OPEN 40001
#define ID_SAVE 40002
#define ID_EXIT 40003
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40004
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
Файл компиляции makefile

# Make file for Turbo Assembler Demo menu
# make -B
# make -B -DDEBUG -DVERN for debug information and version
NAME = dmenu
OBJS = $(NAME).obj
DEF = $(NAME).def
RES = $(NAME).res
!if $d(DEBUG)TASMDEBUG=/zi
LINKDEBUG=/v
!else
TASMDEBUG=/l
LINKDEBUG=
!endif

!if $d(VER2)
TASMVER=/dVER2
!elseif $d(VER3)
TASMVER=/dVER3
!else
TASMVER=/dVER1
!endif

!if $d(MAKEDIR)
IMPORT=$(MAKEDIR)\\..\\lib\\import32
!else
IMPORT=import32
!endif

$(NAME).EXE: $(OBJS) $(DEF) $(RES)
tlink32 /Tpe /aa /c $(LINKDEBUG) $(OBJS),$(NAME),, $(IMPORT), $(DEF), $(RES)

.asm.obj:
tasm32 $(TASMDEBUG) $(TASMVER) /m /mx /z /zd $&.asm

$(RES): $(NAME).RC
BRCC32 -32 $(NAME).RC

О регистрах в assembler

Регистр — это определенный участок памяти внутри самого процессора, от 8-ми до 32-х бит длиной, который используется для промежуточного хранения информации, обрабатываемой процессором. Некоторые регистры содержат только определенную информацию.
Регистры общего назначения — EAX, EBX, ECX, EDX. Они 32-х битные и делятся еще на две части, нижние из которых AX, BX, CD, DX — 16-ти битные, и деляется еще на два 8-ми битных регистра. Так, АХ делится на AH и AL, DX на DH и DL и т.д. Буква \»Н\» означает верхний регистр.

Так, AH и AL каждый по одному байту, АХ — 2 байта (или word — слово), ЕАХ — 4 байта (или dword — двойное слово). Эти регистры используются для операций с данными, такими, как сравнение, математические операции или запись данных в память.

Регистр СХ чаще всего используется как счетчик в циклах.

АН в DOS программах используется как определитель, какой сервис будет использоваться при вызове INT.

Регистры сегментов — это CS, DS, ES, FS, GS, SS. Эти регистры 16-ти битные, и содержат в себе первую половину адреса \»оффсет:сегмент\».

CS — сегмент кода (страница памяти) исполняемой в данный момент программы.
DS — сегмент (страница) данных исполняемой программы, т.е. константы, строковые ссылки и т.д.
SS — сегмент стека исполняемой программы.
ES, FS, GS — дополнительные сегменты, и могут не использоваться программой.
Регистры оффсета — EIP, ESP, EBP, ESI, EDI. Эти регистры 32-х битные, нижняя половина которых доступна как регистры IP, SP, BP, SI, DI.

EIP — указатель команд, и содержит оффсет (величину смещения относительно начала программы) на линию кода, которая будет исполняться следующей. То есть полный адрес на следующую исполняемую линию кода будет CS:ЕIP.
Регистр ESP указывает на адрес вершины стека (адрес, куда будет заноситься следующая переменная командой PUSH).
Регистр ЕВР содержит адрес, начиная с которого в стек вносится или забирается информация (или \»глубина\» стека). Параметры функций имеют положительный сдвиг относительно ЕВР, локальные переменные — отрицательный сдвиг, а полный адрес этого участка памяти будет SS:EBP.
Регистр ESI — адрес источника, и содержит адрес начала блока информации для операции \»переместить блок\» (полный адрес DS:SI), а регистр EDI- адрес назначения в этой операции (полный адрес ES:EDI).
Регистры управления — CR0, CR1, CR2, CR3. Эти 32-х битные регистры устанавливают режим работы процессора (нормальный, защищенный и т.д.), постраничное распределение памяти и т.д. Они доступны только для программ в первом кольце памяти (Kernel, например). Трогать их не следует.

Регистры дебаггера — DR0, DR1, DR2, DR3, DR4, DR5, DR6, DR7. Первые четыре регистра содержат адреса на точки прерывания, остальные устанавливают, что должно произойти при достижении точки прерывания.

Контрольные регистры — TR6, TR7. Используются для контроля постраничной системы распределения памяти операционной системой. Нужны только если вы собираетесь написать свою ОС.

Как узнать сеpийный номеp, тип IDE винта?

.Model Tiny
.Code
Base_Port equ 1f0h
HD equ 0 ; Hard Disk number
.Startup
mov dx, Base_Port + 6
mov al, 10100000b or (HD shl 4)
out dx, al
jmp $ + 2
inc dx
mov al, 0ech
out dx, al
jmp $ + 2
@@Wait: in al, dx
jmp $ + 2
test al, 80h
jnz @@Wait
mov dx, Base_Port
lea di, Buffer
mov cx, 100h
@@1: in ax, dx
xchg ah, al
stosw
loop @@1
xor cx, cx
lea dx, Fname
mov ah, 3ch
int 21h
xchg bx, ax
lea dx, Buffer
mov cx, 100h
mov ah, 40h
int 21h
mov ah, 3eh
int 21h
ret

Fname db \’hdd_id.dat\’, 0
Buffer db 100h dup (?)

end

Система команд процессоров Intel

Ниже приводится алфавитный перечень команд процессоров Intel с кратким описанием действия каждой команды и примерами ее использования.
В разделах статей, начинающихся с обозначения 386+, описываются отличия действия рассматриваемой команды в современных 32-разрядных процессорах (80386, i486, Pentium). Как правило, эти отличия заключаются в возможности использования не только 8- и 16-разрядных, но и 32-разрядных операндов, а также расширенных режимов адресации памяти. Обычные 16-разрядные программы реального режима вполне могут использовать расширенные регистры процессора (ЕАХ, ЕВХ и проч.), 32-битовые ячейки памяти и варианты команд для их обработки. Для того, чтобы ассемблер правильно транслировал команды с 32-разрядными операндами, в программу необходимо включить директиву ассемблера .386 (можно также использовать директивы .486 или .586), а сегменту команд (и во многих случаях сегменту данных) придать описатель use 16:
.386
codes segment use 16
assume CS:codes
codes ends
data segment use16
data ends
Кроме этого, необходимо разрешить компоновщику обрабатывать 32-разрядные операнды, что для компоновщика TLINK осуществляется указанием ключа /3.
Отдельные статьи, начинающиеся с обозначений 386+ , 486+ и Pentium+, посвящены командам, отсутствующим в МП 86. Многие из этих команд (например, команды проверки бита Ы или условной установки байта set) носят прикладной характер и могут использоваться в обычных программах реального режима.
Новые команды, реализованные впервые в МП 80386, сохраняют свое значение и в более современных процессорах. Для того, чтобы ассемблер распознавая команды МП 80386, в программе должна присутствовать директива .386.
Новые команды, реализованные впервые в МП 80486, сохраняют свое значение и в процессорах Pentium. Для того, чтобы ассемблер распознавая команды МП 80486, в программе должна присутствовать директива .486.
Для того, чтобы ассемблер распознавал команды, реализованные впервые в процессоре Pentium, в программе должна присутствовать директива .586.
Ряд команд требует для своего выполнения специальных условий, обычно отсутствующих в приложениях MS-DOS. Так, например, команда bound (проверки индекса массива на выход за границы) при обнаружении выхода за границы генерирует прерывание с вектором 5. Это прерывание в защищенном режиме как раз и является исключением нарушения границ массива, но в приложениях MS-DOS используется для вывода на печать содержимого экрана. Поэтому использование таких команд в реальном режиме затруднено.
Отдельные статьи, начинающиеся с обозначения 386Р+, посвящены привилегированным командам современных процессоров, работающих в расширенном режиме, и отсутствующим в МП 86. Для использования этих команд в программу необходимо включить директиву ассемблера .386Р (можно также использовать директивы .486Р или .586Р). Если при этом программа реализуется, как 16-разрядное приложение MS-DOS, сегмент команд должен иметь описатель usc16 (при наличии директивы .386 транслятор по умолчанию создает 32-разрядное приложение). Следует, однако, иметь в виду, что привилегированные команды защищенного режима предназначены для использования не в прикладных программах, а в операционных системах защищенного режима. В прикладных программах привилегированные команды приходится использовать лишь в весьма специальных случаях, когда, например, прикладная программа запускается в реальном режиме под управлением MS-DOS, но затем переводит процессор в защищенный режим и далее использует преимущества этого режима. Типичный пример программы такого рода — приложение DOS, которому требуется использовать всю оперативную память компьютера. В настоящей книге, посвященной, в основном, реальному режиму, дается лишь перечисление привилегированных команд защищенного режима.

ААА ASCII-коррекция регистра АХ после сложения
Команда ааа используется вслед за операцией сложения add в регистре AL двух неупакованных двоично-десятичных (BCD) чисел, если в АХ находится двухразрядное неупакованное двоично-десятичное число. Команда не имеет параметров. Она преобразует результат сложения в неупакованное двоично-десятичное число, младший десятичный разряд которого находится в AL. Если результат превышает 9, выполняется инкремент содержимого регистра АН. Команда воздействует на флаги AF и CF.

Пример
mov AX,0605h ; Неупакованное BCD 65
add AL,09h ;Неупакованное BCD 9, AX=060Eh
ааа ;AX=0704h, неупакованное BCD 74

AAD ASCII-коррекция регистра АХ перед делением
Команда aad используется перед операцией деления неупакованного двоично-десятичного (BCD) числа в регистре АХ на другое неупакован
ное двоично-десятичное число. Команда не имеет параметров. Она преобразует делимое в регистре АХ в двоичное число без знака, чтобы в результате деления получились правильные неупакованные двоично-десятичные числа (частное в AL, остаток в АН). Команда воздействует на флаги SF, ZF и PF.

Пример
raov AX,0207h ;Неупакованное BCD 27
mov DL,06h ;Неупакованное BCD 6
aad ;AX=001Bh=27
div DL ;AX=0304h, т.е. 4 и З в остатке

AAM ASCII-коррекция регистра АХ после умножения
Команда aam используется вслед за операцией умножения двух неупакованных двоично-десятичных чисел. Команда не имеет параметров. Она преобразует результат умножения, являющийся двоичным числом, в правильное неупакованное двоично-десятичное (BCD) число, младший разряд которого помещается в AL, а старший — в АН. Команда воздействует на флаги SF, ZF и PF.

Пример
mov AL,08h ;Неупакованное BCD 8
mov CL,07h ;Неупакованное BCD 7
mul CL ;AX=0038h=56
aam ;AX=0506h, BCD 56

AAS ASCII-коррекция регистра AL после вычитания
Команда aas используется вслед за операцией вычитания одного неупакованного двоично-десятичного числа (BCD) из другого в AL. Команда не имеет параметров. Она преобразует результат вычитания в неупакованное двоично-десятичное число. Если результат вычитания оказывается меньше 0, выполняется декремент содержимого регистра АН. Команда воздействует на флаги AF и CF; после ее выполнения AF=1, CF=1.
Пример
mov AX,0708h ;Неупакованное BCD 78
mov CL,09h ;Неупакованное BCD 9
sub AL,CL ;AX=07FFh
aas ;AX=0609h, неупакованное BCD 69

ADC Целочисленное сложение с переносом
Команда adc осуществляет сложение первого и второго операндов, прибаатяя к результату значение флага переноса CF. Исходное значение первого операнда (приемника) теряется, замещаясь результатом сложения. Второй операнд не изменяется. В качестве первого операнда команды adc можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команда adc (совместно с командой add) обычно используется для сложения 32-разрядных чисел. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov AX,1125h
adc AX,2C25h ;AX=3D4Bh, если CF был = 1
;AX=3D4Ah, если CF был = 0

Пример 2
; В полях данных:
numlow dw 0FFFFh ;Младшая часть 2-го слагаемого
numhigh dw 000Sh ;Старшая часть 2-го слагаемого
;Число 0005FFFFh=393215
;В программном сегменте:
mov AX,000Sh ;Младшая часть 1-го слагаемого
mov BX,0002h ;Старшая часть 1-го слагаемого
;Число 00020005h=131077
add АХ,numlow ;Сложение младших частей. АХ=4, CF=1
adc BX, numhigh ;Сложение старших частей с
;переносом.ВХ:АХ=0008:0004h.
;Число 00080004h=524292

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. Команда adc с 32-разрядными операндами может использоваться для сложения 64-разрядных целых чисел.

Пример
; В полях данных
mem321 dd 0FFFFFFFFh ;Младшая часть 1-го слагаемого
mem32h dd 98765432h ;Старшая часть 1-го слагаемого
; В программном сегменте
mov EAX,1 ;Младшая часть 2-го слагаемого
mov EBX,0 ;Старшая часть 2-го слагаемого
add EAX,mem321 ;Складываем младшие половины
;Сумма=100000000Ь>32 бит
;EAX=000000h, перенос
adc EBX,mem32h ;Складываем старшие половины
;и перенос. EBX=90000001h ;Сумма: 9876543300000000h

ADD Целочисленное сложение
Команда add осуществляет сложение первого и второго операндов. Исходное значение первого операнда (приемника) теряется, замещаясь результатом сложения. Второй операнд не изменяется. В качестве первого операнда команды add можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячей-ку памяти или непосредственное значение, однако не допускается опре-делять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команду add можно использовать для сложения как обычных целых чи-сел, так и двоично-десятичных (с использованием регистра АХ для хра-нения результата). Если складываются неупакованные двоично- десятич-ные (BCD) числа, после команды add следует использовать команду ааа; если складываются упакованные числа, то команду daa. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov BX,lFFEh
mov CX,3
add BX,CX ;BX=2001h, CX=0003h

Пример 2
mov AX,25h
add AX,12h ;AX=0037h

Пример 3
; В полях данных:
mem dw 128
;B программном сегменте:
add mem,100 ;mem=228

Пример 4
mov AX,0507h ;BCD распакованное 57 add AL,05h ;BCD 5, AX=050Ch aaa ;AX=0602h, BCD 62

Пример 5
mov AL,57h ;BCD упакованное 57 add AL,05h ;BCD 5, AL=5Ch daa ;AL=62h, BCD 62

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EAX,98765432h
add EAX,11111111h ; EAX=A9876543h

AND Логическое И
Команда and осущестшшет логическое (побитовое) умножение первого операнда на второй. Исходное значение первого операнда (приемника) теряется, замещаясь результатом умножения. В качестве первого операнда команды and можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами. Команда воздействует на флаги SF, ZF и PF.
Правила побитового умножения:
Первый операнд-бит 0101
Второй операнд-бит 0011
Бит результата 0001

Пример 1
mov AX,0FFEh
and AX,5555h ;AX=0554h

Пример 2
; В полях данных:
mem dw 0С003h
;В программном сегменте:
mov AX,700Eh
and AX,mem ;AX=4002h

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EDX, 0FA8 8 0 0 4 lh
and EDX,0FF00000Fh ; EDX = FA000001h

386P+ ARPL
Коррекция запрашиваемого уровня привилегий селектора
Команда aprl сравнивает селектор с образцом, содержащим максимально допустимый уровень привилегий (обычно используется селектор CS) и устанавливает проверяемое значение в соответствии с меньшим из двух уровней привилегий. Если изменение уровня не потребовалось, флаг ZF сбрасывается, если потребовалось — устанавливается. В качестве первого операнда команды aprl может использоваться 16-разрядный регистр или слово памяти с проверяемым селектором; в качестве второго операнда — 16-разрядный регистр с селектором-образцом.

386+ BOUND
Проверка индекса массива на выход за границы массива
Команда bound проверяет, лежит ли указанный индекс, рассматриваемый, как число со знаком, внутри заданных вторым операндом границ.
Если индекс выходит за границы массива снизу или сверху, генерируется прерывание с вектором 5. Первый операнд должен быть регистром, содержащим проверяемый индекс, второй — адресом поля памяти с двумя границами проверяемого массива. В команде bound допустимо использование как 16-битовых, так и 32-битовых операндов (но и первый, и второй операнды должны быть одного типа).

386+ BSF Прямое сканирование битов
Команда bsf сканирует слово или двойное слово в поисках бита, равного 1. Сканирование выполняется от младшего бита (0) к старшему. Если в слове не найдено устаноштенных битов, то устанавливается флаг ZF. Если установленные биты есть, то номер первого установленного бита заносится в указанный в команде регистр. Номером бита считается его позиция в слове, отсчитываемая от бита 0. В качестве первого операнда команды bsf следует указывать регистр, куда будет помещен результат сканирования, в качестве второго — регистр или ячейку памяти со сканируемым словом. В команде bsf допустимо использование как 16-битовых, так и 32-битовых операндов (но и первый, и второй операнды должны быть одного типа).

Пример 1
mov BX,70h ;Анализируемое данное
bsf АХ,ВХ ;АХ=4, ZF=0

Пример 2
mov SI,0 ;Анализируемое данное
bsf BX,SI ;ZF=1, в ВХ прежнее значение

Пример 3
mov SI,8 ;Анализируемое данное
bsf BX,SI ;BX=3, ZF=1

Пример 4
;В полях данных
mem dw 9000h Анализируемое данное
;В программном сегменте:
bsf AX, mem ;AX=000Ch=12, ZF=0

386+ BSR Обратное сканирование битов
Команда bsf сканирует слою или двойное слово в поисках бита, равного 1. Сканирование выполняется от старшего бита (15 или 31) к младшему. Если в слове не найдено установленных битов, то устанашгивается флаг ZF. Если установленные биты есть, то номер первого устаноштенного бита заносится в указанный в команде регистр. Номером бита считается его позиция в слове, отсчитываемая от бита 0. В качестве первого операнда команды bsf следует указывать регистр, куда будет помещен результат сканирования, в качестве второго — регистр или ячейку памяти со сканируемым словом. В команде bsf допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд — константа).

Пример 1
mov BX,70h ;Анализируемое данное bsr AX,BX ;AX=6, ZF=0

Пример 2
mov SI,0 ;Анализируемое данное
bsr BX,SI ;ZF=1, в ВХ прежнее значение

Пример 3
mov SI,8 ;Анализируемое данное
bsf BX,SI ;BX=3, ZF=1

Пример 4
; В полях данных
mem dw 9000h ; Анализируемое данное
;В программном сегменте:
bsr AX, mem ;AX=000Fh=15, ZF=0

486+ BSWAP Обмен байтов
Команда bswap изменяет порядок байтов в своем единственном операнде, в качестве которого может выступать только 32-разрядный регистр общего назначения. Биты 7…0 обмениваются с битами 31…24, а биты 15… 18 с битами 23…16. Другими словами, нумерация байтов регистра изменяется на противополжную (вместо 3, 2, 1,0 — 0, 1, 2, 3). Команда не воздействует на флаги процессора.

Пример
mov ЕАХ, 01234567h
bswapEAX ;EAX=67452301h

386+ ВТ Проверка бита
Команда bt позволяет определить, установлен ли в заданном слове определенный бит. Анализируемое слово выступает в качестве первого операнда, номер бита — в качестве второго. Первым операндом команды bt может служить регистр или ячейка памяти, вторым — регистр или непосредственное значение. В команде допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд — константа).
Значение проверяемого бита копируется в флаг CF.

Пример 1
mov AX,00FFh ;Анализируемое данное
bt AX,5 ; бит 5=1, ZF=1

Пример 2
mov AX,00FFh ;Анализируемое данное
bt AX,8 ;бит 8=0, ZF=0

Пример 3
mov AX,8001h ;Анализируемое данное
mov BX,15 ;Номер проверяемого бита
bt АХ,ВХ ;бит 15 = 1, ZF=1

Пример 4
;В полях данных
mem dw IFh ;Анализируемое данное
;В программном сегменте:
bt mem, 4 ;бит 4 = 1, ZF=1

386+ ВТС Проверка и инверсия бита
Команда btc проверяет определенный бит в слове, заданном первым операндом, копирует его значение в флаг CF и инвертирует. Номер бита выступает в качестве второго операнда. Первым операндом команды btc может служить регистр или ячейка памяти, вторым — регистр или непосредственное значение. В команде допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд — константа).

Пример 1
mov AX,00FFh ;Анализируемое данное
btc АХ, 5 ;AX=00DFh Бит 5 был = 1
;Сброс бита 5, ZF=1

Пример 2
mov AX,OOFFh /Анализируемое данное
btc АХ, 8 ;AX=lFFh Бит 8 был = О
;Установка бита 8, ZF=0

Пример 3
mov AX,8001h ;Анализируемое данное
mov BX,15 ;Номер проверяемого бита
btc AX,BX ;AX=0001h, ZF=1

Пример 4
; В полях данных
mem dw IFh
;В программном сегменте: ; Анализируемое данное
btc mem, I /mem=lEh, ZF=1

386+ BTR Проверка и сброс бита
Команда btr проверяет определенный бит в слове, заданном первым операндом, копирует его значение в флаг CF и сбрасывает. Номер бита выступает в качестве второго операнда. Первым операндом команды btr может служить регистр или ячейка памяти, вторым — регистр или непосредственное значение. В команде допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд — константа).

Пример 1
Пример 1
mov AX,00Fh ;Анализируемое данное
btr АХ, 5 ;AX=00DFh. Бит 5 был = 1
;Сброс бита 5, ZF=1

Пример 2
mov AX,00FFh ;Анализируемое данное
btr AX, 8 ;AX=0FFh Бит 8 был =0
;Остался 0, ZF=0

Пример 3
mov AX,8001h ;Анализируемое данное
mov BX,15 ;Номер проверяемого бита
btr AX,BX ;AX=0001h Бит 15 был = 1
;Сброс бита 15, ZF=1

Пример 4
;В полях данных
mem dw IFh
;В программном сегменте: ;Анализируемое данное
btr mem,10 ;mem=lFh Бит 10 был = 0
;Остался 0, ZF=0

386+ BTS Проверка и установка бита
Команда bts проверяет определенный бит в слове, заданном первым операндом, копирует его значение в флаг CF и устанашшвает. Номер бита выступает в качестве второго операнда. Первым операндом команды bts может служить регистр или ячейка памяти, вторым — регистр или непосредственное значение. В команде допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд — константа).

Пример 1

mov AX,OOFFh ;Анализируемое данное
bts AX, 5 ;AX=OOFFh Бит 5 был = 1
;Остался 1, ZF=1

Пример 2
mov AX,OOFFh ;Анализируемое данное
bts AX, 8 ;AX=lFFh Бит 8 был = О
;Установка бита 8, ZF=0

Пример 3
mov AX,8001h ; Анализируемое данное
mov BX,15 ;Номер проверяемого бита
bts AX,BX ;AX=8001h Бит 15 был = 1
;Остался 1, ZF=1

Пример 4
; В полях данных
mem dw IFh ; Анализируемое данное
;В программном сегменте:
bts mem,10 ;mem=4lFh Бит 10 был = О
; Установка бита 10, ZF=0

CALL Вызов подпрограммы
Команда call передает управление подпрограмме, сохранив перед этим в стеке смещение к точке возврата. Команда ret, которой обычно заканчивается подпрограмма, забирает из стека адрес возврата и возвращает управление на команду, следующую за командой call. Команда не воздействует на флаги процессора.
Команда call имеет четыре модификации:
— вызов прямой ближний (в пределах текущего программного сегмента);
— вызов прямой дальний (вызов подпрограммы, расположенной в другом программном сегменте);
— вызов косвенный ближний;
— вызов косвенный дальний.
Все разновидности вызовов имеют одну и ту же мнемонику call, хотя и различающиеся коды операций. Во многих случаях транслятор может определить вид вызова по контексту, в тех же случаях, когда это невозможно, следует использовать атрибутные операторы:
near ptr — прямой ближний вызов;
far ptr — прямой дальний вызов;
word ptr — косвенный ближний вызов;
dword ptr — косвенный дачьний вызов.
Команда call прямого ближнего вызова заносит в стек относительный адрес точки возврата в текущем программном сегменте и модифицирует IP так, чтобы в нем содержатся относительный адрес точки перехода в том же программном сегменте. Необходимая для вычисления этого адреса величина смещения от точки возврата до точки перехода содержится в коде команды, который занимает 3 байт (код операции E8h и смещение к точке перехода).
Команда call прямого дальнего вызова заносит в стек два слова — сначала сегментный адрес текущего программного сегмента, а затем (выше, в слово с меньшим адресом) относительный адрес точки возврата в текущем программном сегменте. Далее модифицируются регистры IP и CS: в IP помещается относительный адрес точки перехода в том сегменте, куда осуществляется переход, а в CS — сегментный адрес этого сегмента. Обе эти величины берутся из кода команды, который занимает 5 байтов (код операции 9А1г, относительный адрес вызываемой подпрограммы и ее сегментный адрес).
Косвенные вызовы отличаются тем, что адрес перехода извлекается не из кода команды, а из ячеек памяти; в коде команды содержится информация о том, где находится адрес вызова. Длина кода команды зависит от используемого способа адресации.

Примеры прямого ближнего вызова
call near ptr subl ;Вызов подпрограммы subl
;из того же сегмента
call subl ;To же самое

Косвенные ближние вызовы

Пример 1
mov BX,offset subl ;ВХ=адрес подпрограммы
call BX ;Вызов подпрограммы

Пример 2
; В полях данных:
addr dw subl ;Ячейка с адресом подпрограммы
;В программном сегменте:
call DS:addr ;Вызов подпрограммы
call word ptr addr ;To же самое

Пример 3
;В полях данных:
addr dw subl ;Ячейка с адресом подпрограммы
;В программном сегменте:
mov SI,offset addr ;SI=адрес ячейки с адресом
;подпрограммы
call [SI] ;Вызов подпрограммы

Пример 4
;В полях данных:
tbl dw subl ;Ячейка с адресом
;подпрограммы 1
dw sub2 ;Ячейка с адресом
;подпрограммы 2
dw sub3 ;Ячейка с адресом
;подпрограммы 3
;В программном сегменте:
mov BX,offset tbl ;ВХ=адрес таблицы адресов
;подпрограмм
mov SI, 2 ;SI=смещение к адресу sub2
call [BX] [SI] ;Вызов подпрограммы 2

Пример прямого дальнего вызова
call far ptr subl ;Вызов подпрограммы sub2,
;расположенной в другом
;программном сегменте

Косвенные дальние вызовы

Пример 1
;В полях данных:
addr dd subl ;Поле с двухсловным
;адресом подпрограммы
;В программном сегменте:
call DS:addr ;Вызов подпрограммы
call dword ptr addr;To же самое

Пример 2
;В полях данных:
addr dd subl ;Поле с двухсловным
;адресом подпрограммы
;В программном сегменте:
mov DI,offset addr ;В1=адрес поля с адресом
;подпрограммы
call [DI] ;Вызов подпрограммы

Пример 3
; В полях данных:
tbl dd subl ;Адрес подпрограммы 1
dd sub2 ;Адрес подпрограммы 2
dd sub3 ;Адрес подпрограммы 3
;В программном сегменте:
mov SI,offset tbl ;DI=адрес таблицы адресов
mov DI,8 ;Смещение к адресу sub3
call [SI] [DI] ;Вызов подпрограммы sub3

386+ Допустимо использование дополнительных режимов адресации 32-разрядных процессоров. В 32-разрядных приложениях допустимо использование 32-битовых операндов. В защищенном режиме роль сегментного адреса выполняет селектор.

Примеры
call [EAX] ;Косвенный вызов
call 8[ЕСХ] ;Косвенный вызов

CBW Преобразование байта в слово
Команда cbw заполняет регистр АН знаковым битом числа, находящегося в регистре AL, что дает возможность выполнять арифметические операции над исходным операндом-байтом, как над словом в регистре АХ. Команда не имеет параметров и не воздействует на флаги процессора.

Пример 1

mov AL,5
cdw ;AX=0005h

Пример 2
mov AL, — 2 ;AL=FEh=-2 (байт)
cdv ;AX=FFFEh=-2 (слово)

386+ CDQ Преобразование двойного слова в четверное
Команда cdq расширяет знак двойного слова в регистре ЕАХ на регистр EDX. Эту команду можно использовать для образования четырсхсловного делимого из двухсловного перед операцией двухсловного деления. Команда не имеет параметров и не воздействует на флаги процессора.

Пример 1
;В полях данных
mem dd -2 ; Отрицательное число
;В программном сегменте
mov ЕАХ,mem ;EAX=FFFFFFFEh
cdq ;EDX=FFFFFFFFh, EAX=FFFFFFFEh

Пример 2
;В полях данных
mem dd 7FFFFFFEh ,’Положительное число
;В программном сегменте
mov ЕАХ,mem ;EAX=7FFFFFFEh
cdq ;EDX=00000000h, EAX=7FFFFFFEh

CLC Сброс флага переноса
Команда clc сбрасывает флаг переноса CF в регистре флагов. Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
clc ;CF=0, независимо от
;исходного состояния

CLD Сброс флага направления
Команда eld сбрасывает флаг направления DF в регистре флагов, устанавливая прямое (в порядке возрастания адресов) направление выполнения операций со строками (цепочками). Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
cld ;DF=0, независимо от
;исходного состояния

CL1 Сброс флага прерываний
Команда sti сбрасывает флаг разрешения прерываний IF в регистре флагов, запрещая (до установки этого флага командой sti) все аппаратные прерывания (от таймера, клавиатуры, дисков и т.д.) Команда не запрещает процессору выполнение команды hit (реализация программных прерываний); также не запрещаются немаскируемые прерывания, поступающие на вход NMI микропроцессора. Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
cli ;IF=0, независимо от
;исходного состояния

386Р+ CLTS Сброс флага переключения задачи в управляющем регистре 0
Команда cits сбрасывает флаг TS в регистре CR0.

CMC Инвертирование флага переноса
Команда сmс изменяет значение флага переноса CF в регистре флагов на обратное. Команда не имеет операндов и не воздействует на остальные флаги процессора.

Пример
cmc ;Состояние флага CF
;изменяется на обратное

СМР Сравнение
Сами операнды не изменяются. Таким образом, если команду сравнения записать в общем виде

стр операнд_1, операнд_2
то ее действие можно условно изобразить следующим образом:

операнд_1 — операнд_2 -> флаги процессора

В качестве первого операнда команды сmр можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно, как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Обычно вслед за командой сmр стоит одна из команд условных переходов, анализирующих состояние флагов процес
сора. При сравнении чисел без знака необходимо использовать команды условных переходов, предназначенные для анализа чисел без знака (ja, jb и проч.). При сравнении чисел со знаком необходимо использовать команды условных переходов, предназначенные для анализа чисел со знаком (jl, jg и проч.).

Пример 1
cmp АХ,10000 ;АХ-10000
je eqlOOOO ;Переход на метку eqlOOOO,
;если АХ=10000

Пример 2
;В полях данных:
base dw 8002h
;В программном сегменте:
cmp DX,base DX-base
jb below ;Переход на метку below,
;если DX, рассматриваемое
;как число без знака, меньше
;числа без знака 8002h=32770

Пример 3
;В полях данных:
base dw 8002h
;В программном сегменте:
cmp DX,base DX-base
jl less ;Переход на метку less, если
;DX, рассматриваемое как
;число со знаком, меньше
;числа со знаком 8002h=-32766

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
cmp EAX,8000000 Oh
ja above ;Переход, если
;EAX=80000001h…FFFFFFFFh

CMPS Сравнение строк

CMPSB Сравнение строк по байтам

CMPSW Сравнение строк по словам
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они сравнивают по одному элементу каждой строки, фактически осуществляя вычитание второго операнда из первого и устанавливая в соответствии с результатом вычитания флаги CF, PF, AF, ZF, SF и OF. Команда cmpsb выполняет сравнение по байтам, команда cmpsw — по словам, а команда cmps может быть использована для сравнения как байтов, так и слов. В последнем случае размер сравниваемых элементов определяется их описанием (с помощью директив db или dw). Первый операнд адресуется через DS:SI, второй — через ES:DI. Таким образом, операцию сравнения можно условно изобразить следующим образом:
(DS:SI) — (ES:DI) -> флаги процессора
После каждой операции сравнения регистры SI и DI получают положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2 в зависимости от размера сравниваемых элементов (байт или слово).
Вариант команды cmps имеет формат
cmps строка_1, строка_2
(что не избавляет от необходимости инициализировать регистры DS:SI и ES:DI адресами строк строка_1 и строка_2 соответственно). В этом формате возможна замена сегмента первой строки:
cmps ES:строка_ 1, строка_2
Рассматриваемые команды могут предваряться префиксами повторения repe/repz (повторять, пока элементы равны, т.е. до первого неравенства) и repne/repiiz (повторять, пока элементы не равны, т.е. до первого равенства). В любом случае выполняется не более СХ операций над последовательными элементами.
После выполнения рассматриваемых команд регистры SI и DI указывают на ячейки памяти, находящиеся за теми (если DF=0) или перед теми (если DF=1) элементами строк, на которых закончились операции сравнения.

Пример 1
;В полях данных сегмента данных, адресуемого через DS:
strl db ‘FILE.001’ ;1-я строка
;В полях данных сегмента данных, адресуемого через ES:
str2 db ‘FILE.012’ ;2-я строка
;В программном сегменте:
eld ;Сравнение вперед
mov SI,offset strl ;DS:SI ® strl
mov DI, off set str2 ;ES:DI ® str2
mov CX,8 ;Длина сравниваемых строк
repe cmpsb ;Поиск различия в строках
je equal ;Переход, если строки
;совпадают
notequ: ;Продолжение, если строки
;не совпадают

В примере 1 строки не совпадают, и команда je выполнена не будет. После завершения сравнения строк управление будет передано на метку notequ. Содержимое регистров в этой точке: СХ=1 (так как не выполнено сравнение одной последней пары символов), SI = <смещение strl> + 7, DI = <смещение strl> + 7 (выполнено сравнение 7 пар символов).

Пример 2
;В полях данных сегмента данных, адресуемого через DS:
strl db ‘12345678*90’ ;1-я строка
;В полях данных сегмента данных, адресуемого через ES:
str2 db ‘ abcdefgh*ij’ ;2-я строка
; В программном сегменте:
cld ;Сравнение вперед
mov SI,offset strl;DS:SI ‘ strl
mov DI,offset str2;ES:DI ‘ str2
mov CX,11 ;Длина сравниваемых строк
repne cmpsb ;Поиск первой пары
;одинаковых элементов
jne notequ ;Переход, если таковой нет
found: ;Продолжение, если пара
;одинаковых элементов найдена

В примере 2 имеется пара одинаковых элементов (*) в позиции 8 от начата строк. Поэтому команда jne выполнена не будет. После завершения сравнения строк управление будет передано на метку found. Содержимое регистров в этой точке: СХ=2 (так как не выполнено сравнение двух последних пар символов), SI = <смещенис strl> + 9, DI = <смешенис strl> + 9 (выполнено сравнение 9 пар символов).

Пример 3
;В полях данных сегмента, адресуемого через ES:
strl db ‘09.12.1998’ ;1-я строка
str2 db ‘09.12.1998’ ;2-я строка
;В программном сегменте:
eld ;Сравнение вперед
mov SI, off set strl ;DS:SI -> strl
mov DI,offset str2 ;ES:DI -> str2
mov CX,10 ;Длина сравниваемых строк
repe cmps ES:str1,ES:str2 ;Поиск различия в строках
je equal ; Переход, если строки
;одинаковы
notequal: ;Продолжение, строки
;различаются

В примере 3 строки одинаковы и после завершения сравнения управление будет передано на метку equal. Поскольку строки описаны с помощью директив db, фактически выполняется команда cmpsb, т.е. побайто вое сравнение.
386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

386+ CMPSD Сравнение строк по двойным словам
Команда аналогична командам МП 86 cmpsb и cmpsw, но позволяет сравнивать 32-битовые участки строк, адресуемых через регистры DS:ESI и ES:EDI (или, в 16-разрядных приложениях, через DS:SI и ES:DI). Использование мнемоники cinpsd с префиксом rep не означает, что в качестве счетчика будет автоматически использоваться расширенный регистр ЕСХ.

Пример
;В полях данных сегмента, адресуемого через DS
areal dd 152345,168666,954333
area2 dd 152345,168666,954331
;B программном сегменте
push DS
pop ES ;ES=DS
mov SI,offset areal ;DS:SI ->areal
mov DI,offset area2 ;ES:DI ->area2
mov CX,3 ;Будем сравнивать З числа
repe cmpsd
je equal

В приведенном примере в строках (фактически это целочисленные массивы) различаются последние элементы, и команды jc выполнена не будет.

486+ CMPXCHG Сравнение и обмен
Команда cmpxchg выполняет в одной операции сравнение и обмен операндов. Команда требует два параметра и неявным образом использует третий операнд — регистр ЕАХ. Первый операнд (приемник) должен находиться в 16- или 32-битовой ячейке памяти, второй операнд (источник) — в регистре общего назначения такого же размера. Команда выполняет сравнение операнда-приемника с содержимым неявного операнда — регистра ЕАХ. Если сравниваемые значения совпадают, операнд-приемник замещается операндом-источником (т.е. содержимое регистра записывается в память). Если сравниваемые значения не совпадают, содержимое памяти (приемник) поступает в регистр ЕАХ (рис. П1). Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Рис.П1. Действие команды cmpxchg

Пример 1
; В полях данных
mem dw 135
; В программном сегменте
mov AX,135
mov BX,60
cmpxchg mem,BX ;mem=AX. Регистр ® память ;
mem=60, BX=60, АХ=135

Пример 2
; В полях данных
mem dw 135
;В программном сегменте
mov AX,148
mov BX,60
cmpxchg mem,BX ;mem<>AX. Память ® АХ
;mem=135, BX=60, AX=148

Pentium+ CMPXCHG8B Сравнение и обмен 8 байтов
Команда cmpxchgSb выполняет в одной операции сравнение и обмен 8-байтовых операндов. Команда требует один параметр и неявным образом использует еще два операнда — пары регистров EDX:EAX и ЕСХ:ЕВХ. В качестве явного операнда команды (приемника) может выступать только 64-битная (8-байтовая) ячейка памяти. Команда выполняет сравнение операнда-приемника в памяти с содержимым EDX:EAX. Если сравниваемые значения совпадают, то операнд-приемник в памяти замещается 64-битным значением ЕСХ:ЕВХ. Если сравниваемые значения не совпадают, содержимое памяти поступает в пару регистров EDXrEAX, замещая один из сравниваемых операндов (рис. П2). Команда воздействует на флаг ZF.

Рис. П2. Действие команды cmpxchg8b

Пример 1
; В полях данных
mem dq 1122334455667788h
;В программном сегменте
mov ЕСХ,9
mov ЕВХ,5
mov EDX,11223344h
mov EAX,55667788h
cmpxchgSb mem ;mem=EDX:EAX. ECX:EBX ® mem ;
mem=0000000900000005h

Пример 2
; В полях данных
mem dq 1122334455667788h
;B программном сегменте
mov ECX,9
mov EBX,5
mov EDX,11223344h
mov EAX,55667789h
cmpxchgSb mem ;memOEDX: EAX. Mem -» EDX : EAX ;mem=1122334455667788h ;EDX=11223344h, EAX=55667788h

При работе с многобайтовыми данными не следует забывать о том, что в памяти байты любых данных всегда выстраиваются в порядке их номеров,т.е. от младшим к все более старшим, в то время как при изображении чисел мы применяем обратный порядок — сначала пишем старшие разряды числа, затем младшие. В то же время символьные строки мы изображаем так же, как они располагаются в памяти — для нас естественно считать, что по мере движения по строке вправо номер символа возрастает.

Пример 3
; В полях данных
meml db ‘12345678’ ;Строка-операнд
mem2 db ‘12345678’ ;Сравниваемая строка
;В программном сегменте
mov ECX,68676665h ;’efgh’
mov EBX,64636261h ;’abcd’
mov EDX,dword ptr mem2+4 ;Забираем старшую
;часть строки
mov EAX,dword ptr mem2 ;Забираем младшую
;часть строки
cmpxchg8b gword ptr meml ;Операнды совпадают
;ZF=1, mem1=»abcdefgh»
;ECX:EBX без изменений
;EDX:EAX без изменений

Пример 4
;В полях данных
meml db ‘12345678’ ;Строка-операнд
mem2 db ‘abcdefgh’ ;Сравниваемая строка
; В программном сегменте
mov ECX,68676665h ;’efgh’
mov EBX,64636261h ;’abed’
mov EDX,dword ptr mem2+4 ;3абираем старшую
;часть строки
mov EAX,dword ptr mem2 ;Забираем младшую
;часть строки
cmpxchg8b qword ptm mem1 ;Операнды не совпадают
;ZF=0, EDX=38373635=’5678′
;EAX=34333231=’1234′
;mem1s без изменения
;При неравенстве ЕСХ:ЕВХ не принимают участие в операции

Pentium+ CPUID Идентификация процессора
Команда cpuid позволяет получить код идентификации процессора, установленного на данном компьютере. Команда в качестве неявного операнда использует регистр ЕАХ. Для процессоров Pentium регистр ЕАХ перед вызовом команды cpuid может принимать два значения: 0 и 1. Если ЕАХ=0, то команда возвращает в регистре ЕАХ код 1, а в регистрах ЕВХ, EDX и ЕСХ (именно в таком порядке) — три части символьной строки, идентифицирующей изготовителя процессора. Для процессоров Intel возвращаемая строка в целом имеет вид «Genumclatcl».
Если перед вызовом команды cpuid значение ЕАХ равно 1, то команда возвращает в регистре ЕАХ коды разработки конкретной версии процессора, а в регистре EDX код IBFli, содержащий информацию о возможностях процессора.
Коды разработки в регистре ЕАХ хранятся в следующем формате:
биты 0 … 3 — номер поколения (например, 3);
биты 4 … 7 — модель (например, 4);
биты 8 … 11 — семейство (5 для Pentium).
Содержимое регистра EDX включает конфиденциальную информацию изготовителя, а также говорит о наличии на кристалле микропроцессора арифметического сопроцессора (бит 0) и поддержке команды cmpxchgSb (бит 8).
Пример
;В полях данных mem dd 0,0,0 ;В программном сегменте
mov ЕАХ,О
cpuid ;EAX=0001h
mov mem, ЕВХ
mov mem+4, EDX
mov mem+8, ECX ;mem=’Genuinelntel’
cpuid EAX=543h (например) ,EDX = lBFh

CWD Преобразование слова в двойное слово
Команда cwd заполняет регистр DX знаковым битом содержимого регистра АХ, преобразуя тем самым 16-разрядное число со знаком в 32-разрядное, размещаемое в регистрах DX:AX. Команду удобно использовать для преобразования двухбайтового делимого в четырсхбайтовое (двойное слово) при делении на 16-разрядный операнд. Команда не имеет параметров и не воздействует на флаги процессора.

Пример 1
mov AX,32767 ;AX=7FFFh
cwd ;AX=7FFFh, DX=OOOOh.
;DX:AX=32767

Пример 2
mov AX,-32768 ;AX=8000h
cwd ;AX=8000h, DX=FFFFh.
;DX:AX=-32768

386+ CWDE
Преобразование слова в двойное слово с расширением
Команда cwde заполняет старшую половину регистра ЕАХ знаковым битом содержимого регистра АХ, преобразуя тем самым 16-разрядное число со знаком в 32-разрядное, размещаемое в расширенном регистре ЕАХ. Команда не имеет операндов и не воздействует на флаги процессора.

Пример
; В полях данных
mem dw — 3
;В программном сегменте
mov AX,mem ;AX=FFFD
cwde ;EAX=FFFFFFFDh

DAA Десятичная коррекция в регистре AL после сложения

DAA Десятичная коррекция в регистре AL после сложения
Команда daa корректирует результат сложения в регистре AL двух упакованных двоично-десятичных (BCD) чисел (по одной цифре в каждом полубайте), чтобы получить пару правильных упакованных двоично-десятичных цифр. Команда используется вслед за операцией сложения упакованных двоично-десятичных чисел. Если результат сложения превышает 99, возникает перенос и устанавливается флаг CF. Команда воздействует на флаги SF, ZF, AF, PF и CF.

Пример 1
mov AL,87h ;Упакованное BCD 87
add AL,04h ;После сложения AL=8Bh
daa ;AL=91h, т.е. упакованное BCD 91

Пример 2
mov AL,87h ;Упакованное BCD 87
add AL,11h ;После сложения AL=97h
daa ;AL=97h, т.е. упакованное
;BCD 97 (в данном случае
;команда daa ничего не делает)

DAS Десятичная коррекция в регистре AL после вычитания
Команда das корректирует результат вычитания в регистре AL двух упакованных двоично-десятичных (BCD) чисел (по одной цифре в каждом полубайте), чтобы получить пару правильных упакованных десятичных цифр. Команда используется вслед за операцией вычитания упакованных двоично-десятичных чисел. Если для вычитания требовался заем, устанавливается флаг CF. Команда воздействует на флаги SF, ZF, AF, PF и CF.

Пример 1
mov AL,55h ;Упакованное BCD 55
sub AL,19h ;После вычитания AL=3Ch
das ;AL=36h, т.е. упакованное BCD 36

Пример 2
mov AL,55h ;Упакованное BCD 55
sub AL,15h ;После вычитания AL=40h
das ;AL=40h, т.е. упакованное
;BCD 40 (в данном случае
;команда das ничего не делает)

DEC Декремент (уменьшение на 1)
Команда dec вычитает 1 из операнда, в качестве которого можно указывать регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Операнд интерпретируется как число без знака. Команда воздействует на флаги OF, SF, ZF, AF и PF.

Пример 1
mov AX,0FFFFh
dec AX ;AX=FFFEh

Пример 2
mov CX,0
dec CX ;CX=FFFFh

Пример 3
mov CX,3500h
dec CL ;CX=35FFh

Пример 4
; В полях данных
mem dw 68
;B программном сегменте
dec mem mem=67

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EAX, 0
dec EAX ;EAX=FFFFFFFFh

DIV Деление целых чисел без знака
Команда div выполняет деление целого числа без знака, находящегося в регистрах АХ (в случае деления на байт) или DX:AX (в случае деления на слово), на операнд-источник (целое число без знака). Размер делимого в два раза больше размеров делителя и остатка.
Для однобайтовых операций делимое помещается в регистр АХ; после выполнения операции частное записывается в регистр AL, остаток — в регистр АН.
Для двухбайтовых операций делимое помещается в регистры DX:AX (в DX — старшая часть, в АХ — младшая); после выполнения операции частное записывается в регистр АХ, остаток — в регистр DX.
В качестве операнда-делителя команды div можно указывать регистр (кроме сегментного) или ячейку памяти; не допускается деление на непосредственное значение. Если делитель равен 0, или если частное не помещается в назначенный регистр, возбуждается прерывание с вектором 0. Команда не воздействует на флаги процессора.
Команду div можно использовать для целочисленного деления неупакованного двоично-десятичного числа в регистре АХ не неупакованный двоично-десятичный делитель, если перед ней выполнить команду aad (см. пример 3).

Пример 1
mov AX,506 ;Делимое
mov BL,50 ;Делитель
div BL ;AL=0Ah (частное), AH=06h (остаток)

Пример 2
; В полях данных
long dd 65537 ;Делимое
;В программном сегменте
mov DX,word ptr long+2 ;DX=0001h, старшая
;часть делимого
mov AX,word ptr long ;AX=0001h, младшая
;часть делимого
mov CX,256 ;Делитель
div CX ;AX=0100h (частное),
;DX=0001h (остаток)

Пример З
mov AX,0807h ;Неупакованное BCD 87
mov DL,09h ;Неупакованное BCD 9
aad ;AX=0057h=87
div DL ;AX=0609h, т.е. 9 и 6 в остатке

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. При этом, если делитель представляет 32-битовую величину, то возможен только один вариант команды деления, когда делимое находится в парс регистров EDX:EAX. В этом случае частное будет помещено в регистр ЕАХ, остаток — в EDX.

Пример
mov ЕАХ, 0FFFFFFFh ;Младшая часть делимого
mov EDX,0 ;Старшая часть делимого
mov EBX,256 ;Делитель
div EBX ;Частное в EAX=000FFFFFh,
;Остаток в EDX=000000FFh

386+ ENTER
Создание стекового кадра для параметров процедуры
Команда enter, обычно являющаяся первой командой процедуры, выделяет заданный объем стекового пространства для локальных (автоматических) параметров процедуры, предоставляя процедуре указатель на выделенную область (в качестве такого указателя используется регистр ЕВР) и смещая указатель стека ESP так, чтобы он указывал на начало свободного стекового пространства. В результате процедура имеет возможность обращаться по ходу своего выполнения к своим локальным параметрам и, в то же время, пользоваться оставшимся пространством стека для временного сохранения в нем любых данных с помощью команд push и pop. Команда leave в конце процедуры выполняет обратные действия, возвращая стек в исходное состояние и уничтожая область локальных переменных. Локальными, как известно, называются как раз те переменные, которые существуют только в течение времени выполнения некоторой процедуры, и автоматически исчезают после се завершения.
Команды enter и leave используются многими языками высокого уровня для управления доступом к локальным переменным вложенных процедур.
Команда enter имеет два операнда. Первый (16-битовое непосредственное значение) определяет число байтов, выделяемых в стеке для локальных переменных. Для 32-разрядных приложений место в стеке выделяется двойными словами (по 4 байт), для 16-разрядных — словами (по 2 байт). Второй операнд (8-битовос непосредственное значение) задаст так называемый лексический уровень процедуры, характеризующий степень ее вложенности. В зависимости от значения лексического уровня, команда enter выполняется по-разному. При лексическом уровне, равном 0, реа-лизуется невложенная форма команды enter. В этом случае после входа в процедуру (командой call) с сохранением в стеке адреса возврата, в стек заносится текущее содержимое регистра ЕВР, в ЕВР копируется текущее значение указателя стека, а указатель стека смещается на число байтов, заданное первым операндом команды enter (рис. ПЗ). Создаваемая на сте-ке структура носит название стекового кадра, а регистр ЕВР выполняет в данном случае функцию указателя стекового кадра.

Рис. ПЗ. Состояние стека после входа в подпрограмму и выполнения команды enter 8,0 (на рисунке адреса ячеек стека уменьшаются вниз).

Подпрограмма имеет возможность обращаться к своим локальным переменным по адресам ESP-4 и ESP-8 (для случая резервирования места под две переменные). Занеся в стек по этим адресам некоторые данные (полученные в качестве параметров вызова через регистры общего назначения или созданные самостоятельно) подпрограмма может затем многократно к ним обращаться, не боясь их затирания в процессе использования стека. Поскольку команда enter настроила указатель стека на область, находящуюся за пределами локальных переменных, программа может использовать команды push для сохранения в стеке временных данных.
Команда leave, размещаемая в самом конце процедуры, перед завершающей командой ret, копирует содержимое ЕВР в ESP, освобождая (в логическом плане) область локальных переменных, и снимает со стека сохраненное там исходное содержимое ЕВР. После этого командой ret можно вернуться в вызывающую процедуру.
Поскольку первый параметр команды enter имеет размерность слова, процедура в принципе имеет возможность зарезервировать в стеке для своих локальных переменных до 64 Кбайт стекового пространства.
Лексические уровни, отличные от 0, используются в тех случаях, когда по правилам языка высокого уровня каждая вложенная процедура имеет право обращаться к локальным переменным всех вышележащих процедур, но не к процедурам, находящимся на параллельных с ней ветвях вложенности. Другими словами, область видимости переменных распространяется на все вложенные процедуры, но две подпрограммы, вызываемые из одной и той же (вышележащей) процедуры, «не видят» друг друга.
В таких случаях главной процедуре назначается лексический уровень 1, все вызываемые из нее подпрограммы получают значение лексического уровня 2, подпрограммы, вызываемые из этих процедур, имеют уровень 3 и т.д. Команды enter при ненулевом значения второго параметра создают в стеке стековые кадры с более сложной структурой. Отличие такого стекового кадра от рассмотренного выше заключается в том, что в него, помимо области локальных переменных, входят также указатели стековых кадров всех вышележащих процедур. В результате любая подпрограмма может с помощью своего указателя (т.е. содержимого ESP) обратиться к собственных! переменным, а используя хранящиеся в стеке указатели кадров вышележащих процедур, «дотянуться» и до их локальных переменных. По-прежнему команды leave освобождают стек от стековых кадров вместе со всеми находящимися в них данными.

Пример
;Вызывающая процедура
call subrl
;Подпрограмма subrl
subrl proc
enter2048,0 ;Место под локальные данные
. . . ;Работа с локальными данными
leave
ret

HLT Останов
Команда hlt прекращает выполнение программы и переводит процессор в состояние останова. Работа процессора возобновляется после операции запуска, а также в случае прихода немаскируемого или разрешенного маскируемого прерываний.

IDIV Деление целых чисел со знаком
Команда IDIV выполняет деление целого числа со знаком, находящегося в регистрах АХ (в случае деления на байт) или DX:AX (в случае деления на слово), на операнд-источник (целое число со знаком). Размер делимого в два раза больше размеров делителя и остатка. Оба результата рассматриваются как числа со знаком, причем знак остатка равен знаку делимого.
Для однобайтовых операций делимое помещается в регистр АХ; после выполнения операции деления частное записывается в регистр AL, остаток — в регистр АН.
Для двухбайтовых операций делимое помещается в регистры DX:AX (в DX — старшая часть, в АХ — младшая); после выполнения операции деления частное записывается в регистр АХ, остаток — в регистр DX.
В качестве операнда-делителя команды idiv можно указывать регистр данных или ячейку памяти; не допускается деление на непосредственное значение. Если делитель равен 0, или если частное не помещается в назначенный регистр, возбуждается прерывание через вектор 0. Команда не воздействует на флаги процессора.

Пример 1
mov AX,506 ;Делимое
mov BL,50 ;Делитель
idiv BL ;AL=0Ah (частное), AH=06h
; (остаток)

Рис.П3. Состояние стека после входа в подпрограмму и выполнения команды enter8,0(на рисунке адреса ячеек уменьшаются в низ)

Пример 2
;В полях данных
long dd 0F0007h ;Делимое
; В программном сегменте
mov DX,word ptr long+2;DX=000Fh, старшая
;часть делимого
mov AX,word ptr long;AX=0007h, младшая
;часть делимого
mov CX,256 ;Делитель
idiv СХ ;AX=0F00h (частное),
;DX=0007h (остаток)
Пример 3
mov AX,-506 ;AX=FE06h, делимое
mov BL,50 ;Делитель
idiv BL ;AL=F6h (-10), AH=FAh (-6)

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. При этом, если делитель представляет 32-битовую величину, то возможен только один вариант команды деления, когда делимое находится в парс регистров EDX:EAX. В этом случае частное будет помещено в регистр БАХ, остаток — в EDX.

Пример
; В полях данных
dvd dq -100001 Делимое
;B программном сегменте
mov EAX,dword ptr dvd EAX=FFFE795Fh
;(младшая часть делимого)
mov EDX,dword ptr dvd+4 EDX=FFFFFFFFh (старшая
;часть делимого)
mov EBX,50 Делитель
idiv EBX Частное в EAX=FFFFF830h=
;-2000, остаток в EDX=FFFFFFFFh=-1

IMUL Умножение целых чисел со знаком
Команда IMUL выполняет умножение целого числа со знаком, находящегося в регистре AL (в случае умножения на байт) или АХ (в случае умножения на слово), на операнд-источник (целое число со знаком). Размер произведения в два раза больше размера сомножителей.
Для однобайтовых операций один из сомножителей помещается в регистр AL; после выполнения операции произведение записывается в регистр АХ.
Для двухбайтовых операций один из сомножителей помещается в регистр АХ; после выполнения операции произведение записывается в регистры DX:AX (в DX — старшая часть, в АХ — младшая).
В качестве операнда-сомножителя команды imul можно указывать регистр (кроме сегментного) или ячейку памяти; не допускается умножение на непосредственное значение. Команда воздействует на флаги OF и CF. Если АН или DX представляют собой просто знаковое расширение AL или АХ, соответственно (т.е. результат умножения со знаком верен), OF и CF сбрасываются в 0; в противном случае (результат со знаком не помещается в АХ или DX:AX) OF и CF устанавливаются в 1.

Пример 1
mov AL,5 ;Первый сомножитель
mov BL,3 ;Второй сомножитель
imul BL ;AX=000Fh (произведение)

Пример 2
mov AX,256 ;Первый сомножитель
mov BX,256 ;Второй сомножитель
imul BX ;DX=0001h, AX=0000h
;(число 65536)

Пример 3
mov AL,-5 ;AL=FBh
mov BL,3 ;BL=03h
imul BL ;AX-‘FFF1h (-15)

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. Имеются также варианты команды с двумя и тремя операндами.
Для команды imul с одним операндом второй сомножитель должен располагаться в AL, АХ или ЕАХ. Процессор выбирает размерность второго сомножителя, исходя из размерности первого, указанного в качестве операнда. 16-, 32- или 64-битовый знаковый результат помещается в регистры АХ, DX:AX или EDX:EAX, соответственно. Если после операции умножения содержимое АН, DX или EDX является лишь знаковым расширением AL, АХ или ЕАХ, соответственно, то флаги CF и OF сбрасываются в 0. В противном случае они устанавливаются в 1.
Для команды imul с двумя операндами их произведение записывается в первый операнд; второй операнд не изменяется. В качестве первого операнда могут выступать 16- или 32-разрядные регистры общего назначения; в качестве второго операнда — 16- или 32-разрядные регистры общего назначения, 16- или 32-битовые ячейки памяти или непосредственное значение. Оба операнда должны иметь один размер. Если результат умножения помещается в первый операнд, флаги CF и OF сбрасываются в 0. В противном случае они устанавливаются в 1.
Для команды imul с тремя операндами произведение второго и третьего операндов записывается в первый операнд. В качестве первого операнда могут выступать 16- или 32-разрядные регистры общего назначения; в качестве второго операнда — 16- или 32-разрядные регистры общего назначения или 16- или 32-битовые ячейки памяти; в качестве третьего операнда — только непосредственное значение. Два первых операнда должны иметь один размер. Если результат умножения помещается в первый операнд, флаги CF и OF сбрасываются в 0. В противном случае они устанавливаются в 1.

Пример 1
mov EAX,-1 ;Первый сомножитель
mov ESI,100000000 ;Второй сомножитель
imul ESI ;EDX=FFFFFFFFh,
;EAX=FA0AlF00h
;Результат=-100000000

Пример 2
;В полях данных
ор2 dd 100h ;Первый сомножитель
; В программном сегменте
mov EAX,400000h ;Второй сомножитель
imul EAX,op2 ;EAX=40000000h
Пример 3
mov BX,300h
imul АХ,ВХ,4 ;AX=300h*4=0C00h

IN Ввод из порта
Команда in вводит в регистр AL или АХ соответственно байт или слово из порта, указываемого вторым операндом. Адрес порта помещается в регистр DX. Если адрес порта не превышает 255, он может быть указан непосредственным значением. Указание регистра-приемника (AL или АХ) обязательно, хотя с другими регистрами команда in не работает, и их указывать нельзя. Команда не воздействует на флаги процессора.

Пример 1
in AL, 60h ;Ввод байта из порта 60h

Пример 2
mov DX,3D5h ;Адрес порта
in AL,DX ;Ввод байта из порта 3D5h

386+ Допустимо использование в качестве операнда-приемника расширенного регистра ЕАХ (если адресуемое устройство позволяет прочитать из его порта двойное слово).

Пример
mov DX,345h ;Адрес порта
in EAX,DX

INC Инкремент (увеличение на 1)
Команда inc прибавляет 1 к операнду, в качестве которого можно указывать регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Операнд интерпретируется как число без знака. Команда воздействует на флаги OF, SF, ZF, AF и PF. Команда не воздействует на флаг CF; если требуется воздействие на этот флаг, необходимо использовать команду add op,l.

Пример 1
mov AX,0563h
inc AX ;AX=0564h

Пример 2
mov BH,15h
inc BH ;BH=16h

Пример 3
mov AX,A5FFh
inc AL ;AX=A500h
inc AH ;AX=A600h

Пример 4
mov AX,0FFFFh
inc AX ;AX=0000h, ZF=1, CF=0
;Для сравнения:
mov CX,0FFFFh
add CX,1 ;CX=0000h, ZF=1, CF=1

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
;В полях данных
mem dd 12345678h
; В программном сегменте
inc mem ;mem=12345679h

386+
INS Ввод строки из порта
INSB Ввод байта из порта
INSW Ввод слова из порта
INSD Ввод двойного слова из порта
Команды предназначены для ввода данных из порта непосредственно в память. Адрес порта указывается, как и для команды in, в регистре DX, при этом задание адреса порта непосредственным значением не допускается. Данные пересылаются по адресу, находящемуся в паре регистров ES:EDI. Замена сегмента не допускается. Команда insb переносит из порта 1 байт, команда insw — 1 слово, команда insd — 1 двойное слово, а команда ins может быть использована для передачи байтов, слов и двойных слов. В последнем случае размер загружаемого данного определяется описанием строки (с помощью директив db, dw или dd). После передачи данных регистр EDI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1, 2 или 4, в зависимости от размера передаваемых данных. Вариант команды ins имеет формат

ins строка, DX

(что не избавляет от необходимости инициализировать регистры ES:EDI адресом строки).
Если устройство, адресуемое через порт, может передавать последовательность данных, то команды ins можно предварить префиксом повторения rep. В этом случае из порта принимается СХ элементов данных заданного размера.
Команды ins не воздействуют на флаги процессора.

Пример
;В сегменте данных, адресуемых через DS
mem dw 0
;В программном сегменте
push DS
pop ES ;ES=DS
mov DI,offset mem;ES:DI -> mem
mov DX,303h ;Адрес порта
insw ;Ввод из порта 16-битового данного

INT Программное прерывание
Команда hit инициирует в процессоре процедуру прерывания, в результате которой управление передается на обработчик прерывания с номером n, который указан в качестве операнда команды int. В стек текущей программы заносится содержимое регистра флагов, сегментного регистра CS и указателя команд IP, после чего в регистры IP и CS передается содержимое двух слов из вектора прерывания с номером n (расположенных по адресам 0:n*4 и 0:n*4+2). Команда сбрасывает флаги IF и TF в 0. Команда iret, которой всегда завершается обработчик прерывания, восстанавливает исходное состояние этих флагов.

Пример 1
int 60h ;Переход на прикладной
;обработчик прерывания 60h

Пример 2
mov AH,1 ;Функция MS-DOS — ввод с
;клавиатуры кода ASCII символа
int 2h ;Вызов MS-DOS

Пример 3
mov АН, 0 ;Функция BIOS (прерывание
;16h) — ввод с клавиатуры
;кода ASCII и скен-кода символа
int 16h ;Вызов BIOS

INTO Прерывание по переполнению
Команда into, будучи установлена вслед за какой-либо арифметической, логической или строковой командой, вызывает обработчик прерываний через вектор 4, если предшествующая команда установила флаг переполнения OF. Перед использованием команды INTO прикладной программист должен поместить в вектор прерывания 4 двухсловный адрес своей программы обработки прерывания по переполнению. Команда сбрасывает флаги IF и TF в 0. Команда iret, которой всегда завершается обработчик прерывания, восстанавливает исходное состояние этих флагов.

Пример
add AX,BX ;Произвольная команда
into ;Вызов прикладного
;обработчика через вектор 4,
;если OF=1
… ;Продолжение программы, если OF=0

IRET Возврат из прерывания
Команда iret возвращает управление прерванному в результате аппаратного или программного прерывания процессу. Команда извлекает из стека три верхние слова и помещает их в регистры IP, CS и флагов (см. команду int). Командой iret должен завершаться любой обработчик прерываний, как аппаратных, так и программных (от команды int). Команда не воздействует на флаги, однако она загружает в регистр флагов из стека его исходное содержимое, которое было там сохранено процессором в процессе обслуживания прерывания. Если требуется, чтобы после возврата из обработчика программного прерывания командой iret какие-либо флаги процессора были установлены требуемым образом (весьма распространенный прием), их установку надо выполнить в копии флагов в стеке.

386+ IRETD
Возврат из прерывания в 32-разрядном режиме
Команда iretd используется в защищенном режиме для возврата из обработчика прерывания или исключения, а также для переключения на исходную задачу. В отличие от 16-разрядной команды iret, данная команда, завершая обработку прерывания или исключения, снимает со стека 3 двойных слова, содержащие расширенный регистр флагов EFALGS, CS и расширенный указатель команд EIP. В случае переключения задач команда iretd выполняет переключение контекстов задач — сохранение состояния завершающейся задачи в ее сегменте состояния задачи и загрузку регистров процессора из сегмента состояния исходной задачи.

Jcc Команды условных переходов
Команды, обозначаемые (в книгах, не в программах!) Jcc, осуществляют переход по указанному адресу при выполнении условия, заданного мнемоникой команды. Если заданное условие не выполняется, переход не осуществляется, а выполняется команда, следующая за командой Jcc. Переход может осуществляться как вперед, так и назад в диапазоне + 127…-128 байтов.
В составе команд процессора предусмотрены следующие команды условных переходов:
Команда Перейти, если Условие перехода
ja выше CF=0 и ZF=0
jae выше или равно CF=0
jb ниже CF=1
jbe ниже или равно CF=1 или ZF=1
jc перенос CF=1
jcxz CX=0 CX=0
je равно ZF=1
jg больше ZF=0 или SF=OF
jge больше или равно SF=OF
jl меньше SF не равно OF
jle меньше или равно ZF=1 или SF не равно OF
jna не выше CF=1 или ZF=1
jnae не выше и не равно CF=1
jnb не ниже CF=0
jnbe не ниже и не равно CF=0 и ZF=0
jnc нет переноса CF=0
jne не равно ZF=0
jng не больше ZF=1 или SF не равно OF
jnge не больше и не равно SF не равно OF
jnl не меньше SF=OF
jnle не меньше и не равно ZF=0 и SF=OF
jno нет переполнения OF=0
jnp нет четности PF=0
jns знаковый бит равен О SF=0
jnz не нуль ZF=0
jo переполнение OF=1
jp есть четность PF=1
jpe сумма битов четная PF=1
jpo сумма битов нечетная PF=0
js знаковый бит равен SF=1
jz нуль ZF= I
Команды условных переходов, осуществляющие переход по условию «выше — ниже», предназначены для анализа чисел без знака; команды, осуществляющие переход по условию «больше — меньше», предназначены для анализа чисел со знаком.

Пример 1
cmp СХ,0 ;CX=0?
je equal ;Если да, перейти па метку equal

Пример 2
cmp AX,1000 ;Пусть AX=8000h=32768
;(=-32768)
ja above ;32768 > 1000. Переход будет

Пример 3
cmp AX,1000h ;Пусть AX=8000h=-32768
; (=32768)
jg greater ;-32768 < 1000h. Перехода не будет Пример 4 int 21h ;Вызов системной функции jc error ;Если CF=1 (ошибка), перейти ; на метку error 386+ Команды условных переходов имеют варианты 16- и 32-разрядной адресации (при тех же мнемонических обозначениях) и могут передавать управление в диапазоне -32768...+32767 байт для сегментов с атрибутом размера 16 и в диапазоне -231...+23|-1 байт для сегментов с атрибутом размера 32. JMP Безусловный переход Команда jmp передает управление в указанную точку того же или другого программного сегмента. Адрес возврата не сохраняется. Команда не воздействует на флаги процессора. Команда jmp имеет пять разновидностей: - переход прямой короткий (в пределах -128... + 127 байтов); - переход прямой ближний (в пределах текущего программного сегмента) ; - переход прямой дальний (в другой программный сегмент); - переход косвенный ближний; - переход косвенный дальний. Все разновидности переходов имеют одну и ту же мнемонику jmp, хотя и различающиеся коды операций. Во многих случаях транслятор может определить вид перехода по контексту, в тех же случаях, когда это невозможно, следует использовать атрибутные операторы: short - прямой короткий переход; near ptr - прямой ближний переход; far ptr - прямой дальний переход; word ptr - косвенный ближний переход; dword ptr - косвенный дальний переход. Примеры прямого короткого перехода jmp short shpt ;Переход на метку shpt ;в пределах +127...-128 байтов jmp shpt ;To же самое, если shpt ;находится выше по тексту программы Примеры прямого ближнего перехода jmp pt ;Переход на метку pt ;в пределах текущего сегмента jmp near ptr pt ;To же самое Примеры косвенных ближних переходов Пример 1 mov BX,offset pt ;ВХ=адрес точки перехода jmp BX ;Переход в точку pt Пример 2 ;В полях данных: addr dw pt ;Ячейка с адресом точки перехода ;В программном сегменте: jmp DS:addr ;Переход в точку pt jmp word ptr addr ;To же самое Пример 3 ; В полях данных: addr dw pt ;Ячейка с адресом точки перехода ;В программном сегменте: mov DI,offset addr ;В1=адрес ячейки с адресом ;точки перехода jmp [DI] ;Переход в точку pt Пример 4 ;В полях данных: tbl dw ptl ;Ячейка с адресом 1 dw pt2 ;Ячейка с адресом 2 dw pt3 ;Ячейка с адресом 3 ;В программном сегменте: mov BX,offset tbl ;BX=aflpec таблицы адресов переходов mov SI, 4 ;31=смещение к адресу pt3 call [BX][SI] ;Переход в точку pt3 Примеры прямых дальних переходов jmp far ptr farpt ;Переход на метку farpt в ;другом программном сегменте jmp farpt ;Переход на метку farpt в другом ;программном сегменте, если farpt ;объявлена дальней меткой ;директивой farpt label far Примеры косвенных дальних переходов Пример 1 ; В полях данных: addr dd pt ;Поле с двухсловным ;адресом точки перехода ;В программном сегменте: jmp DS:addr ;Переход в точку pt jmp dword ptr addr ;To же самое Пример 2 ; В полях данных: addr dd pt ;Поле с двухсловным ;адресом точки перехода ;В программном сегменте: mov DI,offset addr ;DI =адрес поля с адресом ;точки перехода jmp [DI] ;Переход в точку pt 386+ Допустимо использование дополнительных режимов адресации 32-разрядных процессоров. Для 32-разрядных приложений допустимо использование 32-битовых операндов. В защищенном режиме вместо сегментного адреса сегмента (при дальних переходах) выступает его селектор. LAHF Загрузка флагов в регистр АН Команда lahf копирует флаги SF, ZF, AF, PF и CF соответственно в разряды 7, 6, 4, 2 и 0 регистра АН. Значение битов 5, 3 и 1 регистра АН не определено. Команда не имеет параметров и не изменяет флаги процессора. Команда lahf (совместно с командой sahf) дает возможность читать и изменять значения флагов процессора, в том числе флагов SF, ZF, AF и PF, которые нельзя изменить непосредственно. Однако следует иметь в виду, что команда lahf переносит в АН только младший байт регистра флагов. Поэтому нельзя изменить с ее помощью, например, состояние флага OF. Пример 1 lahf ;Регистр АН отображает ;состояние регистра флагов or AH,80h ;Установка бита 7 = SF sahf ;Загрузка АН в регистр ;флагов, где теперь SF = 1 Пример 2 lahf ;Регистр АН отображает ;состояние регистра флагов and AH,0BFh ;Сброс бита 6 = ZF sahf ;Загрузка АН в регистр ;флагов, где теперь ZF = О 386Р+ LAR Загрузка прав доступа Команда lar загружает в первый операнд (16- или 32-разрядный регистр) поле атрибутов сегмента из дескриптора сегмента, заданного селектором во втором операнде. В качестве операнда с селектором может использоваться 16- или 32-разрядный регистр или ячейка памяти. В операнд-приемник поступают два байта атрибутов селектора с замаскированным полем старших битов границы сегмента. LDS Загрузка указателя с использованием регистра DS Команда Ids считывает из памяти по указанному адресу двойное слово (32 бит), содержащее указатель (полный адрес некоторой ячейки), и загружает младшую половину указателя (т.е. относительный адрес) в указанный в команде регистр, а старшую половину указателя (т.е. сегментный адрес) в регистр DS. Таким образом, команда Ids reg, mem эквивалентна следующей группе команд: mov reg,word ptr mem mov DS,word ptr mem+2 В качестве первого операнда команды Ids указывается регистр общего назначения; в качестве второго - ячейка памяти с двухсловным содержимым. Указатель, содержащийся в этой ячейке, может быть адресом как процедуры, так и поля данных. Команда не воздействует на флаги процессора. Пример 1 ; В полях данных: addr dd myproc ;Двухсловный адрес процедуры ;myproc ;В программном сегменте: Ids SI,addr ;DS:SI -> myproc

Пример 2
; В полях данных:
mem dw 25 ;Ячейка памяти с
; произвольным содержимым
addr dd myproc ;Двухсловный адрес этой
;ячейки
;В программном сегменте:
mov BX,offset addr ;ВХ=адрес ячейки addr
Ids DX, [BX] ;DХ=смещение ячейки mem,
;DS=сегментный адрес ячейки
;mem

Пример 3
; В полях данных:
dptr dd procl ;Полный адрес процедуры
; р г о с 1
dd proc2 ;Полный адрес процедуры
;ргос2
dd ргосЗ ;Полный адрес процедуры
; р г о с 3
;В программном сегменте:
mov SI, 8 ; Смещение к адресу ргосЗ
Ids DI,dptr[SI] ;DS:DI ® ргосЗ

386+ Допустимо использование 32-разрядного регистра-приемника и 32-битового смещения в памяти, а также дополнительных режимов адресации 32-разрядных процессоров. В защищенном режиме вместо сегментного адреса сегмента выступает его селектор.

LEA Загрузка исполнительного адреса
Команда lea загружает в регистр, указанный в команде в качестве первого операнда, относительный адрес второго операнда (не значение операнда!). В качестве первого операнда следует указывать регистр общего назначения (не сегментный), в качестве второго — ячейку памяти. Команда

lea reg,mem

эквивалентна команде

mov reg,offset mem

но у первой команды больше возможностей описания адреса интересующей нас ячейки. Команда не воздействует на флаги процессора.

Пример 1
; В полях данных:
message db ; ‘Идут измерения’
;В программном сегменте:
lea SI,message ;DS:SI -> message

Пример 2
; В полях данных:
nmb db ‘0123456789’
;В программном сегменте:
mov SI,7 ;Смещение символа ‘7’
lea DX,nmb[SI] ;ВХ=адрес символа ‘7’

Пример 3
; В полях данных:
nmb db ‘0123456789’
;В программном сегменте:
mov BX, off set msg
mov SI, 9 ;Смещение символа ‘9’
lea SI, [BX] [SI] ;31=адрес символа ‘9’

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

386+ LEAVE Выход из процедуры высокого уровня
Команда leave выполняет действия, противоположные действиям последней команды enter. Она логически уничтожает созданный командой enter стековый кадр со всеми содержащимися в нем локальными переменными и подготавливает стек к выполнению команды irct, завершающей переход в вызывающую процедуру. Команда leave не имеет параметров. Более подробное описание и пример см. в описании команды enter.

LES Загрузка указателя с использованием регистра ES
Команда les считывает из памяти по указанному адресу двойное слово (32 бит), содержащее указатель (полный адрес некоторой ячейки), и загружает младшую половину указателя (т.е. относительный адрес) в указанный в команде регистр, а старшую половину указателя (т.е. сегментный адрес) в регистр ES. Таким образом, команда

les reg,mem

эквивалентна следующей группе команд:

mov reg,word ptr mem mov ES,word ptr mem+2

В качестве первого операнда команды les указывается регистр общего назначения; в качестве второго — ячейка памяти с двухсловным содержимым. Указатель, содержащийся в этой ячейке, может быть адресом как процедуры, так и поля данных. Команда не воздействует на флаги процессора.

Пример 1
;В полях данных:
addr dd myproc ;Двухсловный адрес процедуры
;myproc
;В программном сегменте:
les SI,addr ;ES:SI ® myproc

Пример 2
;В полях данных:
mem dw 25 ;Ячейка памяти с
;произвольным содержимым
addr dd myproc ;Двухсловный адрес этой ячейки
;В программном сегменте:
mov BX,offset addr ;ВХ=адрес ячейки addr

les DX, [BX] ; DХ=смещение ячейки mem,
;ЕS=сегментный адрес ячейки mem

Пример 3
;В полях данных:
dptr dd procl ;Полный адрес процедуры prod
dd proc2 ;Полный адрес процедуры ргос2
dd ргосЗ ;Полный адрес процедуры ргосЗ
;В программном сегменте:
mov SI, 8 ;Смещение к адресу ргосЗ
les DI,dptr[SI] ;ES:DI -> ргосЗ

386+ Допустимо использование 32-разрядного регистра-приемника и 32-битового смещения в памяти, а также дополнительных режимов адресации 32-разрядных процессоров. В защищенном режиме вместо сегментного адреса сегмента выступает его селектор.

386+
LFS Загрузка указателя с использованием регистра FS
LGS Загрузка указателя с использованием регистра FS
LSS Загрузка указателя с использованием регистра FS
Команды считывают из памяти полный указатель, состоящий из селектора и 16-битового или 32-битового смещения, и загружают младшую половину указателя (т.е. относительный адрес) в указанный в команде регистр общего назначения, а старшую половину указателя (т.е. селектор) в сегментный регистр, указанный в мнемонике команды.
В качестве первого операнда всех перечисленных команд указывается 16- или 32-разрядный регистр общего назначения; в качестве второго — ячейка памяти с 32- или 48-битовым содержимым. Команда не воздействует на флаги процессора.
Примеры см. в описании команд Ids и les.

386Р+ LGDT
Загрузка регистра таблицы глобальных дескрипторов
Команда Igdt загружает регистр таблицы глобальных дескрипторов (GDTR) из 48-битового псевдодескриптора, содержащего 32-битовый базовый адрес и 16-битовую границу таблицы глобальных дескрипторов, находящейся в памяти. В качестве операнда команды Igdt выступает относительный адрес псевдодескриптора.

386Р+ LIDT
Загрузка регистра таблицы дескрипторов прерываний
Команда lidt загружает регистр таблицы дескрипторов прерываний (IDTR) из 48-битового псевдодескриптора, содержащего 32-битовый базовый адрес и 16-битовую границу таблицы дескрипторов прерываний, находящейся в памяти. В качестве операнда команды lidt выступает относительный адрес псевдодескриптора.

386Р+ LLDT
Загрузка регистра таблицы локальных дескрипторов
Команда lldt загружает регистр таблицы локальных дескрипторов (LDTR) селектором, определяющим таблицу локальных дескрипторов (LDT). Селектор LDT должен входить в таблицу глобальных дескрипторов. В качестве операнда команды lldt, содержащего селектор LDT, можно использовать 16- или 32-разрядный регистр общего назначения или 16-или 32-битовое поле памяти.

386Р+ LMSW Загрузка слова состояния машины
Команда Imsw загружает в регистр слова состояния машины (так называется младшая половина управляющего регистра процессора CRO) слово состояния машины, взятое из указанного в команде операнда. В качестве операнда можно использовать 16- или 32-разрядный регистр общего назначения или 16- или 32-битовое поле памяти.
Команду Imsw можно использовать для перевода процессора из реального в защищенный режим или наоборот. В первом случае после чтения слова состояния командой smsw надо установить в нем бит 0 (бит РЕ) и загрузить назад в CRO командой Imsw. Во втором случае после после чтения слова состояния командой smsw надо сбросить в нем бит 0 и загрузить назад в CRO командой Imsw.

LOCK Запирание шины
Префикс lock, помещенный перед командой, устанавливает сигнал на линии LOCK системной шины и запрещает доступ к шине другим процессорам на время выполнения данной команды. Этот префикс предназначен для использования в многопроцессорных многозадачных системах для обеспечения исключительного доступа к памяти данного процесса (и данного процессора) на время проверки или модификации некоторой ячейки памяти. Типичный пример операций такого рода — работа с семафорами.
Если два (или более) процесса, идущие на разных процессорах, используют какой-либо общий ресурс, например, файл или лазерный диск, то необходимо обеспечить механизм, запрещающий одновременное обращение процессов к общему ресурсу. Эта задача решается с помощью семафора — ячейки памяти (байта или даже бита), состояние которого отражает доступность или, наоборот, занятость ресурса.
Если процессору понадобился общий ресурс, он читает состояние семафора и, если ресурс занят, продолжает опрашивать семафор до тех пор, пока другой процессор, использующий в настоящий момент общий ресурс, не освободит его. Обнаружив, что ресурс свободен, первый процессор устанавливает семафор в состояние «занят» и использует ресурс. Закончив работу с ресурсом, процессор сбрасывает его семафор и дает возможность другому процессу обратиться к тому же ресурсу.
Описанный алгоритм будет работать только в том случае, если операция чтения-модификации-записи семафора будет выполняться в непрерываемом режиме. В противном случае оба процесса могут, одновременно обратившись к семафору, увидеть, что ресурс свободен, и начать его совместное использование. Избежать такой ситуации и позволяет префикс lock в сочетании с командами типа btr или bts, выполняющими комплексные операции проверки и сброса или проверки и установки бита.
Будем считать, что семафор расположен в бите 0 байта по адресу sem, причем сброшенное состояние бита свидетельствует о занятости ресурса, а установленное состояние о том, что ресурс свободен. Тогда типичная процедура ожидания освобождения ресурса выглядит следующим образом:

mov SI,offset sem ;Адрес байта с семафором
getsem:
lock btr byte ptr [SI],1 ;Проверка и сброс бита 0
jnc getsem

Проверка состояния семафора и его модификация (запись в бит семафора 0, т.е. признака «занят») осуществляется в одной команде btr. На время выполнения этой команды шина многопроцессорной системы блокируется префиксом lock, и другой процессор обратиться к тому же семафору не может. Блокировка шины снимается уже после того, как семафор будет переведен в состояние занятости.
Если при обращении к байту sem оказывается, что в битс семафора записан 0, т.е. ресурс занят другим процессом, команда btr сбрасывает флаг CF (путем переноса в него содержимого анализируемого бита), что приводит к многократному повторению процедуры getsem, т.е. к циклу ожидания освобождения ресурса.
Типичная процедура освобождения занятого данным процессом ресурса выглядит следующим образом:

freesem:
lock bts byte ptr [SI],1 ;Проверка и установка бита 0

Собственно говоря, никакая проверка здесь не выполняется, однако процесс будет освобождать ресурс лишь если он этот ресурс использует, и проверять состояние флага в этой операции нет необходимости. Однако и здесь необходимо запирание шины на время записи в бит семафора 1, чтобы исключить одновременное обращение двух процессов к одной ячейке памяти.
386+ Префикс lock может быть использован только со следующими командами (и лишь при условии, что при их выполнении происходит обращение к памяти): adc, add, and, bt, bts, btr, btc, or, sbb, sub, xor, xchg, dec, inc, neg, not.

LODS Загрузка операнда из строки
LODSB Загрузка байта из строки
LODSW Загрузка слова из строки
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они загружают в регистр AL (в случае операций над байтами) или АХ (в случае операций над словами) содержимое ячейки памяти по адресу, находящемуся в паре регистров DS:SI. Команда lodsb загружает 1 байт, команда lodsw — 1 слово, а команда lods может быть использована для загрузки как байтов, так и слов. В последнем случае размер загружаемого данного определяется описанием строки (с помощью директив db или dw). После операции загрузки регистр SI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2, в зависимости от размера загружаемого элемента. Команда не имеет параметров и не воздействует на флаги процессора.
Вариант команды lods имеет формат

lods строка

(что не избавляет от необходимости инициализировать регистры DS:SI адресом строки). В этом формате возможна замена сегмента строки строка:

lods ES:строка

Пример 1
;В полях данных сегмента данных, адресуемого через DS:
str db ‘qwertyuiop’
; В программном сегменте:
сld ;Двигаемся по строке вперед
mov SI, off set str ;Адрес строки
add SI,BX ;Добавим смещение (пусть ВХ=4)
lodsb ;AL=’t’, SI -> ‘у’

Пример 2
;В полях данных сегмента данных, адресуемого через ES:
str db ‘qwertyuiop’
;В программном сегменте:
сld ;Двигаемся по строке вперед
mov SI,offset str ;Адрес строки
lodsbES:str ;AL=’q’, ES:SI -> ‘w’

386+ LODSD Загрузка двойного слова из строки
Команда аналогична командам МП 86 lodb и lodsw, но позволяет загрузить из строки, адресуемой через регистры DS:ESI (DS:SI для 16-разрядных приложений), двойное слово в регистр ЕАХ.

Пример
; В полях данных
dat dd 12789,200000,550000,8000000
;В программном сегменте
mov SI,offset dat
add SI, 4*3 ;DS:SI -> 4-й элемент массива чисел
lodsd ;EAX=8000000

LOOP Циклическое выполнение, пока содержимое СХ не равно нулю
Команда loop выполняет декремент содержимого регистра СХ, и если оно не равно 0, осуществляет переход на указанную метку вперед или назад в том же программном сегменте в диапазоне -128… + 127 байт. Обычно метка помещается перед первым предложением тела цикла, а команда loop является последней командой цикла. Содержимое регистра СХ рассматривается как целое число без знака, поэтому максимальное число повторений группы включенных в цикл команд составляет 65536 (если перед входом в цикл СХ=0). Команда не воздействует на флаги процессора.

Пример 1
;В полях данных:
array dw 4096 dup (?) ;Массив из 4096 слов
;В программном сегменте:
lea BX,array ;ВХ -> array
xor SI,SI ;SI=0
mov CX,4096 ;Счетчик повторений
mov AX,1 ;Число-заполнитель
array: mov [BX] [SI],AX ;Очистка элемента массива
inc SI ;Сдвиг к следующему
inc SI ;слову массива
loop array ;Повторить СХ раз

Пример 2
mov CX,20
delay :loop delay ;Небольшая задержка

386+ При использовании в качестве счетчика расширенного регистра ЕСХ максимальное число шагов в цикле увеличивается до 232. Для того чтобы в 16-разрядном приложении процессор при выполнении команды loop использовал не 16-разрядный регистр СХ, а 32-разрядный регистр ЕСХ, перед командой loop необходимо указать префикс замены размера адреса 67h.

Пример
mov ЕСХ,О
zzzz: db 67h ;Префикс замены размера адреса
loop zzzz ;Цикл из 232: шагов, реализующий
;программную задержку порядка минут

LOOPE/LOOPZ Циклическое выполнение,пока равно/циклическое выполнение, пока нуль
Оба обозначения представляют собой синонимы и относятся к одной команде. Команда выполняет декремент содержимого регистра СХ, и если оно не равно 0, и флаг ZF установлен, осуществляет переход на указанную метку вперед или назад в том же программном сегменте в диапазоне -128…+127 байтов. Содержимое регистра СХ рассматривается как целое число без знака, поэтому максимальное число повторений группы включенных в цикл команд составляет 65536. Команда не воздействует на флаги процессора.

Пример
;В полях данных, адресуемых через DS:
command db 80 dup (‘ ‘)
;В программном сегменте:
. . . ;Копирование в поле command строки,
;содержимое которой следует анализировать
lea SI, command ;Настроим DS:SI
сld ;Обработка вперед
mov CX,80 ;Обрабатывать не более 80
;байтов
pass: lodsb ;Загрузим в AL очередной
;символ
сmр АL, ‘ ‘ ;Пропустим все пробелы в
loopepass ;начале строки
dec SI ;Сдвиг на 1 символ назад
;DS:SI -> первый символ, отличный от пробела

386+ При использовании в качестве счетчика расширенного регистра ЕСХ максимальное число шагов в цикле увеличивается до 232. Для того, чтобы в 16-разрядном приложении процессор при выполнении команд loope/loopz использовал не 16-разрядный регистр СХ, а 32-разрядный регистр ЕСХ, перед командами loope/loopz необходимо указать префикс замены размера адреса 67h.

Пример
mov ЕСХ, 1000000 ;Предельное число шагов
хххх: … ;Тело цикла
db 67h
loopexxxx

LOOPNE/LOOPNZ Циклическое выполнение, пока не равно/циклическое выполнение, пока не нуль
Оба обозначения представляют собой синонимы и относятся к одной команде. Команда выполняет декремент содержимого регистра СХ, и если оно не равно 0, и флаг ZF сброшен, осуществляет переход на указанную метку вперед или назад в том же программном сегменте в диапазоне -128… + 127 байтов. Содержимое регистра СХ рассматривается как целое число без знака, поэтому максимальное число повторений группы включенных в цикл команд составляет 65536. Команда не воздействует на флаги процессора.

Пример
;В полях данных:
command db 80 dup (0)
;В программном сегменте:
. . . ;Копирование в поле command строки,
; содержимое которой следует анализировать
lea SI,command ;Настроим DS:SI
cld ;Обработка вперед
mov CX,80 ;Обрабатывать не более 80
;байтов
slash: lodsb ;Загрузим в AL очередной символ
cmp AL, ‘ / ‘ ;Ищем знак ‘ / ‘
loopne slash ;во всей строке
;DS:SI -> первый символ за знаком ‘/’

386+ При использовании в качестве счетчика расширенного регистра ЕСХ максимальное число шагов в цикле увеличивается до 232. Для того чтобы в 16-разрядном приложении процессор при выполнении команд loopne/loopnz использовал не 16-разрядный регистр СХ, а 32-разрядный регистр ЕСХ, перед командами loopne/loopnz необходимо указать префикс замены размера адреса 67h.

Пример
mov ЕСХ,1000000 ;Предельное число шагов
хххх: . . . ;Тело цикла
db 67h
loopne xxxx

386Р+ LSL Загрузка границы сегмента
Команда Isl загружает в первый операнд границу сегмента из дескриптора сегмента, заданного селектором во втором операнде.
В качестве первого операнда команды Isl можно использовать 16- или 32-разрядный регистр общего назначения; в качестве второго — 16- или 32-разрядный регистр общего назначения или 16- или 32-битовое поле памяти.

386Р+ LTR Загрузка регистра задачи TR
Команда Itr загружает регистр задачи TR селектором сегмента состояния задачи TSS из второго операнда, в качестве которого можно использовать 16- или 32-разрядный регистр общего назначения или 16- или 32-битовое поле памяти. Команда используется в защищенном режиме, если программный комплекс выполнен в виде нескольких самостоятельных задач, и переключения между ними осуществляются с использованием включенных в процессор аппаратных средств поддержки многозадачности.

MOV Пересылка данных
Команда mov замещает первый операнд (приемник) вторым (источником). При этом исходное значение первого операнда теряется. Второй операнд не изменяется. В зависимости от описания операндов, пересылается слово или байт. Если операнды описаны по-разному или режим адресации не позволяет однозначно определить размер операнда, для уточнения размера передаваемых данных в команду следует включить один из атрибутных операторов byte ptr или word ptr. Команда не воздействует на флаги процессора. В зависимости от используемых режимов адресации, команда mov может осуществлять пересылки следующих видов:
— из регистра общего назначения в регистр общего назначения;
— из регистра общего назначения в ячейку памяти;
— из регистра общего назначения в сегментные регистры DS, ES и SS;
— из ячейки памяти в регистр общего назначения;
— из ячейки памяти в сегментный регистр;
— из сегментного регистра в регистр общего назначения;
— из сегментного регистра в ячейку памяти;
— непосредственный операнд в регистр общего назначения;
— непосредственный операнд в ячейку памяти.
Запрещены пересылки из ячейки памяти в ячейку памяти (для этого предусмотрена команда movs), а также загрузка сегментного регистра непосредственным значением, которое, таким образом, приходится загружать через регистр общего назначения:

mov AX,seg mem ;Сегментный адрес ячейки mem
mov DS,AX ;Загрузка его в регистр DS

Нельзя также непосредственно переслать содержимое одного сегментного регистра в другой. Такого рода операции удобно выполнять с использованием стека:

push DS
pop ES ; DS копируется в ES

Примеры
;В полях данных:
memb db 5,6
memd dd 0 ;Двухсловная ячейка
;В программном сегменте:
mov DX,AX ;Из регистра в регистр
mov AL,memb ;Из памяти в регистр
mov AX,0B800h ;Непосредственное значение в
;регистр
mov ES,AX ;Из регистра в сегментный
;регистр
mov word ptr memd+2,ES ;Из сегментного
;регистра в память
mov word ptr memd, 2000;Непосредственное
;значение в память
mov BX,word ptr memb ;Слово из памяти в
;регистр (число 0605)
mov DI,word ptr memd ;Слово из памяти в
;регистр
mov ES,word ptr memd+2;Слово из памяти в
;сегментный регистр

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример 1
mov EAX,ESI

Пример 2
; В полях данных
mem dd 0
;В программном сегменте
mov mem,EBP

386Р+ MOV Пересылка в\из специальных регистров
Этот вариант команды mov (с той же мнемоникой, но другими кодами операций) используется в защищенном режиме и предназначен для обмена данными со специальными регистрами процессора: управляющими CRO…CR3, тестирования TR6 и TR7, а также регистрами отладки DRO…DR7. Один из операндов команды mov должен быть 32-разрядным регистром общего назначения, другим — один из специальных регистров процессора.

MOVS Пересылка данных из строки в строку
MOVSB Пересылка байта данных из строки в строку
MOVSW Пересылка слова данных из строки в строку
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они пересылают по одному элементу строки, который может быть байтом или словом. Первый операнд (приемник) адресуется через ES:DI, второй (источник) — через DS:SI. Операцию пересылки можно условно изобразить следующим образом:

(DS:SI) -> (ES:DI)

После каждой операции пересылки регистры SI и DI получают положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2 в зависимости от размера пересылаемых элементов. Вариант команды movs имеет формат:

movs строка_1, строка_2

В этом случае байты или слова из строки строка_2 пересылаются на место строки строка_]. Размер пересылаемых элементов определяется описанием строк (с помощью директив db или dw). Это не избавляет от необходимости инициализировать регистры ES:DI и DS:SI адресами строк строка _1 и строка_2. В этом формате возможна замена сегмента второй строки (источника):

movs строка_1, ES:строка_2

Рассматриваемые команды могут предваряться префиксом повторения rep (повторять СХ раз). После выполнения рассматриваемых команд регистры SI и DI указывают на ячейки памяти, находящиеся за теми (если DF=0) или перед теми (если DF=1) элементами строк, на которых закончились операции пересылки. Если флаг DF сброшен, то пары регистров DS:SI и ES:DI следует инициализировать начальными адресами строк-операндов; строка-источник будет пересылаться от се начала, в порядке возрастания номеров ее байтов. Если флаг DF установлен, то пары регистров DS:SI и ES:DI следует инициализировать конечными адресами строк-операндов; строка-источник будет пересылаться от ее конца, в порядке уменьшения номеров ее байтов. Команды не воздействует на флаги процессора.

Пример 1
;В полях данных основного сегмента данных,
;адресуемого через DS:
txt db ‘Урок 1’ ;Пересылаемая строка
txt_len equ S-txt ;Ee длина
;В ‘полях данных дополнительного сегмента данных,
;адресуемого через ES :
string db 80 dup (‘ ‘)
;В программном сегменте:
lea SI,txt ;DS:SI -> txt
lea DI,string+10.;ES:DI -> string+10
сld ;Движение по строке вперед
mov CX,txt_len ;Столько байтов переслать
rep movsb ;Пересылка

Пример 2
;В полях данных сегмента данных, адресуемого через DS:
txt db ‘А’,84h, ‘В’,84h, ‘A’,84h, ‘P’,
db 84h,’И’,84h,’Я’,84h,’!’,84h
txt_len=$-txt ;B программном сегменте:
mov AX,0B800h ;Сегментный адрес видеобуфера
mov ES,AX ;Инициализируем ES
;Выведем на экран текст
mov DI,1672 ;Смещение к середине экрана
lea SI,txt ;DS:SI ® txt
сld ;Движение по строке вперед
mov CX,txt_len/2 ;Столько слов переслать
rep movsw ;Пересылка в середину экрана
;красной мерцающей (атрибут
;84h) надписи ‘АВАРИЯ!’

Пример 3
;В полях данных сегмента данных, адресуемого через DS:
datal dw 10000 dup(‘) ;Массив произвольных данных
data2 dw 5000 dup(‘) ;Массив-приемник
;В программном сегменте
push DS ;Настроим
pop ES ;ES на тот же сегмент данных
mov SI,offset datal;SI -> datal
add SI,5000 ;Сместим SI к середине
;массива
mov DI,offset data2;DI -> data2
mov CX,2500 ;Размер половины массива (в
;словах)
сld ;Движение вперед
rep movsw ;Перешлем вторую половину
;массива datal на место data2

Пример 4
;В полях данных сегмента, адресуемого через DS
file db ‘MYFILE.01.DAT1,0 ;Строка-источник
fname db 128 dup(?) ;Строка-приемник
;В программном сегменте
push DS
pop ES ;Теперь ES=DS
mov SI,offset file;DS:SI -> strl
mov SI,128 ;Максимальная длина имени
;файла
сld ;Движение по строке вперед
null: lodsb ;Загрузим в AL очередной
; символ
cmp AL, 0 ;Ищем 0 в конце имени файла
loopne null
;DS:SI ® Первый символ за концом имени файла (за
;завершающим нулем)
dec SI ;SI -> байт с 0 std ;Движение по строке назад
mov ВХ,128 ;Из начального значения СХ
sub BX,CX ;вычтем то, что в СХ осталось
mov СХ,ВХ ;СХ=число символов в имени (с 0)
dec ВХ ;Смещение к 0 от начала имени файла
lea DI,fname[ВХ] ;Смещение завершающего 0
rep movsb ;Перешлем все имя (от конца к началу)

386+ MOVSD Пересылка двойного слова из строки в строку
Команда аналогична командам МП 86 movsb и movsw, но позволяет скопировать двойное слово из строки, адресуемой через регистры DS:ESI, в строку, адресуемую через регистры ES:EDI.

Пример 1
;В полях данных сегмента, адресуемого через DS
strl db ‘01234567890ABCDEF’ ;Строка-источник
str2 db 16 dup(?) ;Строка-приемник
;B программном сегменте
push DS
pop ES ;Теперь ES=DS
mov SI,offset strl ;DS:SI ®strl
mov DI,offset str2 ;ES:DI -> str2
сld ;Движение по строке вперед
mov CX,4 ;Коэффициент повторения
rep movsd ;Копирование по 4*4 байт

386+ MOVSX Пересылка с расширением знака
Команда пересылает байт в слово или двойное слово, а также слово в двойное слово с расширением знака. В качестве первого операнда (приемника) может использоваться 16- или 32-разрядный регистр общего назначения, в качестве второго — 8- или 16-разрядный регистр общего назначения или ячейка памяти такого же размера. Недопустима пересылка из памяти в память, в или из сегментного регистра, а также непосредственного значения. Фактически команда movsx увеличивает размер как положительного, так и отрицательного числа, ни изменяя ни его значения, ни знака.

Пример 1
mov CL,-5 ;CL=FBh
movsxAX,CL ;AX=FFFBh

Пример 2
mov CL,+5 ;CL=05h
movsxAX,CL ;AX=0005h

Пример 3
mov BL,-128 ;BL=80h
movsxECX,BL ;ECX=FFFFFF80h

Пример 4
; В полях данных
mem dw -3 ;mem=FFFDh
;В программном сегменте
movsxEB-X,mem ; EBX=FFFFFFFDh

386+ MOVZX Пересылка с расширением нуля
Команда пересылает байт в слово или двойное слово, а также слово в двойное слово с заполнением старших разрядов нулями. В качестве первого операнда (приемника) может использоваться 16- или 32-разрядный регистр общего назначения, в качестве второго — 8- или 16-разрядный регистр общего назначения или ячейка памяти такого же размера. Недопустима пересылка из памяти в память, в или из сегментного регистра, а также непосредственного значения. Фактически команда movzx увеличивает размер числа, считая его числом без знака.

Пример 1
mov CL,5 ;CL=05h
movsxAX,CL ;AX=0005h

Пример 2
mov CL,-5 ;CL=FBh
movsxAX,CL ;AX=00FBh

Пример 3
mov BL,80h ;BL=80h
movsxECX,BL ;ECX=00000080h

Пример 4
;B полях данных
mem dw 0FFFFh ;mem=FFFFh
;B программном сегменте
movsxEBX,mem ;EBX=0000FFFFh

MUL Умножение целых чисел без знака
Команда inul выполняет умножение целого числа без знака, находящегося в регистре AL (в случае умножения на байт) или АХ (в случае умножения на слово), на операнд-источник (целое число без знака). Размер произведения в два раза больше размера сомножителей.
Для однобайтовых операций один из сомножителей помещается в регистр AL; после выполнения операции произведение записывается в регистр АХ.
Для двухбайтовых операций один из сомножителей помещается в регистр АХ; после выполнения операции произведение записывается в регистры DX:AX (в DX — старшая часть, в АХ — младшая). Предыдущее содержимое регистра DX затирается.
Если содержимое регистра АН после однобайтового умножения или содержимое регистра DX после двухбайтового умножения не равны 0, флаги CF и OF устанавливаются в 1. В противном случае оба флага сбрасываются в 0.
В качестве операнда-сомножителя команды mul можно указывать регистр (кроме сегментного) или ячейку памяти; не допускается умножение на непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
mov AL,5 ;Первый сомножитель
mov BL,3 ;Второй сомножитель
mul BL ;AX=000Fh, произведение

Пример 2
mov AX,256 ;Первый сомножитель
mov BX,256 ;Второй сомножитель
mul BX ;DX=0001h, AX=0000h
;(DX:AX=65536, произведение)

Пример 3
;В полях данных
coef db 100 ;Первый сомножитель
datal db 126 ;Второй сомножитель
mov AL,datal ;AL=7Eh=126
mul coef ;AX=3138h=12600,произведение

Пример 4
;B полях данных
coef dw 5000 ;Первый сомножитель
datal dw 1200 ;Второй сомножитель
mov AX,datal ;AX=4BOh=1200
mul coef ;DX=005Bh, AX=8D80h
;Произведение=ВХ:AX=
;5B8D80h=6000000

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. При этом, если указанный операнд представляет собой 32-байтовую величину, то результат размещается в регистрах EDX:EAX.

Пример 1
mov EAX,200h ;Первый сомножитель
mov ESI,l000000lh ;Второй сомножитель
mul ESI ;Произведение в EDX:EAX
;EDX=00000020h,EAX=00000200h

Пример 2
; В полях данных
nmbs db 15,22,36,78,84,98,100
;В программном сегменте
mov EDX,offset nmbs ;Относительный адрес
movzx EDX,DX ;массива
mov ECX,5 ;Смещение в массиве
mov AL,10 ;Множитель
mul byte ptr [EDX] [ЕСХ] ;Умножаем элемент
;массива с индексом 5 (98)
;на AL (10) Результат в
;АХ=980

NEG Изменение знака, дополнение до 2
Команда neg выполняет вычитание целочисленного операнда со знаком из нуля, превращая положительное число в отрицательное и наоборот. Исходный операнд затирается. В качестве операнда можно указывать регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov АХ,О О 01
neg AX ;AX=FFFFh=-l

Пример 2
mov BX,-2 ;BX=FFFEh=-2
neg BX ;BX=0002h

Пример 3
;В полях данных
nmb dw 800lh ;Если число со знаком,
;то -32767
;В программном сегменте
neg nmb ;nmb=7FFFh=+32767

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov ECX, 5
neg ECX ;ECX=FFFFFFFBh=-5

NOP Холостая команда
По команде пор процессор не выполняет никаких действий, кроме увеличения на 1 содержимого указателя команд IP (поскольку команда пор занимает 1 байт). Команда иногда используется в отладочных целях, чтобы «забить» какие-то ненужные команды, не изменяя длину загрузочного модуля или, наоборот, оставить место в загрузочном модуле для последующей вставки команд. В ряде случаев команды пор включаются в текст объектного модуля транслятором. Команда не имеет ни параметров, ни операндов и не воздействует на флаги процессора.

NOT Инверсия, дополнение до 1, логическое отрицание
Команда not выполняет инверсию битов указанного операнда, заменяя 0 на 1 и наоборот. В качестве операнда можно указывать регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда не воздействует на флаги процессора.

Правила побитовой инверсии:
Операнд-бит 0 1
Бит результата 1 0

Пример 1
mov AX,0FFFFh
not AX ;AX=0000h

Пример 2
mov SI,5551h
not SI ;SI=AAAEh

386+ Допустимо использование 32-битовых операндов и дополнитель ных режимов адресации 32-разрядных процессоров.

Пример
mov EAX,0C00SFF00h
not EAX ;EAX=3FFC00FFh

OR Логическое ВКЛЮЧАЮЩЕЕ ИЛИ
Команда or выполняет операцию логического (побитового) сложения двух операндов. Результат замещает первый операнд (приемник); второй операнд (источник) не изменяется. В качестве первого операнда можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды команды or могут быть байтами или словами. Команда воздействует на флаги OF, SF, ZF, PF и CF, при этом флаги CF и OF всегда сбрасываются в 0.

Правила побитового сложения:
Первый операнд-бит 0101
Второй операнд-бит 0011
Бит результата 0111

Пример 1
mov AX,000Fh
mov BX,00F0h
or AX,BX ;AX=00FFh, BX=00F0h

Пример 2
mov AX,000Fh
mov BX,00F7h
or AX,BX ;AX=00FFh, BX=00F7h

Пример 3
mov AX,000Fh
or AX,8001h ;AX=800Fh

Пример 4
; В полях данных
mask db 80h
;B программном сегменте
mov CH,17h
or CH,mask ;CH=97h

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
;В полях данных
mem dd 80000000h
; В программном сегменте
or mem,0C0h ;mem=800000C0h

OUT Вывод в порт
Команда out выводит в порт, указываемый первым операндом, байт или слово соответственно из регистра AL или АХ. Адрес порта помещается в регистр DX. Если адрес порта не превышает 255, он может быть указан непосредственным значением. Указание регистра-источника (AL или АХ) обязательно, хотя с другими регистрами команда out не работает, и их указывать нельзя. Команда не воздействует на флаги процессора.

Пример 1
mov AL,20h ;Команда конца прерывания (EOI)
out 20h,AL ;Вывод команды EOI в порт
;20h контроллера прерываний

Пример 2
mov DX,3CEh ; Адрес порта
mov AL,5 ;Данное
out DX,AL ;Вывод байта из AL в порт 3CEh

386+ Допустимо использование в качестве операнда-источника расширенного регистра ЕАХ (если адресуемое устройство позволяет записать в его порт двойное слово).

Пример
mov ЕАХ,1А008РРЗh;Пересылаемое данное
mov DX,345h ;Адрес порта
out DX,EAX ;Вывод в порт двойного слова

386+
OUTS Вывод строки в порт
OUTSB Вывод байта в порт
OUTSW Вывод слова в порт
OUTSD Вывод двойного слова в порт
Команды предназначены для вывода данных в порт непосредственно из памяти. Адрес порта указывается, как и для команды out, в регистре DX, при этом задание адреса порта непосредственным значением не допускается. Данные извлекаются из памяти по адресу, находящемуся в паре регистров DS:ESI. Замена сегмента не допускается. Команда outsb передает в порт 1 байт, команда outsw — 1 слово, команда outsd — 1 двойное слово, а команда outs может быть использована для передачи байтов, слов и двойных слов. В последнем случае размер загружаемого данного определяется описанием строки (с помощью директив db, dw или dd). После передачи данных регистр ESI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1, 2 или 4 в зависимости от размера передаваемых данных.
Вариант команды outs имеет формат

outs DX, строка

(что не избавляет от необходимости инициализировать регистры DS:ESI адресом строки).
Если устройство, адресуемое через порт, может принимать последовательность данных, то команды outs можно предварить префиксом повторения rep. В этом случае в порт пересылается СХ элементов данных заданного размера.
Команды outs не воздействуют на флаги процессора.

Пример 1
; В полях данных
mem dw 0FFh
;В программном сегменте
mov SI, offset mem;ES:DI ® mem
mov DX,303h ;Адрес порта
outsb ;Вывод в порт 8-битового
;данного

Пример 2
; В полях данных
string dw 0FFh,1,5,0Bh, 0
; В программном сегменте
mov SI, off set mem;ES:DI -> mem
mov DX,340h Адрес порта
mov CX, 5 ;Число данных
eld ;Движение по данным вперед
rep outsb ;Последовательный вывод в
;порт пяти 8-битовых данных

POP Извлечение слова из стека
Команда pop выталкивает 16-битовое данное из стека, т.е. пересылает слово из вершины стека (на которую указывает регистр SP) по адресу операнда-приемника. После этого содержимое SP увеличивается на 2, и SP указывает на предыдущее слово стека, которое теперь является его новой вершиной. Выталкивать из стека можно только целые слова (не байты). Программа должна строго следить за тем, чтобы каждой команде проталкивания в стек push отвечала обратная команда выталкивания из стека pop. Если стек используется для временного хранения некоторых данных, то извлекать эти данные из стека следует в порядке, обратном их сохранению.
В качестве операнда-приемника можно использовать любой 16-разрядный регистр (кроме CS) или ячейку памяти. Команда не воздействует на флаги процессора.
Пара команд push — pop часто используется для пересылки данного из регистра в регистр (особенно, в сегментный) через стек.

Пример 1
push AX ;Временное сохранение
push BX ;в стеке
push DS ;трех операндов
pop DS ;Восстановление из стека
pop BX ;трех операндов
pop AX ; в обратном порядке

Пример 2
push CS ;Пересылка CS через стек pop DS ;Теперь DS=CS

Пример 3
;В полях данных
mem dw 0
; В программном сегменте
pop mem ;Восстановление из стека в память

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
pop EAX ;Извлечение из стека двойного слова

386+ РОРА Восстановление из стека всех регистров
Команда рора восстанавливает из стека содержимое всех регистров, предварительно сохраненных в стеке командой pusha. Заполнение из стека регистров осуществляется в следующем порядке: DI, SI, BP, SP, ВХ, DX, СХ, АХ. Исходное содержимое указателя стека SP, сохраненное в стеке командой pusha, командой рора из стека извлекается, но отбрасывается. Команда не имеет параметров.

Пример
рора

386+ POPAD Восстановление из стека всех регистров в 32-разрядном режиме
Команда popad восстанавливает из стека содержимое всех расширенных регистров, предварительно сохраненных в стеке командой pushad. Заполнение из стека регистров осуществляется в следующем порядке: EDI, ESI, EBP, ESP, ЕВХ, EDX, ЕСХ, ЕАХ. Исходное содержимое указателя стека ESP, сохраненное в стеке командой pusha, командой рора из стека извлекается, но отбрасывается. Команда не имеет параметров.

Пример
popad

POPF Восстановление из стека регистра флагов
Команда popf пересылает верхнее слово стека (на которое указывает регистр SP) в регистр флагов FLAGS. После этого содержимое SP увеличивается на 2, и SP указывает на предыдущее слово стека, которое теперь является его новой вершиной. Команда popf не имеет параметров; она воздействует на все флаги процессора, кроме флагов VM и RF.

Пример 1
popf ;Регистр флагов загружается из стека

Пример 2
pushf ;Отправим флаги в стек
mov BP,SP ;Настроим ВР на флаги в стеке
or [BP],100h ;Установим бит 100h (флаг TF)
popf ;Вытолкнем в регистр флагов.
;Теперь в регистре флагов TF=1

386+ POPFD
Восстановление из стека расширенного регистра флагов
Команда popfd пересылает верхнее слово стека (на которое указывает регистр ESP) в расширенный регистр флагов EFLAGS. После этого содержимое ESP увеличивается на 4, и ESP указывает на предыдущее слово стека, которое теперь является его новой вершиной. Команда popfd не имеет параметров; она воздействует на все флаги процессора.

Пример
popfd ;Регистр EFLGS загружается из стека

PUSH Занесение операнда в стек
Команда push уменьшает на 2 содержимое указателя стека SP и заносит на эту новую вершину двухбайтовый операнд-источник (проталкивает в стек новое данное). Проталкивать в стек можно только целые слова (не байты). Программа должна строго следить за тем, чтобы каждой команде проталкивания в стек push отвечала обратная команда выталкивания из стека pop. Если стек используется для временного хранения некоторых данных, то извлекать эти данные из стека следует в порядке, обратном их сохранению.
В качестве операнда-источника может использоваться любой 16-разрядный регистр (включая сегментный) или ячейка памяти. Не допускается занесение в стек непосредственного значения, хотя некоторые трансляторы преобразуют команду вида

push 1234h

в неэффективную последовательность операций со стеком, результатом которой будет проталкивание указанного операнда в стек. Команда push не воздействует на флаги процессора.
Пара команд push — pop часто используется для пересылки данного из регистра в регистр (особенно, в сегментный) через стек.

Пример 1
push ES:mem ;Сохранение содержимого
push DS ;слова памяти mem из
push BP ;дополнительного сегмента
;а также регистров DS и ВР
… ;
pop PP ;Восстановление из стека
pop DS ;трех операндов
pop ES: mem ;в обратном порядке

Пример 2
push DS ;Пересылка DS через стек
pop ES ;Теперь ES=DS

386+ Допустима засылка в стек 32-битовых операндов (регистров и ячеек памяти), а также занесение в стек 8-, 16- и 32-битовых непосредственных значений. Каждое 8-битовое значение занимает в стеке целое слово. Операнды любого допустимого размера могут заноситься з стек вперемежку’, если это не вступает в противоречие с операциями по извлечению этих данных из стека.

Пример 1
push AX ; Сохранение в стеке регистра АХ
push 32h ;Сохранение в стеке
;константы 32h (она займет в
;стеке 1 слово)
push EAX ;Сохранение в стеке регистра
;ЕАХ (два слова стека)

386+ PUSHA Сохранение в стеке всех регистров
Команда pusha сохраняет в стеке содержимое всех регистров в следующем порядке: АХ, СХ, DX, ВХ, значение указателя стека SP перед выполнением данной команды, дачее ВР, SI и DI. Команда не имеет параметров и не воздействует на флаги процессора.

Пример
pusha

386+ PUSHAD Сохранение в стеке всех регистров в 32-разрядном режиме
Команда pushad сохраняет в стеке содержимое всех регистров в следующем порядке: EAX, ECX, EDX, ЕВХ, значение указателя стека ESP перед выполнением данной команды, далее EBP, ESI и EDI. Команда не имеет параметров и не воздействует на флаги процессора.

Пример
pushad

386+ PUSHFD Занесение в стек содержимого расширенного регистра флагов
Команда pushfd уменьшает на 4 содержимое указателя стека ESP и заносит на эту новую вершину содержимое расширенного регистра флагов EFALGS. При этом сохраняются все флаги процессора. Команда pushfd не имеет параметров и не воздействует на флаги процессора.

Пример
pushfd ;Содержимое регистра флагов
;сохраняется в стеке

RCL Циклический сдвиг влево через бит переноса
Команда гсl осуществляет сдвиг влево всех битов операнда. Если команда записана в формате

rcl операнд,1

сдвиг осуществляется на 1 бит. В младший бит операнда заносится значение флага CF; старший бит операнда загружается в CF. Если команда записана в формате

rcl операнд,CL

сдвиг осуществляется на число бит, указанное в регистре-счетчике CL, при этом в процессе последовательных сдвигов старшие биты операнда поступают сначала в CF, а оттуда — в младшие биты операнда (рис. П4).

Рис. П4. Действие команды rcl.

В качестве операнда команды rcl можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
clc ;Сбросим CF
mov AX, 7
rcl АХ,1 ‘ ;AX=000Eh=14, CF=0

Пример 2
stc ;Установим CF
mov DL,7
rcl DL,1 ;DL=0Fh=15, CF=0

Пример 3
clc ;Сбросим CF
mov BX,0FFFFh
rcl BX,1 ‘ ;BX=FFFEh, CF=1

Пример 4
clc ;Сбросим CF
mov DH,3
mov CL,4 ;Счетчик сдвигов
rcl DH,CL ;DH=30h=48, CF=0

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа битов сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov EAX,0С0000003h
clc ;Сбросим CF
rcl EAX,2 ;EAX=0000000Dh, CF=1

RCR Циклический сдвиг вправо через бит переноса
Команда rсl осуществляет сдвиг вправо всех битов операнда. Если команда записана в формате

rcl операнд,1

сдвиг осуществляется на 1 бит. В старший бит операнда заносится значение флага CF; младший бит операнда загружается в CF. Если команда записана в формате

rcl операнд,CL

сдвиг осуществляется на число бит, указанное в регистре-счетчике CL, при этом в процессе последовательных сдвигов младшие биты операнда поступают сначала в CF, а оттуда — в старшие биты операнда (Рис. П5).

Рис. П.5. Действие команды rcl.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
clc ;Сбросим флаг CF
mov AX, 2
rcr АХ,1 ;АХ=1, CF=0

Пример 2
stc ;Установим флаг CF
mov DL,8
rcr DL,1 ;DL=84h, CF=0

Пример 3
clc ;Сбросим флаг CF
mov BX,OFh
rcr BX,1 ;BX=7, CF=1

Пример 4
clc ;Сбросим флаг CF
mov DH,80h
mov CL,5 ;Счетчик сдвигов
rcr DH,CL ;DH=4, CF=0

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov ESI,0FFFF000lh
clc ;Сбросим флаг CF
rcr ESI,8 ;ESI=02FFFF00h, CF=0

Pentium+P RDMSR Чтение особого регистра модели
Команда читает содержимое внутреннего регистра, специфического для конкретной модели процессора.

REP Повторение
REPE Повторение пока равно
REPZ Повторение пока нуль
REPNE Повторение пока равно
REPNZ Повторение пока не равно
Префиксы повторения, позволяющие организовывать циклическое выполнение команд обработки строк cmps, movs и seas, и при этом проверять наличие указанного в префиксе условия.
Префикс rep, будучи установлен перед строковой командой movs или stos, заставляет ее выполняться СХ раз.
Префикс rере (и полностью эквивалентный ему префикс repz), будучи установлен перед строковой командой cmps или seas, заставляет ее выполняться до тех пор, пока результат выполнения равен 0 и, соответственно, ZF=1, но не более СХ раз.
Префикс rерnе (и полностью эквивалентный ему префикс repnz), будучи установлен перед строковой командой cmps или seas, заставляет ее выполняться до тех пор, пока результат выполнения не равен 0 и, соответственно, ZF=0, но не более СХ раз.
Примеры использования префиксов повторения см. в описаниях строковых команд cmps, movs и seas.

RET Возврат из процедуры
RETN Возврат из ближней процедуры
RETF Возврат из дальней процедуры
Команда ret извлекает из стека адрес возврата и передает управление назад в программу, первоначально вызвавшую процедуру. Если командой ret завершается ближняя процедура, объявленная с атрибутом near, или используется модификация команды retn, со стека снимается одно слово- относительный адрес точки возврата. Передача управления в этом случае осуществляется в пределах одного программного сегмента. Если командой ret завершается дальняя процедура, объявленная с атрибутом far, или используется модификация команды retf, со стека снимаются два слова: смещение и сегментный адрес точки возврата. В этом случае передача управления может быть межсегментной.
В команду ret может быть включен необязательный операнд (кратный 2), который указывает, на сколько байтов дополнительно смещается указатель стека после возврата в вызывающую программу. Прибавляя эту константу к новому значению SP, команда ret обходит аргументы, помещенные в стек вызывающей программой (для передачи процедуре) перед выполнением команды call. Обе разновидности команды не воздействуют на флаги процессора.

Пример 1

call subr ;Вызов подпрограммы
subr proc near
. . . ;Тело процедуры-подпрограммы
ret subr endp

Пример 2
push AX ;Параметр 1, передаваемый в
;подпрограмму
push SI ;Параметр 2, передаваемый в
;подпрограмму
call subr ;Вызов подпрограммы

subr proc near

;Извлечение из стека параметров
; (без изменения содержимого SP)
ret 4 ;Возврат в вызывающую
;программу и снятие со стека
;двух слов с параметрами
subr endp

ROL Циклический сдвиг влево
Команда rol осуществляет сдвиг влево всех битов операнда. Если команда записана в формате

rol операнд,1

сдвиг осуществляется на 1 бит. Старший бит операнда загружается в его младший разряд и одновременно заносится в флаг CF (рис. П6). Если команда записана в формате

rol операнд,CL

сдвиг осуществляется на число бит, указанное в регистре-счетчике CL, при этом в процессе последовательных сдвигов старшие биты операнда перемещаются в его младшие разряды. Последний перенесенный бит одновременно заносится в флаг CF.

Рис. П6. Действие команды rol.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
mov AX,1
rol AX,1 ;AX=0002h, CF=0

Пример 2
mov DL,8 Oh
rol DL,1 ;DL=01h, CF=1

Пример 3
mov DX,3000h
mov CL,4
rol DX,CL ;DX=0003h, CF=1

Пример 4
mov DX,2000h
mov CL,4
rol DX,CL ;DX=0002h, CF=0
386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov ЕАХ,012345678h
rol EАХ,16 ;EAX=56781234h

ROR Циклический сдвиг вправо
Команда ROR осуществляет циклический сдвиг вправо всех битов операнда. Если команда записана в формате

ror операнд,1

сдвиг осуществляется на 1 бит. Младший бит операнда записывается в его старший разряд и одновременно поступает в флаг CF (рис.Ш). Если команда записана в формате

ror операнд,CL

сдвиг осуществляется на число бит, указанное в регистре-счетчике CL, при этом в процессе последовательных сдвигов младшие биты операнда перемещаются в его старшие разряды. Последний перенесенный бит одновременно заносится в флаг CF.

Рис. П7. Действие команды ror.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
mov AX,2
ror AX,1 ;AX=0001h, CF=0

Пример 2
mov DL,81h
ror DL,1 ;DL=C0h, CF=1

Пример 3
mov BX,000Eh
mov CL,4
ror BX,CL ;BX=E000h, CF=1

Пример 4
mov BX,0009h
mov CL,4
ror BX,CL ;BX=9000h, CF=1

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov EDI,90000001h
ror EDI,12 ;EDI=00190000h

SAHF Запись содержимого регистра АН в регистр флагов
Команда sahf копирует разряды 7, 6, 4, 2 и 0 регистра АН в регистр флагов процессора, устанавливая тем самым значения флагов SF, ZF, AF, PF и CF соответственно. Команда не имеет операндов.
Команда sahf (совместно с командой lahf) дает возможность читать и изменять значения флагов процессора, в том числе флагов SF, ZF, AF и PF, которые нельзя изменить непосредственно. Однако следует иметь в виду, что команда sahf заполняет только младший байт регистра флагов. Поэтому нельзя изменить с ее помощью, например, состояние флага OF.

Пример 1
lahf ;Регистр АН отображает
;состояние регистра флагов
or АН,80h ;Установка бита 7 = SF
sahf ;Загрузка АН в регистр
;флагов, где теперь будет SF = 1

Пример 2
lahf ;Регистр АН отображает
;состояние регистра флагов
and АН,0BFh ;Сброс бита 6 = ZF
sahf ;Загрузка АН в регистр
;флагов, где теперь будет ZF = О

Пример 3
mov АН, 5
sahf ;Устанавливаются флаги PF и
;CF и сбрасывается флаги SF,
;ZF и AF

SAL Арифметический сдвиг влево
Команда sal осуществляет сдвиг влево всех битов операнда. Старший бит операнда поступает в флаг CF. Если команда записана в формате

sal операнд, 1

сдвиг осуществляется на 1 бит. В младший бит операнда загружается 0. Если команда записана в формате

sal операнд,CL

сдвиг осуществляется на число битов, указанное в регистре-счетчике CL, при этом в процессе последовательных сдвигов старшие биты операнда, пройдя через флаг CF, теряются, а младшие заполняются нулями (рис. П8.).

Рис. П8. Действие команды sal.

В качестве операнда команды sal можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение.
Каждый сдвиг влево эквивалентен умножению знакового числа на 2, поэтому команду sal удобно использовать для возведения операнда в степень 2.
Команда воздействует на флаги OF, SF, ZF, PF и CF.

Пример 1
mov AL,7
sal AL,1 ;AL= 0Eh=7*2, CF=0

Пример 2
mov AX,IFh
mov CL,8
sal AX,CL ;AX=lF00h=lFFh*256, CF=0

Пример 3
mov SI,-1 ;SI=FFFFh
mov CL,4
sal SI,CL ;SI=FFF0h=-l*16=-16, CF=1

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov EBX,0000C835h
sal EBX,5 ;EBX=001906A0h

SAR Арифметический сдвиг вправо
Команда sar осуществляет сдвиг вправо всех битов операнда. Младший бит операнда поступает в флаг CF. Если команда записана в формате

sar операнд,1

сдвиг осуществляется на 1 бит. Старший бит операнда сохраняет свое значение. Если команда записана в формате

sar операнд,CL

сдвиг осуществляется на число бит, указанное в регистре-счетчике CL, при этом в процессе последовательных сдвигов младшие биты операнда, пройдя через флаг CF, теряются, а старший бит расширяется вправо (рис. П9).

Рис. П9. Действие команды sar.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение.
Каждый сдвиг вправо эквивалентен делению знакового числа на 2, поэтому команду sar удобно использовать для деления операнда на целые степени 2. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov AL,7
sar AL,1 ;AL=3=7/2, CF=1. Остаток
;потерян

Пример 2
mov AX,lFF0h
mov CL,4
sar AX,CL ;AX=01FFh=lFF0h/16, CF=0

Пример 3
mov BX,-8 ;BX=FFF8h
mov CL,2
sar BX,CL ;BX=FFFEh=-2=-8/4, CF=0

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov EAX,0F0001234h
sar EAX,8 EAX=FFF00012h

SBB Целочисленное вычитание с займом
Команда sbb вычитает второй операнд (источник) из первого (приемника). Результат замещает первый операнд, предыдущее значение которого теряется. Если установлен флаг CF, из результата вычитается еще 1. Таким образом, если команду вычитания записать в общем виде

sbb операнд__1, операнд_2

то ее действие можно условно изобразить следующим образом:
операнд_1 — операнд_2 — CF -> операнд_1

В качестве первого операнда можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команда sbb обычно используется для вычитания 32-разрядных чисел. Команда воздействует на флаги OF, SF, ZF, PF и CF.

Пример 1
mov AX,76A5h
sbb AX,76A3h ;AX=1, если CF был = 1
;AX=2, если CF был = 0

Пример 2
; В полях данных:
numlow dw 000Ah ;Младшая часть вычитаемого
numhigh dw 0001h ;Старшая часть вычитаемого
;Число 1000Ah=65546
;В программном сегменте:
mov AX, 0 ;Младшая часть уменьшаемого
mov DX,0002 ;Старшая часть уменьшаемого
;Число 20000h=131072
sub AX, numlow ;Вычитание младших частей.
;AX=FFF6h, CF=1
sbb DX,numhigh ;Вычитание старших частей с
;займом.
;DX:AX=0000:FFF6h=65526

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
;В полях данных:
nlow dd 0Ch ;Младшая часть вычитаемого
nhi dd 40000000h ;Старшая часть вычитаемого
;Число 400000000000000Ch ;В программном сегменте:
mov EAX,0Bh ;Младшая часть уменьшаемого
mov EBX,60000000h ;Старшая часть уменьшаемого
;Число 600000000000000Bh
sub EAX,nlow ;Вычитание младших частей.
;EAX=FFFFFFFFh, CF=1
sbb EBX,nhi ;Вычитание старших частей с
;займом. EBX=lFFFFFFFh
;Разность IFFFFFFFFFFFFFFFh

SCAS Сканирование строки с целью сравнения
SCASB Сканирование строки байтов с целью сравнения
SCASW Сканирование строки слов с целью сравнения
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они сравнивают содержимое регистра AL (в случае операций над байтами) или АХ (в случае операций над словами) с содержимым ячейки памяти по адресу, находящемуся в паре регистров ES:DI. Операция сравнения осуществляется путем вычитания содержимого ячейки памяти из содержимого AL или АХ. Результат операции воздействует на регистр флагов, но не изменяет ни один из операндов. Таким образом, операцию сравнения можно условно изобразить следующим образом:

АХ или AL — (ES:DI) ® флаги процессора

После каждой операции сравнения регистр DI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2, в зависимости от размера сравниваемых элементов.
Вариант команды SCAS имеет формат

sсas строка

(что не избавляет от необходимости инициализировать регистры ES:DI адресом строки строка). Замена сегмента строки невозможна.
Рассматриваемые команды могут предваряться префиксами повторения repe/repz (повторять, пока элементы равны, т.е. до первого неравенства) и repne/repiiz (повторять, пока элементы не равны, т.е. до первого равенства). В любом случае выполняется не более СХ операций над последовательными элементами.
После выполнения рассматриваемых команд регистр DI указывает на ячейку памяти, находящуюся за тем (если DF=0) или перед тем (если DF=1) элементом строки, на котором закончились операции сравнения. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
;В полях данных сегмента данных, адресуемого через ES:
string db ‘ /Т:4’
;В программном сегменте:
сld ;Поиск вперед по строке
lea DI,string ;ES:DI ® string
mov AL, ‘ ‘ ;Символ, который мы пропускаем
mov CX,8 ;Длина строки repe scasb
;Поиск первого символа,
;отличного от пробела
je blank ;Символ не найден — одни пробелы
gotit: ;Продолжение, если символ найден

В примере 1 в строке имеются символы, отличные от искомого (кода пробела), и команда je выполнена не будет. После завершения сканирования управление будет передано на метку gotit. Содержимое регистров в этой точке: СХ=3 (так как не выполнено сканирование 3 символов), DI = <смещение string> + 5 (выполнено сканирование 5 символов). ES:D1 указывают на символ строки, следующий за отличным от пробела (в данном случае символ «Т»).

Пример 2
; В полях данных сегмента данных, адресуемого через ES:
string db ‘ /Т:4’
; В программном сегменте:
cld ;Поиск вперед по строке
lea DI,string ;ES:DI ® string
mov AL, ‘/’ ;Искомый символ
mov CX,8 ;Длина строки
repne scasb ;Поиск символа / в строке
jne blank ;Искомого символа нет
g’otit: ;Искомый символ найден

В примере 2 в строке имеется искомый символ («/»), и команда jne выполнена не будет. После завершения сканирования управление будет передано на метку gotit. Содержимое регистров в этой точке: СХ=3 (так как не выполнено сканирование 3 символов), DI = <смещение string> + 5 (выполнено сканирование 5 символов). ES:DI указывают на символ строки, следующий за найденным знаком «/» (в данном случае символ «Т»).

386+ SCASD
Сканирование строки двойных слов с целью сравнения
Команда аналогична командам МП 86 scab и scasw, но позволяет сравнивать содержимое расширенного регистра ЕАХ с двойным словом в памяти, адресуемым через регистры ES:EDI.

Пример
;В полях данных сегмента данных, адресуемого через ES:
nums dd 156000,432666,100000,0,4567890, …
;В программном сегменте:
cld ;Поиск вперед
mov DI,offset nums ;ES:DI -> nums
mov EAX,0 ;Искомое число
mov CX,10000 ;Максимальное число элементов
repne scasd ;Поиск 0 среди чисел массива
jne no0 ;Искомого числа нет
isO: ;Искомое число найдено

В примере в массиве чисел имеется искомое число 0 (с таким же успехом можно было искать любое другое число, например, 4567890). Команда jne выполнена не будет. После завершения сканирования управление будет передано на метку is0. Содержимое регистров в этой точке: СХ уменьшится на 4 (просмотрено 4 элемента), DI будет увеличено на 10h (4 числа по 4 байт в числе).

386+ SETcc Установка байта по условию
Команды, обозначаемые (в книгах, не в программах!) SETcc, осуществляют запись в указанный байтовый операнд 1 или 0 в зависимости от одного из 16 условий, определяемых флагами состояния. Если условие ее выполняется, команда записывает в операнд 1; если условие не выполняется — 0.В качестве операнда можно использовать байтовый регистр или 8-битовую ячейку памяти.
В составе команд процессора предусмотрены следующие команды условной установки байта:
Команда Установить 1, если Условие установки 1
seta выше CF=0 и ZF=0
setae выше или равно CF=0
setb ниже CF= I
setbe ниже или равно CF=1 или ZF=1
setc перенос CF=1
sete равно ZF=1
setg больше ZF=0 или SF=OF
setge больше или равно SF=OF
setl меньше SF не равно OF
setle меньше или равно ZF=1 или SF не равно ОР
setna не выше CF=1 или ZF=1
setnae не выше и не равно CF=1
setnb не ниже CF=0
setnbe не ниже и не равно CF=0 и ZF=0
setnc нет переноса CF=0
setne не равно ZF=0
setng не больше ZF=1 или SF не равно OF
setnge не больше и не равно SF не равно OF
setnl не меньше SF=OF
setnle не меньше и не равно ZF=0 и SF=OF
setno нет переполнения OF=0
setnp нет четности PF=0
setns знаковый бит равен О SF=0
setnz не нуль ZF=0
seto переполнение OF=1
setp есть четность PF=1
setpe сумма битов четная PF=1
setpo сумма битов нечетная PF=0
sets знаковый бит равен SF=1
setz нуль ZF= I

Команды, осуществляющие установку по условию «выше — ниже», предназначены для анализа чисел без знака; команды, осуществляющие установку по условию «больше — меньшее», предназначены для анализа чисел со знаком.

Пример 1
cmp AX,35h
seta CH ;Если AX>35h, CH=1
;Если AX<=35h, CH=0 Пример 2 ; В полях данных flag db ? ;В программном сегменте test AX,8000h sete flag ;Если в АХ установлен бит 7, ;flag=l. Иначе flag=0 386Р+ SGDT Сохранение в памяти содержимого регистра таблицы глобальных дескрипторов Команда копирует содержимое регистра таблицы глобальных дескрипторов GDTR (линейный базовый адрес таблицы и ее границу) в поле из 6 байт, указанное в качестве операнда. SHL Логический сдвиг влево Команда полностью эквивалентна команде sal (арифметический сдвиг влево). См. описание команды sal. 386+ SHLD Логический сдвиг влево с двойной точностью Трехоперандная команда shld с операндами op1, ор2 и орЗ осуществляет сдвиг влево первого из своих операндов opl. Число битов сдвига определяется третьим операндом орЗ. По мере сдвига операнда opl влево, выдвигаемые из него старшие биты, пройдя через флаг CF, теряются, ; на освобождающиеся места со стороны его младших битов поступают старшие биты второго операнда ор2, как если бы он вдвигался своим левым (старшим) концом в opl. Однако после завершения сдвига значение операнда ор2 не изменяется (рис. П10). Во флаге CF остается последний выдвинутый из операнда opl бит. Максимальное число битов сдвига составляет 31. Рис. П10. Действие команды shld. В качестве первого операнда op1можно указывать 16- или 32-разрядный регистр общего назначения или 16- или 32-битовую ячейку памяти. Вторым операндом ор2 может служить только 16- или 32-разрядный регистр общего назначения. Третий операнд, характеризующий число битов сдвига, может находиться в регистре CL или быть непосредственным значением. Команда воздействует на флаги OF, SF, ZF, PF и CF. Пример 1 mov AX,OC001h mov BX,900Fh shld AX,BX,1 ;AX=8003h, BX=900Fh, CF=1 Пример 2 mov AX,0C001h mov BX,900Fh shld AX,BX,2 ;AX=0006h, BX=900Fh, CF=1 Пример 3 mov AX,0C001h mov BX,900Fh shld AX,BX,3 ;AX=000Ch, BX=900Fh, CF=0 Пример 4 mov EBX,0FFCS000h mov ESI,12340000h mov CL,16 shld EBX,ESI,CL ;EBX=80001234h, ;ESI=12340000h, CF=0 SHR Логический сдвиг вправо Команда shr осуществляет сдвиг вправо всех битов операнда. Младший бит операнда поступает в флаг CF. Если команда записана в формате SHR операнд,1 сдвиг осуществляется на 1 бит. В старший бит операнда загружается 0, а младший теряется. Если команда записана в формате SHR onepand,CL сдвиг осуществляется на число бит, указанное в регистре-счетчике CL, при этом в процессе последовательных сдвигов старшие биты операнда заполняются нулями, а младшие, пройдя через флаг CF, теряются (рис. П11). Рис. П11. Действие команды shr. В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги CF, OF, PF, SF и ZF. Пример 1 mov AL,7 shr AL,1 ;AL=3, CF=1 Пример 2 mov AX, lFF0h mov CL,4 shr AX,CL ;AX=01FFh, CF=0 Пример 3 mov DX,9513h mov CL,8 shr DX,CL ;DX=0095h, CF=0 386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит. Пример 1 mov ESI,0FFFF0009h shr ESI,8 ;ESI=00FFFF00h, CF=0 Пример 2 ; В полях данных mem dd 11111111h ;B программном сегменте shr mem,12 ;mem=00011111h, CF=0 386+ SHRD Логический сдвиг вправо с двойной точностью Трехоперандная команда shrd с операндами opl, ор2 и орЗ осуществляет сдвиг вправо первого из своих операндов opl. Число битов сдвига определяется третьим операндом орЗ. По мере сдвига операнда opl вправо выдвигаемые из него младшие биты, пройдя через флаг CF, теряются, а на освобождающиеся места со стороны его старших битов поступают младшие биты второго операнда ор2, как если бы он вдвигался своим правым (младшим) концом в opl. Однако после завершения сдвига значение операнда ор2 не изменяется (рис. П12). Во флаге CF остается последний выдвинутый из операнда opl бит. Максимальное число битов сдвига составляет 31. В качестве первого операнда opl можно указывать 16- или 32-разрядный регистр общего назначения или 16- или 32-битовую ячейку памяти. Вторым операндом ор2 может служить только 16- или 32-разрядный регистр общего назначения. Рис. П12. Действие команды shrd. Третий операнд, характеризующий число битов сдвига, может находиться в регистре CL или быть непосредственным значением. Команда воздействует на флаги OF, SF, ZF, PF и CF. Пример 1 mov AX,0C001h mov BX,900Eh shrd AX,BX,1 ;AX=6000h, BX=900Eh, CF=1 Пример 2 mov AX,0C001h mov BX,900Eh shrd AX,BX,2 ;AX=B000h, BX=900Eh, CF=0 Пример 3 mov AX,0C001h mov BX,900Eh shrd AX,BX,3 ;AX=D800h, BX=900Eh, CF=0 Пример 4 mov EBX,0FFCS000h mov ESI,12345678h mov CL,16 shrd EBX,ESI,CL ;EBX=5678FFC8h, ;ESI=12345678h, CF=0 386P+ SIDT Сохранение в памяти содержимого регистра таблицы дескрипторов прерываний Команда копирует содержимое регистра таблицы дескрипторов прерываний IDTR (линейный базовый адрес таблицы и ее границу) в поле из 6 байт, указанное в качестве операнда. 386Р+ SLDT Сохранение содержимого регистра таблицы локальных дескрипторов Команда копирует содержимое регистра таблицы локальных дескрипторов LDTR (селектор таблицы) в 16- или 32-разрядный регистр или в 16- или 32-битовое поле памяти, указанные в качестве операнда. 386Р+ SMSW Сохранение слова состояния машины Команда smsw считывает слово состояния машины (так называется младшая половина управляющего регистра процессора CRO) и загружает его в указанный в команде 16-разрядный регистр общего назначения или 16-битовое поле памяти. Команду srnsw можно использовать для перевода процессора из реального в защищенный режим или наоборот. В первом случае после чтения слова состояния командой smsw надо установить в нем бит 0 (бит РЕ) и загрузить назад в CRO командой Imsw. Во втором случае после после чтения слова состояния командой smsw надо сбросить в нем бит 0 и загрузить назад в CRO командой Imsw. STC Установка флага переноса Команда stc устанавливает флаг переноса CF в регистре флагов. Команда не имеет параметров и не воздействует на остальные флаги процессора. Пример stc ;Флаг CF устанавливается STD Установка флага направления Команда STD устанавливает флаг направления DF в регистре флагов, определяя тем самым обратное направление выполнения строковых операций (в порядке убывания адресов элементов строки). Команда не имеет параметров и не воздействует на остальные флаги процессора. Пример std ;Флаг направления устанавливается STI Установка флага прерывания Команда STI устанавливает флаг разрешения прерываний IF в регистре флагов, разрешая все аппаратные прерывания (от таймера, клавиатуры, дисков и т.д.). Команда не имеет параметров и не воздействует на остальные флаги процессора. Пример sti ;Разрешение аппаратных прерываний STOS Запись в строку данных STOSB Запись байта в строку данных STOSW Запись слова в строку данных Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они копируют содержимое регистра AL (в случае операций над байтами) или АХ (в случае операций над словами) в ячейку памяти соответствующего размера по адресу, определяемому содержимым пары регистров ES:DI. После операции копирования регистр DI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2 в зависимости от размера копируемого элемента. Вариант команды stos имеет формат stos строка (что не избавляет от необходимости инициализировать регистры ES:DI адресом строки строка). Заменить сегментный регистр ES нельзя. Рассматриваемые команды могут предваряться префиксом повторения rep. В этом случае они повторяются СХ раз, заполняя последовательные ячейки памяти одним и тем же содержимым регистра AL или АХ. Команда не воздействует на флаги процессора. Пример 1 ;В полях данных сегмента данных, адресуемого через ES: id db 'ID:' ;В программном сегменте: eld ;Движение по строке вперед mov DI,offset id+3;DI -> за знаком ‘:’
mov AL,’3′ ;Код ASCII цифры 3
stosb ;Отправим в строку
mov AL,’9′ ;Код ASCII цифры 9
stosb ;Отправим в строку
;Теперь в строке id записано ‘ID:39’

Пример 2
;В полях данных сегмента данных, адресуемого через ES:
array dw 10000 dup (?) ;Место под массив слов
; В программном сегменте:
mov AX,-1 ;Число-заполнитель
mov CX,10000 ;Заполнить 10000 слов
сld ;Движение по строке вперед
lea DI,array ;ES:DI ® array
rep stosw ;Bce 10000 элементов массива
;получают значение -1 (FFFFh)

Пример 3
;В полях данных сегмента данных, адресуемого через ES:
line db 80 dup (‘ ‘) ;Пустая пока строка
;В программном сегменте:
mov AL,’>’ ;Код ASCII знака ‘>’
mov CX,5 ;Заполнить 5 слов
eld ;Движение по строке вперед
lea DI,line ;ES:DI -» line
rep stos line ;Первые 5 байт строки line
;заполняются кодом ASCII
;знака ‘ >’

Пример 4
;В полях данных сегмента данных, адресуемого через ES:
line dw 80 dup (0) ;Строка, заполненная нулями
;В программном сегменте:
mov AL,’>’ ;Код ASCII знака ‘>’
mov AH,31h ;Атрибут (синий по бирюзовому)
mov CX,5 ; Заполнить 5 слов
сld ;Движение по строке вперед
lea DI,line ;ES:DI -» line
rep stos line ;Первые 5 слов строки line
;заполняются кодом ASCII
;знака ‘>’вместе с атрибутом
;для последующего вывода на экран

386+ STOSD Запись двойного слова в строку данных
Команда аналогична командам МП 86 stosb и stosw, но позволяет записать в строку, адресуемую через регистры ES:EDI, двойное слово из регистра ЕАХ.

Пример
; В полях данных
dat dd 12789,2,550000,100000
; В программном сегменте
mov ЕАХ,444777
push DS
pop ES ;ES=DS
mov DI, off set dat
add DI,4*2 ;DS:SI -» 3-й элемент массива
;чисел
stosd ;dat=12789,2,444777,100000

386P+ STR
Сохранение содержимого регистра состояния задачи
Команда str копирует содержимое регистра задачи TR (селектор сегмента состояния задачи) в двухбайтовый регистр общего назначения или 16-битовую ячейку памяти, указанные в качестве операнда.

SUB Вычитание целых чисел
Команда sub вычитает второй операнд (источник) из первого (приемника) и помещает результат на место первого операнда. Исходное значение первого операнда (уменьшаемое) теряется. Таким образом, если команду вычитания записать в общем виде

sub операнд_1, операнд_2

то ее действие можно условно изобразить следующим образом:

операнд_1 — операнд_2 -> операнд_1

В качестве первого операнда можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov AX,100
mov BX,60
sub АХ,ВХ ;АХ=40 (АХ-ВХ) , ВХ=60

Пример 2
mov DL, ‘8 ‘ mov DH, ‘0 ‘
sub DL,DH ;DL=8 (преобразование кода
; ASCII в цифру))

Пример 3
; ; В полях данных
datl dw -168
dat2 dw 10
; ; В программном сегменте
mov AX,data2
sub mem,AX ;mem = -178

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EAX,1000000
mov EBX,60000
sub EAX,EBX ;EAX=40000

TEST Логическое сравнение
Команда test выполняет операцию логического умножения И над двумя операндами и, в зависимости от результата, устанавливает флаги SF, ZF и PF. Флаги OF и CF сбрасываются, a AF имеет неопределенное значение. Состояние флагов можно затем проанализировать командами условных переходов. Команда test не изменяет ни один из операндов.
В качестве первого операнда команды test можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака.

Правила побитового умножения:
Первый операнд-бит 0101
Второй операнд-бит 0011
Бит результата 0001
Флаг SF устанавливается в 1, если в результате выполнения команды образовалось число с установленным знаковым битом.
Флаг ZF устанавливается в 1, если в результате выполнения команды образовалось число, состоящее из одних двоичных нулей.
Флаг PF устанавливается в 1, если в результате выполнения команды образовалось число с четным количеством двоичных единиц в его битах.

Пример 1
test AX,1
jne bityes ;Переход, если бит 0 в АХ установлен
je bitno ;Переход, если бит 0 в АХ сброшен

Пример 2
test SI,8
jne bityes ;Переход, если бит 3 в SI установлен
je bitno ;Переход, если бит 0 в АХ сброшен

Пример 3
test DX,0FFFFh
jz null ;Переход, если DX=0
jnz smth ;Переход, если DX не 0

Пример 4
test CX,0F000h
jne bitsyes ;Переход, если какие-либо из
;4 старших битов СХ установлены
je bitsno ;Переход, если все 4 старших бита
; СХ сброшены

Пример 5
test AX,AX
jz zero ;Переход, если АХ=0
jnz notzero ;Переход, если АХ не 0

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
test ЕАХ,80000000h
jz b31 ;Переход, если бит 31 ЕАХ равен 0
jnz nob31 ;Переход, если бит 31 ЕАХ равен 1

386Р+ VERR Проверка сегмента на чтение
Команда verr позволяет определить, разрешено ли чтение из сегмента, за которым закреплен селектор, передаваемый команде в качестве ее операнда. Операндом может служить 16-разрядный регистр общего назначения или 16-битовая ячейка памяти.

386Р+ VERW Проверка сегмента на запись
Команда verw позволяет определить, разрешена ли запись в сегмент, за которым закреплен селектор, передаваемый команде в качестве ее операнда. Операндом может служить 16-разрядный регистр общего назначения или 16-битовая ячейка памяти.

486+ XADD Обмен и сложение
Команда xadd выполняет в одной операции сложение и обмен операндов. Команда требует двух операндов, причем первый операнд должен быть ячейкой памяти, а второй — регистром. После сложения операндов исходное содержимое памяти переносится во второй операнд (регистр), а полученная сумма записывается в память (на место первого слагаемого) (рис. П13). Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Рис. П13. Действие команды xadd.

Пример
; В полях данных
mem dw 99
;В программном сегменте
mov AX,48
xadd mem,AX ;mem=147, AX=99

XCHG Обмен данными между операндами
Команда xchg пересылает значение первого операнда во второй, а второго — в первый. В качестве любого операнда можно указывать регистр (кроме сегментного) или ячейку памяти, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команда не воздействует на флаги процессора.

Пример 1
mov AX,OFF01h
mov SI,1000h
xchg AX,SI ;AX=01000h, SI=FF01h

Пример 2
;В полях данных:
mem dw 0F0F0h
;В программном сегменте
mov CX,1256h
xchg CX,mem ;CX=F0F0h, mem=1256h

Пример 3
mov AX,6031h
xchg AH,AL ;AX=3160h

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
xchg ESI,EDI ;ESI и EDI обмениваются содержимым

XLAT Табличная трансляция
Команда xlat осуществляет выборку байта из массива байтов, который в этом случае называют таблицей трансляции. В регистре ВХ должен находиться относительный адрес таблицы, а в регистре AL — смещение в таблице к выбираемому байту (его индекс). Выбранный байт загружается в регистр AL, замещая находившееся в нем смещение. Длина таблицы может достигать 256 байт. Таблица должна находиться в сегменте данных, адресуемом через сегментный регистр DS. Замена сегмента не допускается. Команда xlat не имеет параметров, но требует предварительной настройки регистров ВХ и AL. Команда не воздействует на флаги процессора.

Пример
;Пример демонстрирует преобразование первых 14 скен-кодов
;(фактически это скен-коды клавиш верхнего ряда
;клавиатуры) в коды ASCII соответствующих символов
; В полях данных:
table db 0,27, ‘ 1234567890- = \’;Таблица кодов ASCII
;В программном сегменте
lea BX,table
mov AL,5 ;Скен-код 5 клавиши <4/$>
xlat ;AL=34h, код ASCII символа 4

386+ XLAT
386+ XLATB
Команда xlatb эквивалентна команде xlat МП 86 за исключением того, что для 32-разрядных приложений относительный адрес таблицы размещается в расширенном регистре ЕВХ.
Команда xlat может иметь в качестве операнда относительный адрес таблицы трансляции; в этом случае помещение адреса таблицы в регистр ЕВХ не требуется. Действие команды от этого не изменяется, однако возможна замена сегмента.

Пример
;В сегменте, адресуемом через сегментный регистр ES:
table db 0,27,’1234567890-=\’;Таблица кодов ASCII
;В программном сегменте
mov AL,13 ;Скен-код клавиши <=/+>
xlat ES:table ;AL=3Dh, код ASCII символа =

XOR Логическое ИСКЛЮЧАЮЩЕЕ ИЛИ
Команда хог выполняет операцию логического (побитового) ИСКЛЮЧАЮЩЕГО ИЛИ над своими двумя операндами. Результат операции замещает первый операнд; второй операнд не изменяется. Каждый бит результата устанавливается в 1, если соответствующие биты операндов различны, и сбрасывается в 0, если соответствующие биты операндов совпадают.
В качестве первого операнда команды хог можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго — регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами. Команда воздействует на флаги OF, SF, ZF, PF и CF, причем флаги OF и CF всегда сбрасываются, а остальные флаги устанавливаются в зависимости от результата.

Правила побитового ИСКЛЮЧАЮЩЕГО ИЛИ:
Первый операнд-бит 0101
Второй операнд-бит 0011
Бит результата 0110

Пример 1
mov AX,0Fh
хог AX,0FFFFh ;AX=FFF0h

Пример 2
хог ВХ,ВХ ;Обнуление ВХ

Пример 3
mov SI,0AAAAh
mov BX,5555h
xor SI,BX ;SI=FFFFh,BX=5555h

386+ Допустимо использование 32-битовьгх операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EAX,4444AAAAh
xor EAX,4441AAACh ;EAX=00050006h

Взаимодействие с GLIBC

GLIBC — стандартная библиотека Си от GNU. Если вы программируете на ассемблере под Linux, то использование функций из этой библиотеки — хороший способ сократить размер программы и затраченные усилия. Безусловно, использование их замедляет программу, но это всего лишь значит, что их не стоит использовать в критических участках — циклах. Если же вы используете GLIBC скажем для форматированного вывода на консоль, то вряд ли вы заметите какое-нибудь замедление.

Более того — использование GLIBC в большинстве случаев сделает вашу программу легко портируемой на многие другие UNIX-платформы.

В качестве примера рассмотрим программу, которая импортирует функцию puts (вывод на консоль null-terminated строки)

;Точка входа «_start» на самом деле находится
;в подключаемом *.o файле стандартной библиотеки Си
;Она передает управление на функцию «main»,
;которая должна находиться в нашей программе
global main

;Внешние функции
extern exit
extern puts

;Сегмент кода:
section .text

;Функция main:
main:

;Параметры передаются в стеке:
push dword msg
call puts

;По конвенции Си вызывающая процедура должна
;очищать стек от параметров самостоятельно:
sub esp, 4

;Завершение программы с кодом выхода 0:
push dword 0
call exit

ret

;Сегмент данных
section .data

msg: db «An example of interfacing with GLIBC.»,0x0D,0

Компиляция:

nasm -felf inglibc.asm

Компоновка: Для вызова компоновщика с нужными параметрами мы не будем заморачиваться с командой ld, а воспользуемся GCC, который сам определит, что нужно нашему объектному файлу:

gcc inglibc.o -o inglibc

Своя маленькая операционная система

А что если хочется написать программку, для которой ничего не надо? Вставляем дискетку в компьютер, загружаемся с нее и …»Hello World»! Можно даже прокричать это приветствие из защищенного режима…
Сказано — сделано. С чего бы начать?.. Набраться знаний, конечно. Для этого очень хорошо полазить в исходниках Linux и Thix. Первая система всем хорошо знакома, вторая менее известна, но не менее полезна.

Подучились? Теперь займемся. Понятно, что первым делом надо написать загрузочный сектор для нашей мини-операционки (а ведь это будет именно мини-операционка!). Поскольку процессор грузится в 16-разрядном режиме, то для создания загрузочного сектора используется ассемблер и линковщик из пакета bin86. Можно, конечно, поискать еще что-нибудь, но оба наших примера используют именно его; и мы тоже пойдем по стопам учителей. Синтаксис этого ассемблера немного странноватый, совмещающий черты, характерные и для Intel и для AT&T, но после пары недель мучений можно привыкнуть.

Загрузочный сектор (boot.S)

Сознательно не буду приводить полных листингов программ. Так станут понятней основные идеи, да и вам будет намного приятней, если все напишете своими руками.

Для начала определимся с основными константами.
START_HEAD = 0 — Головка привода, которою будем использовать.
START_TRACK = 0 — Дорожка, откуда начнем чтение.
START_SECTOR = 2 — Сектор, начиная с которого будем считывать наше ядрышко.
SYSSIZE = 10 — Размер ядра в секторах (каждый сектор содержит 512 байт)
FLOPPY_ID = 0 — Идентификатор привода. 0 — для первого, 1 — для второго
HEADS = 2 — Количество головок привода.
SECTORS = 18 — Количество дорожек на дискете. Для формата 1.44 МБ это количество равно 18.

В процессе загрузки будет происходить следующее. Загрузчик BIOS считает первый сектор дискеты, положит его по адресу 0000:0x7c00 и передаст туда управление. Мы его получим и — для начала — переместим себя пониже по адресу 0000:0x600, перейдем туда и спокойно продолжим работу. Собственно вся наша работа будет состоять из загрузки ядра (сектора 2 — 12 первой дорожки дискеты) по адресу 0x100:0000, переходу в защищенный режим и скачку на первые строки ядра. В связи с этим еще несколько констант:
BOOTSEG = 0x7c00 — Сюда поместит загрузочный сектор BIOS.
INITSEG = 0x600 — Сюда его переместим мы.
SYSSEG = 0x100 — А здесь приятно расположится наше ядро.
DATA_ARB = 0x92 — Определитель сегмента данных для дескриптора
CODE_ARB = 0x9A — Определитель сегмента кода для дескриптора.

Первым делом произведем перемещение самих себя в более приемлемое место.

cli
xor ax, ax
mov ss, ax
mov sp, #BOOTSEG
mov si, sp
mov ds, ax
mov es, ax
sti
cld
mov di, #INITSEG
mov cx, #0x100
repnz
movsw
jmpi go, #0
;прыжок в новое местоположение загрузочного сектора на метку go
Теперь необходимо настроить как следует сегменты для данных (es, ds) и для стека. Неприятно, конечно, что все приходится делать вручную, но что поделаешь — ведь кроме нас и BIOS в памяти компьютера никого нет.

go:
mov ax, #0xF0
mov ss, ax
mov sp, ax ; Стек разместим как 0xF0:0xF0 = 0xFF0
mov ax, #0x60 ; Сегменты для данных ES и DS зададим в 0x60
mov ds, ax
mov es, ax
Наконец, можно вывести победное приветствие. Пусть мир узнает, что мы смогли загрузиться! Поскольку у нас есть все-таки целый BIOS, воспользуемся готовой функцией 0x13 прерывания 0x10. Можно, конечно, его презреть и написать напрямую в видеопамять, но у нас каждый байт команды на счету, а байт таких всего 512. Потратим их лучше на что-нибудь более полезное.

mov cx,#18
mov bp,#boot_msg
call write_message
Функция write_message выглядит следующим образом
write_message:
push bx
push ax
push cx
push dx
push cx
mov ah,#0x03 ; прочитаем текущее положение курсора,
; дабы не выводить сообщения где попало.
xor bh,bh
int 0x10
pop cx
mov bx,#0x0007 ; Параметры выводимых символов : видеостраница 0,
; атрибут 7 (серый на черном)
mov ax,#0x1301 ; Выводим строку и сдвигаем курсор.
int 0x10
pop dx
pop cx
pop ax
pop bx
ret
А сообщение так

boot_msg:
.byte 13,10
.ascii «Booting data …»
.byte 0
К этому времени на дисплее компьютера появится скромное «Booting data …». Это в принципе не хуже, чем «Hello World», но давайте добьемся чуть большего. Перейдем в защищенный режим и выведем этот «Hello» уже из программы, написанной на C.

Ядро 32-разрядное. Оно будет у нас размещаться отдельно от загрузочного сектора и собираться уже с помощью gcc и gas. Синтаксис ассемблера gas соответствует требованиям AT&T, так что тут все будет попроще. Но для начала нам нужно прочитать ядро. Опять воспользуемся готовой функцией 0x2 прерывания 0x13.

recalibrate:
mov ah, #0
mov dl, #FLOPPY_ID
int 0x13 ; проведем реинициализацию дисковода.
jc recalibrate
call read_track ; вызов функции чтения ядра
jnc next_work ; если во время чтения не произошло
; ничего плохого, то работаем дальше

bad_read:
; если чтение произошло неудачно — выводим сообщение об ошибке
mov bp,#error_read_msg
mov cx,7
call write_message
inf1: jmp inf1 ; и уходим в бесконечный цикл.
; Теперь нас спасет только ручная перезагрузка
Сама функция чтения предельно простая: долго и нудно заполняем параметры, а затем одним махом считываем ядро. Сложности начнутся, когда ядро перестанет помещаться в 17 секторах (то есть 8.5КБ); но это пока в будущем, а сейчас вполне достаточно такого молниеносного чтения

read_track:
pusha
push es
push ds
mov di, #SYSSEG ; Определяем
mov es, di ; адрес буфера для данных
xor bx, bx
mov ch, #START_TRACK ;дорожка 0
mov cl, #START_SECTOR ;начиная с сектора 2
mov dl, #FLOPPY_ID
mov dh, #START_HEAD
mov ah, #2
mov al, #SYSSIZE ;считать 10 секторов
int 0x13
pop ds
pop es
popa
ret
Вот и все. Ядро успешно прочитано, и можно вывести еще одно радостное сообщение на экран.

next_work:
call kill_motor ; останавливаем привод дисковода
mov bp,#load_msg ; выводим сообщение
mov cx,#4
call write_message
Вот содержимое сообщения
load_msg:
.ascii «done»
.byte 0
А вот функция остановки двигателя привода.
kill_motor:
push dx
push ax
mov dx,#0x3f2
xor al,al
out dx,al
pop ax
pop dx
ret
На данный момент на экране выведено «Booting data …done» и лампочка привода флоппи-дисков погашена. Все затихли и готовы к смертельному номеру — прыжку в защищенный режим.

Для начала надо включить адресную линию A20. Это в точности означает, что мы будем использовать 32-разрядную адресацию к данным.

mov al, #0xD1 ; команда записи для 8042
out #0x64, al
mov al, #0xDF ; включить A20
out #0x60, al
Выведем предупреждающее сообщение — о том, что переходим в защищенный режим. Пусть все знают, какие мы важные.

protected_mode:
mov bp,#loadp_msg
mov cx,#25
call write_message
Сообщение:

loadp_msg:
.byte 13,10
.ascii «Go to protected mode…»
.byte 0
Пока у нас еще жив BIOS, запомним позицию курсора и сохраним ее в известном месте (0000:0x8000 ). Ядро позже заберет все данные и будет их использовать для вывода на экран победного сообщения.

save_cursor:
mov ah,#0x03 ; читаем текущую позицию курсора
xor bh,bh
int 0x10
seg cs
mov [0x8000],dx ;сохраняем в специальном тайнике
Теперь внимание, запрещаем прерывания (нечего отвлекаться во время такой работы) и загружаем таблицу дескрипторов

cli
lgdt GDT_DESCRIPTOR ; загружаем описатель таблицы дескрипторов.
У нас таблица дескрипторов состоит из трех описателей: нулевой (всегда должен присутствовать), сегмента кода и сегмента данных .

align 4
.word 0
GDT_DESCRIPTOR: .word 3 * 8 — 1 ; размер таблицы дескрипторов
.long 0x600 + GDT ; местоположение таблицы дескрипторов
.align 2
GDT:
.long 0, 0 ; Номер 0: пустой дескриптор
.word 0xFFFF, 0 ; Номер 8: дескриптор кода
.byte 0, CODE_ARB, 0xC0, 0
.word 0xFFFF, 0 ; Номер 0x10: дескриптор данных
.byte 0, DATA_ARB, 0xCF, 0
Переход в защищенный режим может происходить минимум двумя способами, но обе ОС, выбранные нами для примера (Linux и Thix) используют для совместимости с 286 процессором команду lmsw. Мы будем действовать тем же способом

mov ax, #1
lmsw ax ; прощай реальный режим.
; Мы теперь находимся в защищенном режиме.
jmpi 0x1000, 8 ; Затяжной прыжок на 32-разрядное ядро.
Вот и вся работа загрузочного сектора – не мало, но и не много. Теперь с ним мы попрощаемся и направимся к ядру.

В конце ассемблерного файла полезно добавить следующую инструкцию.

org 511
end_boot: .byte 0
В результате скомпилированный код будет занимать ровно 512 байт, что очень удобно для подготовки образа загрузочного диска.

Первые вздохи ядра (head.S)

Ядро, к сожалению, опять начнется с ассемблерного кода. Но теперь его будет совсем немного.

Мы собственно зададим правильные значения сегментов для данных (ES, DS, FS, GS). Записав туда значение соответствующего дескриптора данных.

cld
cli
movl $(__KERNEL_DS),%eax
movl %ax,%ds
movl %ax,%es
movl %ax,%fs
movl %ax,%gs
Проверим, нормально ли включилась адресная линия A20 — простым тестом записи. Обнулим для чистоты эксперимента регистр флагов.

xorl %eax,%eax
1: incl %eax
movl %eax,0x000000
cmpl %eax,0x100000
je 1b
pushl $0
popfl
Вызовем долгожданную функцию, уже написанную на С.

call SYMBOL_NAME(start_my_kernel)
И больше нам тут делать нечего.

inf: jmp inf
Поговорим на языке высокого уровня (start.c)

Вот теперь мы вернулись к тому, с чего начинали рассказ. Почти вернулись, потому что printf() теперь надо делать вручную. Поскольку готовых прерываний уже нет, то будем использовать прямую запись в видеопамять. Для любопытных — почти весь код этой части, с незначительными изменениями, позаимствован из части ядра Linux, осуществляющей распаковку (/arch/i386/boot/compressed/*). Для сборки вам потребуется дополнительно определить такие макросы как inb(), outb(), inb_p(), outb_p(). Готовые определения проще всего одолжить из любой версии Linux.

Теперь, дабы не путаться со встроенными в glibc функциями, отменим их определение

#undef memcpy
Зададим несколько своих:
static void puts(const char *);
static char *vidmem = (char *)0xb8000; /*адрес видеопамяти*/
static int vidport; /*видеопорт*/
static int lines, cols; /*количество линий и строк на экран*/
static int curr_x,curr_y; /*текущее положение курсора */
И начнем, наконец, писать код на языке высокого уровня… правда, с небольшими ассемблерными вставками.

/* функция перевода курсора в положение (x,y).
* Работа ведется через ввод/вывод в видеопорт
*/
void gotoxy(int x, int y)
{
int pos;
pos = (x + cols * y) * 2;
outb_p(14, vidport);
outb_p(0xff & (pos >> 9), vidport+1);
outb_p(15, vidport);
outb_p(0xff & (pos >> 1), vidport+1);
}

/* функция прокручивания экрана. Работает,
* используя прямую запись в видеопамять
*/
static void scroll()
{
int i;
memcpy ( vidmem, vidmem + cols * 2, ( lines — 1 ) * cols * 2 );
for ( i = ( lines — 1 ) * cols * 2; i < lines * cols * 2; i += 2 ) vidmem[i] = ' '; } /* функция вывода строки на экран */ static void puts(const char *s) { int x,y; char c; x = curr_x; y = curr_y; while ( ( c = *s++ ) != '\0' ) { if ( c == '\n' ) { x = 0; if ( ++y >= lines ) {
scroll();
y—;
}
} else {
vidmem [ ( x + cols * y ) * 2 ] = c;
if ( ++x >= cols ) {
x = 0;
if ( ++y >= lines ) {
scroll();
y—;
}
}
}
}
gotoxy(x,y);
}
/* функция копирования из одной области
* памяти в другую. Заменитель стандартной функции glibc
*/
void* memcpy(void* __dest, __const void* __src,
unsigned int __n)
{
int i;
char *d = (char *)__dest, *s = (char *)__src;
for (i=0;i<__n;i++) d[i]="s[i];" }="}" /* функция, издающая долгий и протяжный звук. * Использует только ввод/вывод в порты поэтому * очень полезна для отладки */ make_sound() { __asm__(" movb $0xB6, %al\n\t outb %al, $0x43\n\t movb $0x0D, %al\n\t outb %al, $0x42\n\t movb $0x11, %al\n\t outb %al, $0x42\n\t inb $0x61, %al\n\t orb $3, %al\n\t outb %al, $0x61\n\t "); } /*А вот и основная функция*/ int start_my_kernel() { /*задаются основные параметры */ vidmem = (char *) 0xb8000; vidport = 0x3d4; lines = 25; cols = 80; /* считываются предусмотрительно * сохраненные координаты курсора */ curr_x=*(unsigned char *)(0x8000); curr_y=*(unsigned char *)(0x8001); /*выводится строка*/ puts("done\n"); /*уходим в бесконечный цикл*/ while(1); } Вот и вывели мы этот "Hello World" на экран. Сколько проделано работы, а на экране только две строчки Booting data ...done Go to proteсted mode ...done А что – плохо?! Закричала новая операционная система. Мир с радостью воспринял ее. Кто знает, может быть - это новый Linux ?... Подготовка загрузочного образа (floppy.img) Теперь подготовим загрузочный образ нашей системки.Для начала соберем загрузочный сектор. as86 -0 -a -o boot.o boot.S ld86 -0 -s -o boot.img boot.o Обрежем 32-битный заголовок и получим таким образом чистый двоичный код. dd if=boot.img of=boot.bin bs=32 skip=1 Соберем ядро gcc -traditional -c head.S -o head.o gcc -O2 -DSTDC_HEADERS -c start.c При компоновке НЕ ЗАБУДЬТЕ параметр "-T"! Он указывает, относительно какого смещения вести расчеты; в нашем случае, поскольку ядро грузится по адресy 0x1000, смещение соответствующее: ld -m elf_i386 -Ttext 0x1000 -e startup_32 head.o start.o -o head.img Отделим зерна от плевел, то есть чистый двоичный код от всяческих служебных заголовков и комментариев: objcopy -O binary -R .note -R .comment -S head.img head.bin И соединим воедино загрузочный сектор и ядро cat boot.bin head.bin >floppy.img
Образ готов. Записываем на дискетку (заготовьте несколько для экспериментов, я прикончил три штуки), перезагружаем компьютер и наслаждаемся…

cat floppy.img >/dev/fd0

Пишем свой загрузочный сектор

Мы будем писать загрузочный сектор для трехдюймовой дискеты с файловой системой FAT12. После окончания начальной загрузки программа POST находит активное устройство и загружает с него короткую программу загрузки ОС — загрузочный сектор. Загрузочный сектор это первый физический сектор устройства, в данном случае дискеты и его размет равен всего ничего 512 байт. С помощью этих 512 байт кода мы должны найти основную часть загрузчика операционной системы, загрузить его в память и передать ему управление. Заголовок файловой системы FAT находится в первом секторе дискеты, благодаря чему этот заголовок, содержащий всю необходимую информацию о файловой системе, загружается вместе нашим загрузчиком. Наш загрузочный сектор будет искать в корневом каталоге некоторый файл — загрузчик, загрузит его в память и передаст ему управление на его начало. А загрузчик уже сам разберется, что ему делать дальше. Я использую NASM, т.к. считаю, что он больше подходит для наших целей.
И так, приступим. Как я уже говорил, в начале нашего загрузочного сектора располагается заголовок FAT, опишем его:
; Общая часть для всех типов FAT
BS_jmpBoot:
jmp short BootStart ; Переходим на код загрузчика
nop
BS_OEMName db ‘*-v4VIHC’ ; 8 байт, что было на моей дискете, то и написал
BPB_BytsPerSec dw 0x200 ; Байт на сектор
BPB_SecPerClus db 1 ; Секторов на кластер
BPB_RsvdSecCnt dw 1 ; Число резервных секторов
BPB_NumFATs db 2 ; Количектво копий FAT
BPB_RootEntCnt dw 224 ; Элементов в корневом катологе (max)
BPB_TotSec16 dw 2880 ; Всего секторов или 0
BPB_Media db 0xF0 ; код типа устройства
BPB_FATsz16 dw 9 ; Секторов на элемент таблицы FAT
BPB_SecPerTrk dw 18 ; Секторов на дорожку
BPB_NumHeads dw 2 ; Число головок
BPB_HiddSec dd 0 ; Скрытых секторов
BPB_TotSec32 dd 0 ; Всего секторов или 0
; Заголовок для FAT12 и FAT16
BS_DrvNum db 0 ; Номер дика для прерывания int 0x13
BS_ResNT db 0 ; Зарезервировано для Windows NT
BS_BootSig db 29h ; Сигнатура расширения
BS_VolID dd 2a876CE1h ; Серийный номер тома
BS_VolLab db ‘X boot disk’ ; 11 байт, метка тома
BS_FilSysType db ‘FAT12 ‘ ; 8 байт, тип ФС
; Структура элемента каталога
struc DirItem
DIR_Name: resb 11
DIR_Attr: resb 1
DIR_ResNT: resb 1
DIR_CrtTimeTenth resb 1
DIR_CrtTime: resw 1
DIR_CrtDate: resw 1
DIR_LstAccDate: resw 1
DIR_FstClusHi: resw 1
DIR_WrtTime: resw 1
DIR_WrtDate: resw 1
DIR_FstClusLow: resw 1
DIR_FileSize: resd 1
endstruc ;DirItem

Большинство полей мы использовать не будем, и так мало места для полета. Загрузчик BIOS передает нам управление на начало загрузочного сектора, т.е. на BS_jmpBoot, поэтому в начале заголовка FAT на отводится 3 байта для короткой или длинной инструкции jmp. Мы в данном случае использовали короткую, указав модификатор short, и в третьем байте просто разместили однобайтовую инструкцию nop.

По инструкции jmp short BootStart мы переходим на наш код. Проведем небольшую инициализацию:
; Наши не инициализированные переменные
; При инициализации они затрут не нужные нам
; поля заголовка FAT: BS_jmpBoot и BS_OEMName
struc NotInitData
SysSize: resd 1 ; Размер системной области FAT
fails: resd 1 ; Число неудачных попыток при чтении
fat: resd 1 ; Номер загруженного сектора с элементами FAT
endstruc ;NotInitData
; По этому адресу мы будем загружать загрузчик
%define SETUP_ADDR 0x1000
; А по этому адресу нас должны были загрузить
%define BOOT_ADDR 0x7C00
%define BUF 0x500
BootStart:
cld
xor cx, cx
mov ss, cx
mov es, cx
mov ds, cx
mov sp, BOOT_ADDR
mov bp, sp
; Сообщим о том что мы загружаемся
mov si, BOOT_ADDR + mLoading
call print

Все сегментные регистры настраиваем на начало физической памяти. Вершину стека настраиваем на начало нашего сектора, стек растет вниз (т.е. в сторону младших адресов), так что проблем быть не должно. Туда же указывает регистр bp — нам нужно обращаться к полям заголовка FAT и паре наших переменных. Мы используем базовую адресацию со смещением, для чего используем регистр bp т.к. в этом случае можно использовать однобайтовые смещения, вместо двухбайтовых адресов, что позволяет сократить код. Процедуру print, выводящую сообщение на экран, рассмотрим позже.

Теперь нам нужно вычислить номера первых секторов корневого каталога и данных файлов.

mov al, [byte bp+BPB_NumFATs] cbw
mul word [byte bp+BPB_FATsz16] add ax, [byte bp+BPB_HiddSec] adc dx, [byte bp+BPB_HiddSec+2] add ax, [byte bp+BPB_RsvdSecCnt] adc dx, cx
mov si, [byte bp+BPB_RootEntCnt] ; dx:ax — Номер первого сектора корневого каталога
; si — Количество элементов в корневом каталоге
pusha
; Вычислим размер системной области FAT = резервные сектора +
; все копии FAT + корневой каталог
mov [bp+SysSize], ax ; осталось добавить размер каталога
mov [bp+SysSize+2], dx
; Вычислим размер корневого каталога
mov ax, 32
mul si
; dx:ax — размер корневого каталога в байтах, а надо в секторах
mov bx, [byte bp+BPB_BytsPerSec] add ax, bx
dec ax
div bx
; ax — размер корневого каталога в секторах
add [bp+SysSize], ax ; Теперь мы знаем размер системной
adc [bp+SysSize+2], cx ; области FAT, и начало области данных
popa
; В dx:ax — снова номер первого сектора корневого каталога
; si — количество элементов в корневом каталоге

Теперь мы будем просматривать корневой каталог в поисках нужного нам файла
NextDirSector:
; Загрузим очередной сектор каталога во временный буфер
mov bx, 700h ; es:bx — буфер для считываемого сектора
mov di, bx ; указатель текущего элемента каталога
mov cx, 1 ; количество секторов для чтения
call ReadSectors
jc near DiskError ; ошибка при чтении
RootDirLoop:
; Ищем наш файл
; cx = 0 после функции ReadSectors
cmp [di], ch ; byte ptr [di] = 0?
jz near NotFound ; Да, это последний элемент в каталоге
; Нет, не последний, сравним имя файла
pusha
mov cl, 11 ; длина имени файла с расширением
mov si, BOOT_ADDR + LoaderName ; указатель на имя искомого файла
rep cmpsb ; сравниваем
popa
jz short Found ; Нашли, выходим из цикла
; Нет, ищем дальше
dec si ; RootEntCnt
jz near NotFound ; Это был последний элемент каталога
add di, 32 ; Переходим к следующему элементу каталога
; bx указывает на конец прочтенного сектора после call ReadSectors
cmp di, bx ; Последний элемент в буфере?
jb short RootDirLoop ; Нет, проверим следующий элемент
jmp short NextDirSector ; Да последний, загрузим следующий сектор

Из этого кода мы можем выйти одну из трех точек: ошибка при чтении DiskError, файл наден Found или файл не найден NotFound.

Если файл найден, то загрузим его в память и передадим управление на его начало.

Found:
; Загрузка загрузчика (извените, калабур)
mov bx, SETUP_ADDR
mov ax, [byte di+DIR_FstClusLow] ; Номер первого кластера файла
; Загружаем сектор с элемнтами FAT, среди которых есть FAT[ax] ; LoadFAT сохраняет значения всех регистров
call LoadFAT
ReadCluster:
; ax — Номер очередного кластера
; Загрузим его в память
push ax
; Первые два элемента FAT служебные
dec ax
dec ax
; Число секторов для чтения
; cx = 0 после ReadSectors
mov cl, [byte bp+BPB_SecPerClus] ; Секторов на кластер
mul cx
; dx:ax — Смещение кластера относительно области данных
add ax, [byte bp+SysSize] adc dx, [byte bp+SysSize+2] ; dx:ax — Номер первого сектора требуемого кластера
; cx еще хранит количество секторов на кластер
; es:bx — конец прошлого кластера и начало нового
call ReadSectors ; читаем кластер
jc near DiskError ; Увы, ошибка чтения
pop ax ; Номер кластера
; Это конец файла?
; Получим значение следующего элемента FAT
pusha
; Вычислим адрес элемента FAT
mov bx, ax
shl ax, 1
add ax, bx
shr ax, 1
; Получим номер сектора, в котором находится текущий элемент FAT
cwd
div word [byte bp+BPB_BytsPerSec] cmp ax, [bp+fat] ; Мы уже читали этот сектор?
popa
je Checked ; Да, читали
; Нет, надо загрузить этот сектор
call LoadFAT
Checked:
; Вычислим адрес элемента FAT в буфере
push bx
mov bx, ax
shl bx, 1
add bx, ax
shr bx, 1
and bx, 511 ; остаток от деления на 512
mov bx, [bx+0x700] ; а вот и адрес
; Извлечем следующий элемент FAT
; В FAT16 и FAT32 все немного проще 🙁
test al, 1
jnz odd
and bx, 0xFFF
jmp short done
odd:
shr bx, 4
done:
mov ax, bx
pop bx
; bx — новый элемент FAT
cmp ax, 0xFF8 ; EOF — конец файла?
jb ReadCluster ; Нет, читаем следующий кластер
; Наконец-то загрузили
mov ax, SETUP_ADDR>>4 ; SETUP_SEG
mov es, ax
mov ds, ax
; Передаем управление, наше дело сделано 🙂
jmp SETUP_ADDR>>4:0

LoadFAT ;proc
; Процедура для загрузки сектора с элементами FAT
; Элемент ax должен находится в этом секторе
; Процедура не должна менять никаких регистров
pusha
; Вычисляем адрес слова содержащего нужный элемент
mov bx, ax
shl ax, 1
add ax, bx
shr ax, 1
cwd
div word [byte bp+BPB_BytsPerSec] ; ax — смещение сектора относительно начала таблицы FAT
mov [bp+fat], ax ; Запомним это смещение, dx = 0
cwd ; dx:ax — номер сектора, содержащего FAT[?] ; Добавим смещение к первой копии таблицы FAT
add ax, [byte bp+BPB_RsvdSecCnt] adc dx, 0
add ax, [byte bp+BPB_HiddSec] adc dx, [byte bp+BPB_HiddSec+2] mov cx, 1 ; Читаем один сектор. Можно было бы и больше, но не быстрее
mov bx, 700h ; Адрес буфера
call ReadSectors
jc DiskError ; Ошибочка вышла
popa
ret
;LoadFAT endp

В FAT12 на каждый элемент FAT отводится по 12 бит, что несколько усложняет нашу работу, в FAT16 и FAT32 на каждый элемент отводится по 16 и 32 бита соответственно и можно просто прочесть слово или двойное слово, а в FAT12 необходимо прочесть слово содержащее элемент FAT и правильно извлечь из него 12 бит.

Теперь разберем процедуру загрузки секторов. Процедура получает номер сектора в dx:ax (нумерация с нуля) и преобразует его к формату CSH (цилиндр, сектор, сторона), используемому прерыванием BIOS int 0x13.
; *************************************************
; * Чтение секторов с диска *
; *************************************************
; * Входные параметры: *
; * dx:ax — (LBA) номер сектора *
; * cx — количество секторов для чтения *
; * es:bx — адрес буфера *
; *************************************************
; * Выходные параметры: *
; * cx — Количество не прочтенных секторов *
; * es:bx — Указывает на конец буфера *
; * cf = 1 — Произошла ошибка при чтении *
; *************************************************
ReadSectors ;proc
next_sector:
; Читаем очередной сектор
mov byte [bp+fails], 3 ; Количество попыток прочесть сектор
try:
; Очередная попытка
pusha
; Преобразуем линейный адрес в CSH
; dx:ax = a1:a0
xchg ax, cx ; cx = a0
mov ax, [byte bp+BPB_SecPerTrk] xchg ax, si ; si = Scnt
xchg ax, dx ; ax = a1
xor dx, dx
; dx:ax = 0:a1
div si ; ax = q1, dx = c1
xchg ax, cx ; cx = q1, ax = a0
; dx:ax = c1:a0
div si ; ax = q2, dx = c2 = c
inc dx ; dx = Sector?
xchg cx, dx ; cx = c, dx = q1
; dx:ax = q1:q2
div word [byte bp+BPB_NumHeads] ; ax = C (track), dx = H
mov dh, dl ; dh = H
mov ch, al
ror ah, 2
or cl, ah
mov ax, 0201h ; ah=2 — номер функции, al = 1 сектор
mov dl, [byte bp+BS_DrvNum] int 13h
popa
jc Failure ; Ошибка при чтении
; Номер следующего сектора
inc ax
jnz next
inc dx
next:
add bx, [byte bp+BPB_BytsPerSec] dec cx ; Все сектора прочтены?
jnz next_sector ; Нет, читаем дальше
return:
ret
Failure:
dec byte [bp+fails] ; Последняя попытка?
jnz try ; Нет, еще раз
; Последняя, выходим с ошибкой
stc
ret
;ReadSectors endp

Осталось всего ничего:
; Сообщения об ошибках
NotFound: ; Файл не найден
mov si, BOOT_ADDR + mLoaderNotFound
call print
jmp short die
DiskError: ; Ошибка чтения
mov si, BOOT_ADDR + mDiskError
call print
;jmp short die
die: ; Просто ошибка
mov si, BOOT_ADDR + mReboot
call print
_die: ; Бесконечный цикл, пользователь сам нажмет Reset
jmp short _die
; Процедура вывода ASCIIZ строки на экран
; ds:si — адрес строки
print: ; proc
pusha
print_char:
lodsb ; Читаем очередной символ
test al, al ; 0 — конец?
jz short pr_exit ; Да конец
; Нет, выводим этот символ
mov ah, 0eh
mov bl, 7
int 10h
jmp short print_char ; Следующий
pr_exit:
popa
ret
;print endp
; Перевод строки
%define endl 10,13,0
; Строковые сообщения
mLoading db ‘Loading…’,endl
mDiskError db ‘Disk I/O error’,endl
mLoaderNotFound db ‘Loader not found’,endl
mReboot db ‘Reboot system’,endl
; Выравнивание размера образа на 512 байт
times 499-($-$$) db 0
LoaderName db ‘BOOTOR ‘ ; Имя файла загрузчика
BootMagic dw 0xAA55 ; Сигнатура загрузочного сектора

Ну вот вроде бы и все. Компилируется все это до безобразия просто:
> nasm -f bin boot.asm -lboot.lst -oboot.bin

Осталось только как-то записать этот образ в загрузочный сектор вашей дискеты и разместить в корне этой дискеты файл загрузчика BOOTOR. Загрузочный сектор можно записать с помощью такой вот простой программы на Turbo (Borland) Pascal. Эта программа будет работать как в DOS, так и в Windows — пробовал на WinXP — работает как ни странно, но только с floopy. Но все же я рекомендую запускать эту утилиту из-под чистого DOS’а, т.к. WinXP обновляет не все поля в заголовке FAT и загрузочный сектор может работать некорректно.

var
fn:string;
f:file;
buf:array[0..511] of byte;
ok:boolean;
begin
fn:=ParamStr(1);
if fn=» then writeln(‘makeboot bootsect.bin’)
else
begin
writeln(‘Making boot floppy’);
{$I-}
assign(f,fn);
reset(f,sizeof(buf));
BlockRead(f,buf,1);
close(f);
{$I+}
if IOResult<>0 then
begin
Writeln(‘Failed to read file «‘,fn,'»‘);
Halt(1);
end;
ok:=false;
asm
mov ax, 0301h
mov cx, 1
mov dx, 0
mov bx, seg buf
mov es, bx
mov bx, offset buf
int 13h
jc @error
mov ok, true
@error:
end;
if ok then writeln(‘Done :)’)
else begin
writeln(‘Makeboot failed :(‘);
Halt(1);
end;
end;
end.

Проверка готовности накопителя

Программа проверяет готовность устройства. Если устройство не готово, программа просит нажать клавишу ESC.

; Проверяем корректность и готовность устройства.
; Полностью завершенная COM-программа.
codeseg segment
assume CS:codeseg, DS:codeseg, ES:codeseg
org 100h

Begin: jmp Start
; —-переменные—-
Intvec dd ? ;старый вектор прерывания 24h
Luukku db ‘Disk not valid or ready. Hit Esc!’,10,13,’$’

Start:
;———— Основная программа ————
;Перехватываем прерывание 24h
push ES
mov AX,3524h ;вектор int 24h записывается в ES:BX
int 21h
mov word ptr Intvec,BX ;смещение
mov word ptr Intvec[2],ES ;сегмент
pop ES
;load a new int 24h
mov AX,2524h ;новый вектор 24h
mov DX,offset CError ;адрес
int 21h
;код для проверки готовности устройства
mov DL,1 ;1 — A:, 2 — B: и т.д..
mov AH,36h ;функция определения свободного места на диске
int 21h
cmp AX,-1 ;AX — число секторов в кластере -1
je Loppu ;выход если нет диска или не готов
;устройство готово
;здесь ваш код….
Loppu: int 20h ;завершаем COM-программу
;——— новое прерывание int 24h ——————
assume DS:nothing ;будут использоваться дальние вызовы
CError proc far
pushf ;сохраняем флаги
or AH,AH
js EiLevyke
push DX
push DS
mov AX,CS
mov DS,AX
assume DS:Codeseg
mov DX,offset Luukku
mov AH,9 ;выводим строку DS:DX
int 21h
mov AH,0
int 16h ;ждем нажатия клавиши
cmp AL,27 ;это Esc ?
jne EiEsc
mov AH,4Ch ;завершаем программу
int 21h
EiEsc: pop DS
assume DS:nothing
pop DX
popf
mov AL,1 ;еще раз
iret ;возвращаем управление главной программе
EiLevyke: popf ;восстанавливаем флаги
jmp CS:Intvec ;вызываем старый обработчик int 24h
CError endp
codeseg ends
end Begin