【问题标题】:What's the point of the pointer in MASM?MASM中的指针有什么意义?
【发布时间】:2017-03-01 11:47:16
【问题描述】:

我正在学习Assembly Language For Intel-Based Computers,有些事情让我很困惑。

众所周知,我们可以使用任何通用寄存器来寻址,如下所示:

.data
array1 byte 1,2,3,4
.code
mov esi,offset array1
mov al,[esi] ; al=1
mov al,[esi+1] ; al=2
mov al,[esi+2] ; al=3
mov al,[esi+3] ; al=4

[esi+idata](idata 是立即数)就像指针一样,够强,够用。

但是这本书告诉我如何键入定义这样的指针:

pbyte tpyedef ptr byte
.data
array1 byte 1,2,3,4
ptr1 pbyte array
.code
mov esi,ptr1
mov al,[esi] ; al=1
mov al,[esi+1] ; al=2
mov al,[esi+2] ; al=3
mov al,[esi+3] ; al=4

所以真正让我困惑的是,既然array1 是指向数据的指针,那么ptr1 的意义何在?指针也需要 4 个字节来存储。

mov esi,ptr1不等于mov esi,offset array1吗?

另外,书中给出了不同类型的指针,例如:

pbyte tpyedef ptr byte
pbyte tpyedef ptr word
pbyte tpyedef ptr dword
pbyte tpyedef ptr qword

指针不只是指向内存中的一个字节吗?有什么不同?我试过这样:

pt typedef ptr qword 
.data 
array byte 1,2,3,4,5,6,7,8
arrptr pt array
.code
mov esi,arrptr
mov eax,[esi]

我在 eax 中得到 04030201,一个 dword 类型,而不是一个 qword。

综上所述,我想知道MASM中指针的存在是否很有必要。或者有什么指针可以但[esi+idata] 不能?

【问题讨论】:

  • ptr1 可以修改为在数据中移动,而array1 将始终指向数据的开头。它是更高级别的抽象,因此您不必显式使用寄存器索引。
  • @m0skit0 好吧,这是我忽略的用法,但我曾经使用寄存器索引(如果需要更多指针,我会pushpop)。
  • pushpop 仅适用于堆栈。
  • @m0skit0 我的意思是我会在寄存器用于其他事情之前将指针压入堆栈以存储它,并在我需要使用它时弹出指针。它还可以在数据中移动。
  • masm 有一些愚蠢的高级功能,这就是其中之一。

标签: assembly masm masm32


【解决方案1】:

mov esi,ptr1 不等于 mov esi,offset array1 吗?

请注意,mov esi, ptr1 实际上意味着 mov esi, DWORD PTR [ptr1],这是一个重要的区别。
这是一个比mov esi, OFFSET array 更多的间接级别。

简单地说 - 如果array 驻留在比方说 0x1000,mov esi, OFFSET array 只是mov esi, 1000h 的可读版本(实际执行的指令)。
而如果ptr1 位于 0x1010,mov esi, ptr1 被组装成 mov esi, DWORD PTR [1010h] - 读取 0x1010 处 DWORD 的内容并将其存储到 ESI 中

为了使两条指令具有相同的行为,0x1010 处的 DWORD 必须为 0x1000(即指向 array)。

但是,它也可以指向任何其他数组。
在您的示例中,它几乎没用,不是因为您只有一个数组,而是因为数据是静态分配的-因此您始终可以使用 OFFSET 获取其地址。
想象一下你想丢弃一个字符串的第一个单词,一个过程可以把一个指针作为参数(如果数据是静态分配的,可以用OFFSET初始化)并返回一个指向第一个非丢弃单词的指针

+--- Original pointer, initialized with OFFSET
|
v
Hello world from SO!
      ^
      |
      +--- Pointer returned

您可以将结果保存到一个名为 ptr1 的变量中。
现在,如果您想重复该过程,您需要使用 ptr1 的值作为该过程的输入。
没有OFFSET 会这样做,因为在汇编程序时无法推断出指针的值。


指针不只是指向内存中的一个字节吗?有什么区别?

