50、汇编语言编程练习与高级语言接口

汇编语言编程练习与高级语言接口

1. 编程练习

1.1 MOV 指令的 Mod R/M 字节

提供以下 MOV 指令的 Mod R/M 字节:

.data
array WORD 5 DUP(?)
.code
mov  ax,@data
mov  ds,ax
mov  BYTE PTR array,5 
; a.
mov  dx,[bp+5] 
; b.
mov  [di],bx 
; c.
mov  [di+2],dx 
; d.
mov  array[si+2],ax 
; e.
mov  array[bx+di],ax 
; f.

1.2 手动汇编指令

手动汇编以下指令,并为每个标记的指令写出十六进制机器语言字节。假设 val1 位于偏移量 0 处。当使用 16 位值时,字节必须以小端序出现:

.data
val1 BYTE 5
val2 WORD 256
.code
mov  ax,@data
mov  ds,ax 
; a.
mov  al,val1 
; b.
mov  cx,val2 
; c.
mov  dx,OFFSET val1 
; d.
mov  dl,2 
; e.
mov  bx,1000h 
; f.

1.3 浮点比较

用汇编语言实现以下 C++ 代码。用 WriteString 调用替代 printf() 函数调用:

double X;
double Y;
if( X < Y )
    printf("X is lower\n");
else
    printf("X is not lower\n");

使用 Irvine32 库例程进行控制台输出,而不是调用标准 C 库的 printf 函数。多次运行程序,为 X 和 Y 分配一系列值来测试程序的逻辑。

1.4 显示浮点二进制

编写一个过程,接收单精度浮点二进制值,并按以下格式显示:
- 符号:显示 + -
- 尾数:二进制浮点,前缀为 “1.”;
- 指数:以十进制显示,无偏置,前面加上字母 E 和指数的符号。

示例:

.data
sample REAL4 -1.75

显示输出:

-1.11000000000000000000000 E+0

1.5 设置舍入模式

(需要宏的知识)编写一个宏来设置 FPU 舍入模式。单个输入参数是一个双字母代码:
- RE:舍入到最接近的偶数
- RD:向下舍入到负无穷
- RU:向上舍入到正无穷
- RZ:向零舍入(截断)

示例宏调用(大小写无关):

mRound Re 
mRound rd
mRound RU
mRound rZ

编写一个简短的测试程序,使用 FIST(存储整数)指令来测试每种可能的舍入模式。

1.6 表达式求值

编写一个程序来计算以下算术表达式:

((A + B) / C) * ((D - A) + E)

为变量分配测试值并显示结果值。

1.7 圆的面积

编写一个程序,提示用户输入圆的半径。计算并显示圆的面积。使用库中的 ReadFloat 和 WriteFloat 过程。使用 FLDPI 指令将 π 加载到寄存器堆栈。

1.8 二次公式

提示用户输入多项式 $ax^2 + bx + c = 0$ 的系数 a、b 和 c。使用二次公式计算并显示多项式的实根。如果有任何根是虚数,则显示适当的消息。

1.9 显示寄存器状态值

Tag 寄存器指示每个 FPU 寄存器中的内容类型,每个寄存器使用 2 位。可以通过调用 FSTENV 指令加载 Tag 字,该指令会填充以下保护模式结构:

FPU_ENVIRON STRUCT
    controlWord    WORD ?
    ALIGN DWORD
    statusWord     WORD ?
    ALIGN DWORD
    tagWord        WORD ?
    ALIGN DWORD
    instrPointerOffset     DWORD ?
    instrPointerSelector   DWORD ?
    operandPointerOffset   DWORD ?
    operandPointerSelector WORD ?
    WORD ? ; not used
FPU_ENVIRON ENDS

编写一个程序,将两个或更多值压入 FPU 堆栈,通过调用 ShowFPUStack 显示堆栈,显示每个 FPU 数据寄存器的 Tag 值,并显示对应于 ST(0) 的寄存器编号。(对于后者,调用 FSTSW 指令将状态字保存到一个 16 位整数变量中,并从位 11 到 13 提取堆栈 TOP 指示符。)

示例输出:

------ FPU Stack ------
ST(0): +1.5000000E+000
ST(1): +2.0000000E+000
R0  is empty
R1  is empty
R2  is empty
R3  is empty
R4  is empty
R5  is empty
R6  is valid
R7  is valid
ST(0) = R6

Tag 值:
| 值 | 含义 |
| — | — |
| 00 | 有效 |
| 01 | 零 |
| 10 | 特殊(NaN、不支持、无穷大或非规格化) |
| 11 | 空 |

