assembly x86 16位汇编中CLI和STI指令的工作示例

h4cxqtbf  于 2023-05-23  发布在  其他
关注(0)|答案(1)|浏览(174)

我想要一个x86 16位汇编中的clisti指令的实际示例,即一个用这种语言写的代码的例子,它让我通过实践知道这些指令是干什么的,并比理论走得更远。
我知道文档说cli禁用中断标志,sti启用它,中断标志不影响不处理不可屏蔽中断(NMI)或由int指令生成的软件中断。
在我的教程中,我有这样的代码:

mov ax, 0x8000
    cli
    mov ss, ax
    mov sp, 0xF000
    sti
  • 我的测试让我说clisti在课程中给出的例子中是无用的,在做了几次测试之后,我能够验证无论我放clisti还是删除这些指令,结果总是相同的。
  • 对于课程中给出的例子,演讲者在不同主题上对clisti的有用性的解释纯粹是理论上的。也就是说,为了安全起见,必须放置clisti,以避免bug/崩溃。discord上的一位发言人说,当我初始化段和堆栈偏移量时,有百万分之一的可能性会出错。这意味着他永远无法自己验证他的理论解释,他只是接受理论,没有好奇心去进一步实验,不可能通过实践来验证,因为有百万分之一的机会出现问题。
  • 在各种文档/网站上,严格来说,没有其他实际的例子可以真正演示clisti的功能以及它是如何有用的,只是复制和粘贴了没有示例代码的文档,即cli将中断标志设置为0,sti将其设置为1。禁用时,忽略硬件中断。零使用的例子,只是理论上的句子,在实践中没有任何东西可以测试这种东西,确实有一个例子在法语文档中,但这个例子也没有用,比我遵循的教程的例子真正理解。也就是说,一个初始化一个段并将clisti放在代码行之前和之后的示例,如果我们删除clisti,无论如何结果都是相同的(也许我们有百万分之一的机会,如果我们删除clisti,就会出现问题,这很好,它让我永远不会在实践中检查理论)。
  • 另一位在discord上发言的人告诉我,他对所有这些都做了一些实验,他用汇编语言编写了一段时间,根据他的经验,他理解为什么你必须放clisti,因为否则会导致问题,所以你必须放它,仅此而已。当我要求他给予我一个实际的例子(这应该是他的胡同,因为他练习),他没有这样做,因为他不在家,但另一方面给了我一个理论垫再次向我解释它是如何有用的,所以显然我们可以非常详细地解释它是如何有用,但从来没有证明实用程序与一个实际的例子在x86 16位汇编。

我指定我不熟悉硬件中断。我只测试了可以用int调用的软件中断。
我在内核模式,我想要一个实际的例子,导致代码有硬件中断的问题,然后另一个例子与clisti,可以解决这个问题。

oxosxuxt

oxosxuxt1#

当然,cli/sti的全部目的是管理硬件中断的处理,因此您需要对硬件中断的一般工作原理有一些了解。
以下是一个简要概述:连接到CPU的硬件设备可以触发硬件中断(在8086的情况下,通过将高电压连接到CPU芯片上的INTR引脚,并使用其他引脚来发信号通知应该调用哪个中断向量)。当这种情况发生时,* 如果 * 中断标志被设置,则CPU完成当前正在执行的指令,然后将CS、IP和FLAGS压入堆栈,并跳转到中断向量表的相应条目中指定的地址,即内存的低1024字节(0000:0000-0000:0400)。程序员应该事先将此条目设置为指向要作为响应运行的代码块(中断处理程序)。中断处理程序将执行处理硬件中断所需的任何操作,然后执行IRET以返回到被中断的任何代码。导致硬件中断的设备示例如下:按下键盘上的键,字节到达串行端口,定时器中断(MS-DOS设置外部定时器以产生18.2 Hz的中断,即每55 ms)。
如果中断标志没有被设置,什么都不会发生,但是当标志最终被再次设置时,中断处理程序将被调用。
因此,只要您不希望中断发生,就应该清除中断标志。这通常是因为您正在使用当前代码和中断处理程序之间共享的某些资源,因此如果中断处理程序在此时运行,则会出现冲突。
例如,让我们考虑定时器中断。一个简单的处理程序可能什么也不做,只是增加一个计数器,这样执行的主线程就可以知道已经过去了多少时间。(8086没有任何其他内置时钟硬件。)如果一个16位计数器就足够了,您可以简单地拥有:

ticks DW 0
handler:
    inc word ptr [ticks]
    iret

main_code:
    mov ax, [ticks] ; now ax contains the number of ticks

但是在18.2 Hz时,我们非常接近每小时65536次滴答(我想这就是为什么选择数字18.2),所以计数器大约每小时都会溢出。如果需要跟踪比这更长的时间间隔,这就不好了,所以我们应该使用32位计数器。由于x86-16没有32位算术指令,我们必须使用ADD/ADC对。我们的代码可能看起来像这样:

ticks DD 0
handler:
    add word ptr [ticks], 1
    adc word ptr [ticks+2], 0
    iret