一般来说,给汇编器提供比需要更多的信息会很方便:

  1. 记录代码
  2. 让它执行一些初始化
  3. 让它执行一些检查

知道变量是指针可以帮助未来的读者。

TYPEDEF PTRcan be used to specify near and far pointers,但这与任何现代操作系统设置的保护模式无关。
远指针的初始化与近指针不同。

也许 MASM 会对指针和指针的类型进行一些检查?我对此表示怀疑,但我承认我不知道。

正如Michael Petch in the comments所指出的:

MASM 实际上(与其他汇编程序不同)对指向已明确定义然后直接引用的数据的指针进行基本类型检查。


我在 eax 中得到 04030201,一个 dword 类型,而不是一个 qword。

目标是 32 位,所以源不能是 64 位。
如果程序员明确使用大小(例如,使用寄存器名称或WORD PTR [...]),则隐式大小将被覆盖。


是否有指针可以但[esi+idata] 不能的东西?

不,任何高级代码功能最终都会组装到 ISA 指令中,因此通过使用它们,您可以模拟任何高级功能。

请注意,某些指令在元级别上起作用,例如 .data
这些无法模拟,因为它们不只是生成代码。

【讨论】:

  • 我应该更准确。我的意思是我可以将[esi+idata] 与堆栈一起使用(在寄存器被其他指令更改之前存储指针)而不是实际的指针,我认为通过寄存器寻址在某种程度上更容易。现在我认为将其存储在数据段中可能会更方便。非常感谢这么详细的回答!
  • 另外,在大多数调用约定中,返回的指针在寄存器中,不是吗?如果我想返回一个指针变量,我必须在调用函数之前定义一个。
  • @Kon 堆栈对于小型本地临时人员通常很方便;易于分配,从函数返回时易于释放,缓存的“局部性”很大(调用者通过将返回地址推入堆栈来修改堆栈,因此至少部分堆栈内存在缓存中是热的)。关于您的问题和 MASM:MASM 有几个想要成为高级别的指令和关键字,我个人认为它们无用且令人困惑。也许它们对于每天在大型项目中使用它们的人来说很方便,但是为什么要在 asm 中编写更大的项目呢?我更喜欢简单的 asm 原始方式,例如 mov eax,offset array1 ; or [ptr1]
  • MASM 实际上(与其他汇编程序不同)对指向已显式定义然后直接引用的数据的指针进行基本类型检查。我个人认为这是一个无用的功能。
  • 感谢@MichaelPetch,我将您的评论添加到我的答案中,因为我相信它很有价值。不过,我将省略最后一句,即使我非常同意。
【解决方案2】:

在 ASM 中,您实际上永远需要 typedef 语句来开发应用程序。实际上,它们只不过是注释文本。如果您使用 FPU,则需要学习一些真正的内部格式,但对于标准 32 位 ASM,您真的只需要知道某些内容的大小,即它是 BYTE、WORD 还是 DWORD。当然,如果您正在与将 MSB 视为符号位的库进行交互,或者您需要自己使用有符号值,请记住这一点并使用 jge 而不是 jae 等,但这不是火箭科学。

一个大问题: 这本书是一本你需要通读的教育书籍,因此将根据你对方法的坚持程度进行评分吗? p>

  • 如果是,对不起,伙计,咬紧牙关开始记住那些东西。

  • 如果不是,我认为它可能是由使用 C 进行思考并且只是将他们以 C 为中心的思想转换为 ASM 的人编写的。 (不是犯罪——可能是学习 C 的好人:)

您显然只是在学习,但您提出的问题非常好。

所以真正让我困惑的是,既然array1 是一个指向数据的指针,那么ptr1 的意义何在?

对于您的示例代码,没有。由于 ptr1 处的 DWORD 被初始化为 OFFSET array1 然后永远不会改变。 然而, 很多时候你会想要维护一个指向列表/数组等的全局指针。如果你每次输入那段代码时都从数组的开头开始,那么当你指出,你可以使用 OFFSET ... 简单。但是,如果您的任务的性质是,您必须记住下次返回时要到达的位置,那么您需要将该位置存储在某个地方;因此指针。

指针也需要4个字节来存储。