2. 高级语言接口

2.1 引言

大多数程序员不会用汇编语言编写大规模应用程序,因为这会花费太多时间。相反,高级语言隐藏了那些会减慢项目开发速度的细节。然而,汇编语言仍然广泛用于配置硬件设备以及优化程序的速度和代码大小。

我们将重点关注汇编语言与高级编程语言之间的接口。首先,展示如何在 C++ 中编写内联汇编代码。接着,将 32 位汇编语言模块链接到 C++ 程序。最后,展示如何从汇编语言调用 C 库函数。

2.2 一般约定

从高级语言调用汇编语言过程时,需要考虑以下几个一般因素:
1. 命名约定 :语言使用的命名约定指的是变量和过程命名的规则或特征。例如,需要回答一个重要问题:汇编器或编译器是否会更改对象文件中标识符的名称,如果是,如何更改?
2. 段名 :段名必须与高级语言使用的段名兼容。
3. 内存模型 :程序使用的内存模型(tiny、small、compact、medium、large、huge 或 flat)决定了段大小(16 位或 32 位),以及调用和引用是近调用(在同一段内)还是远调用(在不同段之间)。

2.2.1 调用约定

调用约定指的是过程调用的底层细节。需要考虑以下细节:
- 被调用过程必须保存哪些寄存器
- 传递参数的方法:在寄存器中、在堆栈上、在共享内存中或通过其他方法
- 调用程序向过程传递参数的顺序
- 参数是按值传递还是按引用传递
- 过程调用后如何恢复堆栈指针
- 函数如何将值返回给调用程序

2.2.2 命名约定和外部标识符

从用另一种语言编写的程序调用汇编语言过程时,外部标识符必须具有兼容的命名约定(命名规则)。外部标识符是指以某种方式放置在模块的对象文件中,以便链接器可以将这些名称提供给其他程序模块的名称。链接器解析对外部标识符的引用,但只有在使用的命名约定一致时才能这样做。

例如,假设一个名为 Main.c 的 C 程序调用一个名为 ArraySum 的外部过程。C 编译器会自动保留大小写,并在外部名称前加上一个下划线,将其更改为 _ArraySum 。而用汇编语言编写的 Array.asm 模块,由于在其 .MODEL 指令中使用了 Pascal 语言选项,将 ArraySum 过程名称导出为 ARRAYSUM 。链接器无法生成可执行程序,因为两个导出的名称不同。

较旧的编程语言(如 COBOL 和 PASCAL)的编译器通常将标识符转换为全大写字母。较新的语言(如 C、C++ 和 Java)保留标识符的大小写。此外,支持函数重载的语言(如 C++)使用一种称为名称修饰的技术,在函数名称中添加额外的字符。例如,一个名为 MySub(int n, double b) 的函数可能会导出为 MySub#int#double

在汇编语言模块中,可以通过选择 .MODEL 指令中的一种语言说明符来控制大小写敏感性。

2.2.3 段名

将汇编语言过程链接到用高级语言编写的程序时,段名必须兼容。使用 Microsoft 简化段指令 .CODE .STACK .DATA ,因为它们与 Microsoft C++ 编译器生成的段名兼容。

2.2.4 内存模型

调用程序和被调用过程必须使用相同的内存模型。例如,在实地址模式下,可以从 small、medium、compact、large 和 huge 模型中选择。在保护模式下,必须使用 flat 模型。

2.3 .MODEL 指令

在 16 位和 32 位模式下,MASM 使用 .MODEL 指令来确定程序的几个重要特征:其内存模型类型、过程命名方案和参数传递约定。当汇编语言被其他编程语言编写的程序调用时,后两者尤为重要。 .MODEL 指令的语法是:

.MODEL memorymodel [,modeloptions]
2.3.1 内存模型

memorymodel 字段可以是表 1 中描述的模型之一。除了 flat 之外,所有模型都用于 16 位实地址模式编程。32 位程序使用 flat 内存模型,其中偏移量为 32 位,代码和数据可以大到 4GB。例如,Irvine32.inc 文件包含以下 .MODEL 指令:

.model flat,STDCALL
模型 描述
Tiny 一个单一的段,包含代码和数据。此模型用于文件名具有 .com 扩展名的程序。
Small 一个代码段和一个数据段。默认情况下,所有代码和数据都是近的。
Medium 多个代码段和一个数据段。
Compact 一个代码段和多个数据段。
Large 多个代码和数据段。
Huge 与 large 模型相同,只是单个数据项可能大于单个段。
Flat 保护模式。使用 32 位偏移量进行代码和数据操作。所有数据和代码(包括系统资源)都在一个 32 位段中。
2.3.2 模型选项