main_code:
    mov ax, [ticks]
    ;;; BUG what if interrupt occurs here ???
    mov dx, [ticks+2]
    ; now dx:ax contains the 32-bit number of ticks

但是这个代码有一个bug。如果偶然的定时器中断应该发生在标记为BUG的指令之间,则主代码将使ticks的低和高字不同步。例如,假设ticks的值是0x1234ffff。主代码将低位字0xffff加载到ax中。然后发生定时器中断并递增ticks,因此现在为0x12350000。中断处理程序返回,主代码执行mov dx, [ticks+2],得到值0x1235。所以现在主代码已经加载了值0x1235ffff,这是非常错误的:比实际时间晚了整整一个小时。
我们可以通过使用cli/sti禁用中断来解决这个问题,这样在标记为BUG的站点上就不会发生中断。更正后的代码如下所示:

main_code:
    cli
    mov ax, [ticks]
    mov dx, [ticks+2]
    sti

在32位计数器的特定情况下,碰巧有其他方法可以在不禁用中断的情况下解决这个问题,但您可以理解。你可以想象一些更复杂的数据结构,处理程序和主代码可能都需要用途:一个I/O缓冲区,一些更大的结构,其中包含关于刚刚发生的I/O事件的信息,一个链表,等等。
CPU的寄存器也是一个共享资源,例如您注意到的SS:SP示例。假设堆栈当前位于1234:5678,主代码希望将其切换到2222:4444。你会想到做:

switch_stack:
    mov ax, 0x2222
    mov ss, ax
    ;;; BUG: what if interrupt occurs here?
    mov sp, 0x4444

如果中断发生在BUG行,SS:SP的值将是2222:5678,这是CPU在跳转到处理程序之前推送CS/IP/FLAGS值的地方。这将非常糟糕,因为这不是旧堆栈或新堆栈的正确位置。该地址可能有重要数据,CPU现在正在覆盖这些数据,因此我们现在将面临一个难以重现的内存损坏错误。
所以我们同样会考虑用

switch_stack:
    mov ax, 0x2222
    cli
    mov ss, ax
    ;;; interrupt can't occur here!
    mov sp, 0x4444
    sti

现在碰巧这实际上是一个特例。由于在这种情况下,忘记禁用中断将是特别令人讨厌的,8086的设计者决定为程序员做一点帮助。mov ss, reg指令有一个非常特殊的功能,它会自动禁用一条指令的中断。因此,实际上,如果您编写mov ss, ax立即mov sp, 0x2222,则在此期间不会发生中断,并且代码实际上在没有cli/sti的情况下是安全的。

但让我再次强调,这是一个独特的特殊情况。我相信只有mov ss, regpop ss具有这样的功能,所以像32位滴答计数器这样的例子确实需要cli/sti。事实上,如果你把这两条指令颠倒过来,先写mov sp, 0x2222,然后写mov ss, ax(从表面上看,mov ss, ax看起来一样好),你会再次遇到一个bug,中断处理程序可能会被调用,堆栈指向1234:2222。此外,正如@ecm在评论中指出的那样,一些早期的8086/8088芯片有一个硬件错误(?),其中“禁用一条指令的中断”功能不起作用,因此在这样的芯片上,您还必须使用cli/sti。(或者这个特性直到后来才真正成为规范的一部分?)
386增加了一条lss指令,在一条指令中加载堆栈段和堆栈指针,这是解决这个问题的一种更健壮的方法。在这种情况下,这一点也更重要,因为在虚拟8086模式下,cli/sti不会直接执行,而是会陷入操作系统,这非常缓慢,最好尽可能避免。
你认为这可能性很低,我们真的不需要担心它。让我们看看我们的32位计时器示例,并想象一个具有“闹钟”功能的应用程序。在执行其他工作时,它会定期检查滴答计数器,比如说每秒100次,以查看是否已经过了指定的时间,如果是,它会做一些事情来提醒用户。如果您忽略了cli/sti,那么如果在那里发生了一个中断,低字等于0xffff(每小时发生一次),它会认为时间比实际时间晚一个小时,因此可能会提前一个小时发出警报。(如果你想更戏剧化,用“激活危险机械”、“发射导弹”等代替“发出警报”)
8086上的mov ax, mem指令占用了10个时钟周期,因此当我们易受攻击时,每秒有1000个时钟周期。最初的IBM PC的时钟频率为4.7 MHz,因此我们在每小时触发错误的顶部有大约1/4700的机会。如果您将应用程序交付给50,000名用户,并且每个用户每天使用它8小时,那么通过一点数学计算,您可以计算出在发布的第一周内您可能会收到425个关于此错误的投诉。你老板会很生气的。
请记住,我们回到了20世纪80年代中期,没有互联网,所以你必须给你的50,000名客户每人邮寄一张带有补丁的软盘。在几美元的成本加上邮费,这个错误已经花费了公司约10万美元。相比之下,你在1984年作为入门级程序员的年薪约为20,000美元。你觉得保住工作的机会有多大?

相关问题