当然可以,但是不要太拘泥于指针空间,如果指针是完成这项工作的正确方法,那么 4 个字节就不算什么。如果是我,我会声明为:

.data

lpThisArray   dd  OFFSET array1

因为它就是这样,它是一个 DWORD。它是 32 个 iddy-biddy 晶体管,它们在任何给定的时刻单独地要么开派对,要么睡着。他们既不知道也不关心他们是否持有数组指针、光标 Y 坐标、您的堂兄弟单元格编号或单词“TWIT”。他们有生活要过,他们不关心我们的问题……但没关系!我们在这里不是盲目的。我们称它为“lpThisArray” 我非常有信心我不会不小心拨通它并试图让他在下班途中接我一些东西。我在家也一样聪明,我几乎总是记得不要把餐具插在电源插座上——当你全神贯注的时候,你能做的事情真是太棒了!

在 32 位域中,几乎不需要使用 db、dw 或 dd(= BYTE、WORD 或 DWORD)以外的任何东西来声明标准数字/字符串/指针等内容,因为最终这就是一切无论如何。除了 FPU/SSE 等之外,一切都是 8 位、16 位或 32 位。

当然,结构定义和语法非常方便,因此如果您正在使用 API 结构(或自己制作大型结构),使用它们是有意义的。

MyOfn OPENFILENAME

反正打字很容易;)

...只要你明白:

.data?

csrPos  POINT  <>

.code

invoke GetCursorPos,OFFSET csrPos
mov ebx,csrPos.y

在各方面都相同:

.data?

csrX    dd  ?
csrY    dd  ?

.code

push OFFSET csrX
call GetCursorPos
mov ebx,csrY

它们将编译为相同的可执行文件。

mov esi,ptr1 不等于 mov esi,offset array1 吗?

撇开我个人不喜欢作者不必要地深奥的数据大小声明选择,答案是肯定的,只有当 ptr1 指向数组的开头时。 ptr1 定义了一个 DWORD 位置,可以保存任何 32 位值,但 OFFSET array1 是一个固定值,它是数组中第一个字节的地址。

好的......初学者的偏移量。好点子。最好先考虑一下它之前的“.data”。

与高级语言中的声明不同,“.data”不是一个方便的以人为中心的标题,您可以在其中列出您想要使用的数字。在 ASM 中,它定义了 exe 文件中物理部分的开始,系统加载程序将映射到进程虚拟地址空间中的特定地址。从程序编译的那一刻起,其中的所有内容都被定义为位于特定的预定位置。 (理论上它们可以重新建立,但实际上它们几乎从来没有,而且无论如何这与此无关)。

习惯于高级语言的人可以原谅假设程序运行时,它会找到这个“OFFSET”人,这个人会迅速跑开并找到数组,然后报告它的位置。事实上,OFFSET 是一个编译器指令,它将导致编译器将提供的标签的实际内存地址直接硬编码到程序的操作码中。在这种情况下,array1 标记了数组中第一个字节的位置。该“偏移量”准确地定义了一个且唯一的位置,该位置在可执行文件的生命周期内永远不会改变。在这种情况下,您可以将 ptr1 视为一个变量,因此它可以保存任何 32 位值,并且该值每秒可以更改 1000 次。

数据标签 这对于解释指针点很方便,但实际上它是不准确的。 “ptr1”实际上是一个标签。它标记 .data 部分中的特定位置,并且在程序的生命周期内也是绝对固定的。保存指针值的是该位置的 4 个字节。

例如:

mov ptr1,12345

;( then later)

mov esi,OFFSET ptr1
mov eax,[esi]
; eax now holds 12345

OFFSET 可以接受 ptr1 作为参数这一事实意味着它必须被修复。数据标签像任何其他标签一样定义位置,区别只是语法约定之一,因为数据标签隐含取消引用应用于它们。 “ptr1”就像所有其他标签一样,实际上是 OFFSET 的结果,因为它编译为一个恒定的 32 位立即值,但作为一个数据标签,当我们编码“ptr1”时,MASM 编译“[ptr1]”。这使人们更容易将数据标签视为变量并将其作为变量来使用,但它也往往会掩盖实际发生的情况。