.MODEL 指令中的 modeloptions 字段可以包含语言说明符和堆栈距离。语言说明符确定过程和公共符号的调用和命名约定。堆栈距离可以是 NEARSTACK (默认)或 FARSTACK

2.3.2.1 语言说明符

.MODEL 指令有许多不同的可能语言说明符,其中一些很少使用(如 BASIC、FORTRAN 和 PASCAL)。另一方面,C 和 STDCALL 非常常见。以下是它们与 flat 内存模型结合使用的示例:

.model flat, C
.model flat, STDCALL

STDCALL 是调用 Windows 系统函数时使用的语言说明符。在将汇编语言代码链接到 C 和 C++ 程序时,使用 C 语言说明符。

  • STDCALL :STDCALL 语言说明符会使子程序参数以相反的顺序(从最后一个到第一个)压入堆栈。例如,以下高级语言函数调用:
AddTwo( 5, 6 );

当 STDCALL 是所选语言说明符时,等效的汇编语言代码如下:

push 6
push 5
call AddTwo

另一个重要的考虑因素是过程调用后如何从堆栈中移除参数。STDCALL 要求在 RET 指令中提供一个常量操作数。该常量表示在 RET 从堆栈中弹出返回地址后添加到 ESP 的值:

AddTwo PROC
    push ebp
    mov ebp,esp
    mov eax,[ebp + 12]   ; 第二个参数
    add eax,[ebp + 8]    ; 第一个参数
    pop ebp
    ret 8                ; 清理堆栈
AddTwo ENDP

通过将 8 添加到堆栈指针,将其重置为调用程序将参数压入堆栈之前的值。

最后,STDCALL 修改导出(公共)过程名称,将其存储为以下格式:

_name@nn

在过程名称前添加一个下划线,并在 @ 符号后面跟一个整数,表示过程参数使用的字节数(向上舍入到 4 的倍数)。例如,假设过程 AddTwo 有两个双字参数。汇编器传递给链接器的名称是 _AddTwo@8

  • C 说明符 :C 语言说明符要求过程参数从最后一个到第一个压入堆栈,与 STDCALL 相同。关于过程调用后从堆栈中移除参数,C 语言说明符将责任放在调用者身上。在调用程序中,将一个常量添加到 ESP,将其重置为参数压入堆栈之前的值:
push 6 ; 第二个参数
push 5 ; 第一个参数
call AddTwo
add esp,8 ; 清理堆栈

C 语言说明符在外部过程名称前添加一个下划线字符。例如:

_AddTwo

2.4 检查编译器生成的代码

C 和 C++ 编译器长期以来一直在生成汇编语言源代码,但程序员通常看不到这些代码。这是因为汇编语言是创建可执行文件过程中的一个中间步骤。幸运的是,可以要求大多数编译器生成一个汇编语言源代码文件。例如,表 2 列出了控制汇编源代码输出的 Visual Studio 命令行选项。

命令行 列表文件内容
/FA 仅汇编列表
/FAc 带机器代码的汇编
/FAs 带源代码的汇编
/FAcs 汇编、机器代码和源代码

检查编译器生成的代码文件有助于理解底层细节,如堆栈帧构造、循环和逻辑的编码,并且可能有助于查找底层编程错误。另一个好处是可以更容易地检测不同编译器生成的代码之间的差异。

让我们看一个 C++ 编译器生成优化代码的例子。编写一个简单的 C 方法 ArraySum 并在 Visual Studio 2012 中编译,使用以下设置:
- 优化 = 禁用(使用调试器时需要)
- 倾向大小或速度 = 倾向快速代码
- 汇编器输出 = 带源代码的汇编

以下是用 ANSI C 编写的 ArraySum 源代码:

int arraySum( int array[], int count )
{
    int i;
    int sum = 0;
    for(i = 0; i < count; i++)
        sum += array[i];
    return sum;
}

编译器为 ArraySum 生成的汇编代码如下:

 1: _sum$ = -8 ; size = 4
 2: _i$ = -4 ; size = 4
 3: _array$ = 8 ; size = 4
 4: _count$ = 12 ; size = 4
 5: _arraySum PROC ; COMDAT
 6: 
 7: ; 4    : {
 8: 
 9:    push ebp
10:    mov ebp, esp
11:    sub esp, 72 ; 00000048H
12:    push ebx
13:    push esi
14:    push edi
15:
16: ; 5    : int i;
17: ; 6    : int sum = 0;
18:
19:    mov DWORD PTR _sum$[ebp], 0
20:
21: ; 7    : 
22: ; 8    : for(i = 0; i < count; i++)
23:
24:    mov DWORD PTR _i$[ebp], 0
25:    jmp SHORT $LN3@arraySum
26: $LN2@arraySum:
27:    mov eax, DWORD PTR _i$[ebp]
28:    add eax, 1
29:    mov DWORD PTR _i$[ebp], eax
30: $LN3@arraySum:
31:    mov eax, DWORD PTR _i$[ebp]
32:    cmp eax, DWORD PTR _count$[ebp]
33:    jge SHORT $LN1@arraySum
34:
35: ; 9    : 
sum += array[i];
36:
37:    mov eax, DWORD PTR _i$[ebp]
38:    mov ecx, DWORD PTR _array$[ebp]
39:    mov edx, DWORD PTR _sum$[ebp]
40:    add edx, DWORD PTR [ecx+eax*4]
41:    mov DWORD PTR _sum$[ebp], edx
42:    jmp SHORT $LN2@arraySum
43: $LN1@arraySum:
44:
45: ; 10   : 
46: ; 11   : return sum;
47:
48:    mov eax, DWORD PTR _sum$[ebp]
49:
50: ; 12   : }
51:
52:    pop edi
53:    pop esi
54:    pop ebx
55:    mov esp, ebp
56:    pop ebp
57:    ret 0
58: _arraySum ENDP

2.5 调试器设置

要在 Visual Studio 中调试 C 和 C++ 程序时查看汇编语言源代码,从工具菜单中选择选项以显示对话框窗口,并选择箭头指示的选项。在启动调试器之前进行此操作。然后,在调试会话开始后,右键单击源代码窗口并从弹出菜单中选择“转到反汇编”。

编译器有多种生成代码的方式。例如,它们可以优化代码以使用最少的机器代码字节。或者,它们可以尝试生成尽可能快的代码,即使输出结果会导致更多的机器代码字节(通常是这样)。最后,编译器可以通过同时优化代码大小和速度来进行折衷。为速度优化的代码可能包含更多指令,因为循环可能会被展开以实现更快的执行。机器代码也可以分成两部分,以利用双核处理器同时执行两条并行代码的能力。

3. 代码分析与总结

3.1 汇编编程练习总结

在前面介绍的汇编编程练习中,涵盖了多个重要的方面,从基本的 MOV 指令操作到复杂的浮点运算和表达式求值,每个练习都有其独特的意义和挑战。

练习内容 关键要点
MOV 指令的 Mod R/M 字节 理解指令编码,掌握不同寻址方式下的字节表示
手动汇编指令 熟悉汇编指令到机器语言的转换,了解小端序存储
浮点比较 学会使用汇编语言处理浮点数据的比较逻辑,结合库函数进行输出
显示浮点二进制 掌握浮点数据的二进制表示和格式化输出
设置舍入模式 运用宏来控制 FPU 的舍入行为,通过测试程序验证不同模式
表达式求值 实现复杂算术表达式的计算,合理分配变量值进行测试
圆的面积计算 结合用户输入和库函数,完成几何计算任务
二次公式求解 处理多项式根的计算,考虑实根和虚根的不同情况
显示寄存器状态值 了解 FPU 寄存器的状态表示,通过指令获取和显示相关信息

这些练习不仅加深了对汇编语言基本指令和操作的理解,还锻炼了处理复杂数据类型(如浮点数据)和实现特定功能的能力。

3.2 高级语言接口总结

3.2.1 一般约定的重要性

高级语言与汇编语言之间的接口涉及多个重要的约定,包括命名约定、段名和内存模型。这些约定确保了不同语言编写的模块能够正确链接和协同工作。

  • 命名约定 :不同语言对标识符的处理方式不同,如大小写、名称修饰等。在链接时,必须保证外部标识符的命名一致,否则链接器将无法生成可执行程序。
  • 段名 :使用兼容的段名是链接成功的关键。例如,Microsoft 简化段指令 .CODE .STACK .DATA 与 Microsoft C++ 编译器生成的段名兼容。
  • 内存模型 :调用程序和被调用过程必须使用相同的内存模型,不同的内存模型适用于不同的编程模式(实地址模式和保护模式)。
3.2.2 调用约定和语言说明符

调用约定和语言说明符决定了参数传递、堆栈管理和名称处理的方式。

  • STDCALL :常用于调用 Windows 系统函数,参数以逆序压入堆栈,RET 指令需要指定清理堆栈的常量。导出的过程名称有特定的格式。
  • C 说明符 :参数传递顺序与 STDCALL 相同,但清理堆栈的责任在调用者。外部过程名称前添加下划线。