关于数据标签的另一件事是它们附加了一个大小(不是类型)。 这是 MASM 的功能,许多人(有时包括我自己;)倾向于错误地将其称为类型检查的一种形式。 虽然在这一点上它几乎变成了一种在哲学讨论中,MASM 确实记录了可以实际确定的所有内容的大小,这使用户不必为几乎每条指令键入“DWORD ptr”、“BYTE ptr”或“WORD ptr”而令人作呕。这不是必需的,因为编译器已经知道几乎所有东西的大小。

在我看来,这在很大程度上是作为一种节省时间/提高生产力的功能来实现的,但是副产品是,当编译器尝试处理涉及两种不同大小定义的指令时,它不知道哪一个是正确的,因此不知道应该生成哪个操作码,因此无法继续。唯一让我感到厌烦的是,我觉得我们应该能够通过显式声明 SIZE ptr 来覆盖它,而不必诉诸于旧栗子,即用有问题的数据标签的偏移量加载寄存器并取消引用它,从而摆脱附上尺寸声明。

指针不只是指向内存中的一个字节吗?

是的..有点。指针只是一个数字,它指向一个 address 多于一个 byte,因为使用 32 位处理器,您可以一次读取或写入 1 2 或 4 个字节往返该地址的时间。

pt typedef ptr qword 
.data 
array byte 1,2,3,4,5,6,7,8
arrptr pt array
.code
mov esi,arrptr
mov eax,[esi]

这只是由于过度消费我们将成为普利策奖得主的产品而导致的一点混乱......

实际上你已经这样做了:

.data 
array  db 1,2,3,4,5,6,7,8
arrptr dd OFFSET array

.code
mov esi,arrptr
mov eax,[esi]

这是一种相当迂回的方式:

.data
array  db 1,2,3,4,5,6,7,8

.code
mov eax,OFFSET array
mov eax,[eax]

由于您已将一个值加载到源是取消引用的指针的 eax 中,因此可能已附加到先前保存该指针的容器的任何大小定义现在早已不复存在。对于这个 mov 指令,数据大小由目标操作数定义,即 eax 本身。由于 eax 是一个 32 位(4 字节)的寄存器,因此已经加载了 4 个字节。

我在 eax 中得到 04030201,一个 dword 类型,而不是一个 qword

我恳求你,如果你知道如何将一个 qword 输入 eax,请告诉我诀窍;)

【讨论】:

  • 万分感谢。但是我的意思是既然我们可以使用这么多通用寄存器来寻址[esi+idata],是否需要使用指针?(如果我想将位置存储在某个地方,我可以push寄存器中的地址和pop。)另外我说not a qword 表示我使用了一个 qword 类型指针,但得到了一个 dword 数据,这让我很困惑类型定义的意义何在。
  • 第 1 部分:指针是等于某物地址的数字。您可以将指针存储在寄存器中。您可以将一个推入/弹出堆栈,也可以将其存储在内存中。你的问题似乎是“我是否需要将它存储在我使用基于'ptr'的typedef定义的.data部分的区域中”在这种情况下答案肯定是'否'。
  • 第 2 部分:类型是人类的构造。当您的处理器到达一个地址时,它并不关心您认为那里的“类型”。对于 32 位 CPU,它将根据指令读取或写入 1、2 或 4 个字节,仅此而已。一个 QWORD 是 8 个字节。一个 32 位的 reg 一次只能保存 4 个字节。您可以编写 func 来使用 QWORD 执行操作,但无论它是什么,它们一次只能执行 4 个字节。考虑一个 POINT 结构。指向它的 ptr 指向 2 个 DWORD(=一个 QWORD)。如果后面有另一个 POINT 结构,您可以将其称为 RECT (DQWORD),但 ptr 仍然是 32 位地址。
  • 恕我直言,很少,正如我在回答中所说,我只会使用“lpThisArray dd OFFSET array1”
  • 或者实际上我可能会将其放入 .data 中?并使用“lpThisArray dd?”然后再初始化它
猜你喜欢
  • 2011-12-04
  • 2018-02-09
  • 1970-01-01
  • 2010-12-24
  • 2020-12-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多