3.2.3 编译器生成代码的分析

通过检查编译器生成的汇编代码,可以深入了解高级语言代码在底层的实现细节。例如,在 ArraySum 函数的汇编代码中,可以看到编译器如何进行堆栈帧的构造、变量的初始化和循环的实现。这有助于优化代码、查找错误和理解不同编译器的特性。

3.3 流程总结

下面是一个简单的 mermaid 流程图,展示了从高级语言调用汇编语言过程的一般流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([高级语言程序]):::startend --> B(遵循命名约定):::process
    B --> C(选择兼容的段名):::process
    C --> D(使用相同的内存模型):::process
    D --> E{选择语言说明符}:::decision
    E -->|STDCALL| F(参数逆序压栈,RET 指定清理常量):::process
    E -->|C 说明符| G(参数逆序压栈,调用者清理堆栈):::process
    F --> H(链接汇编模块):::process
    G --> H
    H --> I([生成可执行程序]):::startend

这个流程图清晰地展示了从高级语言调用汇编语言过程时需要遵循的步骤和决策点。首先,高级语言程序需要遵循命名约定,确保外部标识符的一致性。然后,选择兼容的段名和相同的内存模型,以保证链接的正确性。接着,根据需求选择合适的语言说明符,不同的语言说明符决定了参数传递和堆栈管理的方式。最后,将汇编模块与高级语言程序链接起来,生成可执行程序。

3.4 实践建议

  • 多做练习 :通过不断完成各种编程练习,加深对汇编语言和高级语言接口的理解。可以尝试修改练习中的代码,观察不同的运行结果。
  • 分析编译器代码 :经常查看编译器生成的汇编代码,学习编译器的优化策略和底层实现细节,这有助于提高自己的编程水平。
  • 注意约定细节 :在实际项目中,严格遵循命名约定、段名和内存模型等规则,避免因约定不一致导致的链接错误。
  • 结合实际应用 :将所学知识应用到实际项目中,如优化程序性能、配置硬件设备等,提高解决实际问题的能力。

总之,汇编语言与高级语言的结合为我们提供了更强大的编程能力。通过深入理解和掌握这些知识,我们可以更好地应对各种编程挑战,开发出高效、稳定的程序。

在数字化环境中,线上票务获取已成为参各类活动的主要途径。随着公众对热门演出需求的增长,票源往往在开放销售后迅速告罄,导致普通消费者难以顺利购得所需票券。为应对这一挑战,部分技术开发者借助编程手段构建了自动化购票辅助程序,旨在提升用户成功获取门票的概率。本文将以一个针对特定票务平台设计的自动化工具为例,系统阐述其设计理念、技术组成及具体实施流程。 秀动网作为国内知名的演出及体育赛事票务销售平台,因活动热度较高,常出现访问拥堵、瞬时抢购压力大等现象,使得常规购票过程面临困难。因此,开发一款能够协助用户更有效完成票务申购的辅助工具具有实际意义。 该工具主要具备以下几项关键功能:持续监控目标平台的票务信息更新;在票务释放时自动执行选座、添加至购物车及提交订单等系列操作;集成一定的异常处理机制,以应对网络延迟或服务器响应异常等情况。 在技术实现层面,选用Python作为开发语言,主要基于其语法简洁、标准库第三方资源丰富,适合快速构建功能原型。同时,Python在网络通信浏览器自动化方面拥有如requests、selenium等成熟支持库,为程序实现网页交互数据抓取提供了便利。 开发过程主要包括以下环节:首先解析目标网站的页面结构,明确可通过程序操控的网页元素路径;随后编写监控模块,实时检测新票务信息的上线并及时触发后续操作;接着模拟用户操作流程,包括自动填写个人信息、选择座位偏好、完成购物车添加等步骤,并通过行为模拟降低被平台反爬虫机制识别的可能;最终实现订单自动提交,并在成功购票后向用户发送通知。 此外,该工具提供了可配置的操作界面,允许用户根据个人需求设定抢票时间、目标活动类型及座位选择等参数,从而在提升使用体验的同时,减少对票务平台服务器资源的非必要占用。 需指出的是,尽管此类工具能提高购票效率,但其使用可能涉及违反平台服务协议或相关法规的风险。各票务销售方通常对自动化抢票行为设有明确约束,因此开发使用者均应遵守相应规定,确保技术应用的合法性。 综上所述,该基于Python的票务辅助工具是针对特定场景设计的自动化解决方案,通过技术手段改善用户购票体验,但同时也强调必须在法律平台规则框架内合理使用此类技术。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值