目录

  1. 1. 前言
  2. 2. 基础知识
    1. 2.1. 机器语言
    2. 2.2. 汇编语言
    3. 2.3. 存储器
    4. 2.4. 总线
  3. 3. 寄存器
    1. 3.1. 通用寄存器
    2. 3.2. 一些汇编指令
      1. 3.2.1. mov
      2. 3.2.2. add & sub
      3. 3.2.3. 检测点2.1
    3. 3.3. 物理地址
      1. 3.3.1. 检测点2.2
    4. 3.4. 段&段寄存器
      1. 3.4.1. CS和IP
      2. 3.4.2. 修改CS、IP的指令(jmp)
      3. 3.4.3. 代码段
      4. 3.4.4. 检测点2.3
    5. 3.5. Debug程序
  4. 4. 寄存器(内存访问)
    1. 4.1. 内存中字的存储
    2. 4.2. DS和[address]
    3. 4.3. 字的传送
    4. 4.4. mov、add、sub指令
    5. 4.5. 数据段
      1. 4.5.1. 检测点3.1
    6. 4.6. CPU提供的栈机制
    7. 4.7. push、pop指令
    8. 4.8. 栈段
      1. 4.8.1. 检测点3.2
    9. 4.9. 实验二 用机器指令和汇编指令编程
  5. 5. 源程序
    1. 5.1. 实验三 编程、编译、连接、跟踪
  6. 6. [BX]和 loop 指令
    1. 6.1. [BX]
    2. 6.2. loop 指令
    3. 6.3. 段前缀
    4. 6.4. 实验四 [bx]和loop的使用
  7. 7. 包含多个段的程序
    1. 7.1. 实验五 编写、调试具有多个段的程序
  8. 8. 数据处理
    1. 8.1. bx、si、di和bp
    2. 8.2. 数据位置的表达
    3. 8.3. 寻址方式
    4. 8.4. div指令
    5. 8.5. 伪指令dd
    6. 8.6. dup
  9. 9. 转移指令的原理
    1. 9.1. 操作符offset
    2. 9.2. jmp指令
    3. 9.3. jcxz指令
    4. 9.4. loop指令
  10. 10. CALL 和 RET 指令
    1. 10.1. ret和retf
    2. 10.2. call指令
    3. 10.3. mul指令
  11. 11. 标志(flag)寄存器
    1. 11.1. ZF
    2. 11.2. PF
    3. 11.3. SF
    4. 11.4. CF
    5. 11.5. OF
    6. 11.6. adc指令
    7. 11.7. sbb指令
    8. 11.8. cmp指令
    9. 11.9. 检测比较结果的条件转移指令
    10. 11.10. 标志寄存器在 Debug 中的表示
  12. 12. 其它补充
    1. 12.1. EBP
    2. 12.2. ESP
    3. 12.3. 指令指针寄存器
    4. 12.4. 程序状态与控制寄存器
  13. 13. 立即数
  14. 14. 指令
    1. 14.1. db
    2. 14.2. 条件分支指令
  15. 15. 栈帧
  16. 16. 逆向题目
    1. 16.1. [HGAME 2022 week1]easyasm

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

汇编基础

2023/5/5 Basic asm
  |     |   总文章阅读量:

前言

参考:

X86汇编快速入门

《汇编语言(第4版)》王爽


基础知识

汇编语言是直接在硬件之上工作的编程语言,首先要了解硬件系统的结构,才能有效的应用汇编语言对其编程

汇编的研究重点放在如何利用硬件系统的编程结构和指令集有效灵活的控制系统进行工作

机器语言

机器语言是机器指令的集合

机器指令展开来讲就是一台机器可以正确执行的命令,比如这个指令 01010000 (PUSH AX)—— 把 AX 推进堆栈

而机器码只认识 01,所以在很多时候非常不方便,这就产生了汇编语言

汇编语言

汇编指令是机器指令的助记符,同机器指令一一对应

每一种CPU都有自己的汇编指令集

存储器

CPU可以直接使用的信息在存储器中存放

在存储器中指令和数据没有任何区别,都是二进制信息

存储器被划分成若干个存储单元,存储单元从 0 开始顺序编号

image-20240922114430898

一个存储单元可以存储 8 个 bit,即 8 位二进制数

1Byte = 8bit 1KB = 1024B 1MB = 1024KB 1GB = 1024MB

总线

image-20240922114454112

每一个CPU芯片都有许多管脚,引出3种总线:

  • 地址总线的宽度决定了 CPU 的寻址能力:N 根地址线(即宽度为 N)最多可以寻找 2N 个内存单元(即1根就是1个二进制位数)
  • 数据总线的宽度决定了 CPU 与其它器件进行数据传送时的一次数据传送量
  • 控制总线的宽度决定了 CPU 对系统中其它器件的控制能力

寄存器

简单的讲是 CPU 中可以存储数据的器件,一个 CPU 中有多个寄存器

先以 x86 为例,有14个寄存器,都是16位的,可以存放2个字节,分别为:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW

通用寄存器

是用于存储和处理数据的重要组成部分。每个CPU架构都有一组特定的寄存器,这些寄存器在汇编语言中通常用作操作数、临时存储器和地址指针。

AX、BX、CX、DX 通常用来存放一般性数据,被称为通用寄存器

以 AX 为例,来看一下它的逻辑结构:

image-20240922114705656

拿到一个数据,会先把它转换为二进制,再存储

为了保证兼容,这四个通用寄存器都可以分为两个独立的 8 位寄存器使用,例如 AX 可以分为 AH 和 AL,H 就是 High,L 就是 Low

image-20240922114929993

AH 和 AL 中的数据,可以看成是一个字型数据的高8位和低8位,大小是20000;也可以看成是两个独立的字节型数据,大小分别是78和32


一些汇编指令

mov

将数据从一个位置移动到另一个位置

mov destination, source

destination是目标操作数(destination operand),用于存储数据的位置,可以是一个寄存器、一个内存地址或一个立即数

source是源操作数(source operand),用于提供要移动的数据的位置,可以是一个寄存器、一个内存地址或一个立即数

add & sub

add是将两个操作数相加,并将结果存储到一个目标操作数;那么sub就是相减了,从目标操作数中减去源操作数

add destination, source
sub destination, source

mov ax,18	;AX=18 = 0012H
mov ah,78	;AH=78 = 4EH,AX=4E12H
add ax,8	;AX=AX+8
mov ax,bx	;AX=BX
add ax,bx	;AX=AX+BX

注意:对于这样的指令add ax bx,假设 AX = BX = 8226H,相加后所得的值为 1044CH,但是 ax 为16位寄存器,只能存放4位十六进制的数据,所以最高位的1不能在 ax 中保存,最终 ax 中的数据为 044CH


检测点2.1

  1. 写出每条汇编指令执行后相关寄存器中的值。

    指令
    mov ax,62627 AX = F4A3H
    mov ah,31H AX = 31A3H
    mov al,23H AX = 3123H
    add ax,ax AX = 6246H
    mov bx,826CH BX = 826CH
    mov cx,ax CX = 6246H
    mov ax,bx AX = 826CH
    add ax,bx AX = 04D8H
    mov al,bh AX = 0482H
    mov ah,bl AX = 6C82H
    add ah,ah AX = D882H
    add al,6 AX = D888H
    add al,al AX = D810H
    mov ax,cx AX = 6246H

    两个地方要注意:

    一个是add ax,bx这行,AX溢出了,由104D8H变成04D8H

    另一个是add al,al ,88H+88H=110H 也溢出了,变成10H

  2. 最多使用4条指令,编程计算2的4次方

    mov ax,2H
    add ax,ax
    add ax,ax
    add ax,ax

物理地址

x86 是 16 位结构,也就是说能够一次性处理、传输、暂时存储的信息的最大长度是16位

如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出的寻址能力只有64KB = 65536B

但是 x86 采用一种在内部用两个 16 位地址合成的方法来形成一个 20 位的物理地址:

image-20240922120038323

地址加法器采用 物理地址 = 段地址*16 + 偏移地址 的方法用段地址和偏移地址合成物理地址(这里的地址都是按16进制计算的),更一般地说,这种寻址功能是 基础地址 + 偏移地址 = 物理地址

段地址*16在二进制看来相当于 左移4位

image-20240922121743142

  • CPU可以用不同的段地址和偏移地址形成同一个物理地址

  • 如果给定一个段地址,仅通过变化偏移地址来进行寻址,因为偏移地址是16位,变化范围为 0~FFFFH,仅用偏移地址来寻址最多可寻 64KB 个内存单元


检测点2.2

  1. 给定段地址为0001H,仅通过变化偏移地址寻址,CPU的寻址范围为 0010H1000FH

    最小肯定是偏移地址为0,所以段地址左移4位即结果

    最大肯定是偏移地址为FFFFHFFFFH+00010H即结果

  2. 有一数据存放在内存20000H单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则SA应满足的条件是:最小为 1001H ,最大为 2000H

    确定了内存单元找能寻到的段地址SA,最大肯定是偏移地址为0,即2000H

    最小直接算 20000H - FFFFH = 10001H,如果向前取 1000H 的话实际上是 10000H + FFFFH < 20000H

    所以我们向后取1001H,这样不至于让偏移地址为最大FFFFH都无法到20000H


段&段寄存器

因为 8086CPU 用 基础地址+偏移地址=物理地址 的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存,同时也可以用不同的段地址和偏移地址形成同一个物理地址

例:地址 10000H~100FFH 的内存单元组成一个段,该段的起始地址为 10000H ,段地址为 1000H,大小为 100 H

那么我们可以将若干地址连续的内存单元看作一个段,用段地址*16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元

注意:段地址16 必定是16的倍数,所以一个段的起始地址也一定是16的倍数;偏移地址为16位,16位地址的寻址能力为 64KB,所以*一个段的最大长度为 64KB

8086的4个段寄存器:

  • CS:代码段寄存器

  • SS:栈段寄存器

  • DS:数据段寄存器

  • ES:附加数据寄存器


CS和IP

指示了CPU当前要读取指令的地址,CS为代码段寄存器,IP为指针指令寄存器

8086中,任意时刻,CPU将 CS:IP 指向的内容当作指令执行:设CS中的内容为 M,IP中的内容为 N,CPU将从内存 M*16+N 单元开始,读取一条指令并执行,初始CS:IP指向可执行文件的起始地址

读取一条指令后,IP 的值自动增加,以使CPU可以读取下一条指令,增加量取决于当前读入的指令长度(如 B82301 在内存中占 3 个字节,那么 IP 加 3)

总结下工作流程:

  1. 从 CS:IP 指向的内存单元读取指令,读取的指令进入指令缓冲器
  2. IP=IP+所读取指令的长度,从而指向下一条指令
  3. 执行指令。转到步骤1,重复这个过程

先改变IP的值,然后才执行指令

修改CS、IP的指令(jmp)

在CPU中,程序员能够用指令读写的部件只有寄存器,通过改变寄存器中的内容实现对CPU的控制

8086并没有给mov指令设置 CS、IP 的值的功能,能够改变它们内容的指令被统称为转移指令,这里先学习jmp指令

若想同时修改 CS、IP 的内容,可以用jmp 段地址:偏移地址的指令完成

用指令中给出的段地址修改CS,偏移地址修改IP

jmp 2AE3:3	;CS=2AE3H, IP=0003H, CPU从2AE33H处读取指令
jmp 3:0B16	;CS=0003H, IP=0B16H, CPU从00B46H处读取指令

若仅想修改IP的内容,也可以jmp 某一合法寄存器来完成

用寄存器中的值修改IP

jmp ax	;ax=1000H, CS=2000H, IP=0003H 执行后 ax=1000H, CS=2000H, IP=1000H

代码段

将一段内存当作代码段,仅仅是编程中的一种安排,CPU只认被 CS:IP 指向的内存单元中的内容为指令,所以要让CPU执行我们放在代码段中的指令,必须要将 CS:IP 指向所定义的代码段中的第一条指令的首地址

设一段10个字节长的代码存放在 123B0H~123B9H 的内存单元中,那么 CS=123BH,IP=0000H


检测点2.3

下面的3条指令执行后,cpu几次修改IP?都是在什么时候?最后IP中的值是多少?

mov ax,bx 
sub ax,ax 
jmp ax 

一共算4次:

  1. 原本ip指向mov ax,bx这个指令本身,读取完成后ip寄存器中的值立刻改变,指令尚未执行
  2. (上一条指令执行完成)原本ip指向sub ax,ax这个指令本身,读取完成后ip寄存器中的值立刻改变,指令尚未执行
  3. (上一条指令执行完成)原本ip指向jmp ax这个指令本身,读取完成后ip寄存器中的值立刻改变,指令尚未执行
  4. 由于上一条指令尚未执行,jmp 指令把ax寄存器的值赋给了ip,所以ip又改变了一次

Debug程序

本人的DOS环境基于Android termux,部分内存地址会和网上的不一样

命令:https://blog.csdn.net/weixin_43809545/article/details/103640185

R:查看、改变CPU寄存器的内容

D:查看内存中的内容

E:改写内存中的内容

U:将内存中的机器指令翻译成汇编指令

T:执行汇编程序,单步跟踪

A:以汇编指令的格式在内存中写入一条机器指令

G:G [=起始地址] [断点地址],执行汇编指令,意思是从起始地址开始执行到断点地址


寄存器(内存访问)

解决问题:CPU从内存单元中读取数据

内存中字的存储

CPU中,用16位寄存器来存储一个字,高8位存放高位字节,低8位存放存放低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中

image-20241017170047293

字单元:即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成

将起始地址为 N 的字单元简称为 N 地址字单元


DS和[address]

DS寄存器:通常用来存放要访问数据的段地址

mov bx,1000H
mov ds,bx
mov al,[0]	; 将10000H(1000:0)中的数据读到al中

[...]表示一个内存单元,这里的0表示内存单元的偏移地址(因为这里是 al 是8位寄存器,所以[]视为内存单元)

指令执行时,CPU自动取ds中的数据为内存单元的段地址

注意:要将数据送入 ds 寄存器,需要用一个寄存器来中转,不能直接mov ds,1000H


字的传送

mov ax,1000H
mov ds,ax	; 设置ds为1000H
mov ax,[0]	; 1000:0处存放的字型数据送入ax:1000:1存放字型数据的高8位11H,1000:0存放字型数据的低8位23H,则1000:0处存放的存放的字型数据为1123H。指令执行时,高8位送入ah,低8位送入al,则ax中的数据为1123H
mov bx,[2]
mov cx,[1]
add bx,[1]
add cx,[2]

此处 ax、bx、cx 是16位寄存器,所以[]视为字


mov、add、sub指令

mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
add/sub 寄存器,数据
add/sub 寄存器,寄存器
add/sub 寄存器,内存单元
add/sub 内存单元,寄存器

注意:段寄存器不能像通用寄存器那样进行运算,这是硬件限制,mov不涉及运算,只是移动数据


数据段

可以将一组内存单元定义为一个段,是我们在编程时的一种安排

具体操作用 ds 存放数据段的段地址

mov ax,123BH
mov ds,ax	;将 123BH 送入 ds 中,作为数据段的段地址
mov al,0	;用 al 存放累加结果
add al,[0]	;将数据段第一个单元(偏移地址为0)中的数值加到 al 中
add al,[1]	;将数据段第二个单元(偏移地址为1)中的数值加到 al 中
add al,[2]	;将数据段第三个单元(偏移地址为2)中的数值加到 al 中

检测点3.1

  1. 在DEBUG中,用 d 0:0 lf查看内存,结果如下:

    0000:0000 70 80 F0 30 EF 60 30 E2-00 80 80 12 66 20 22 60
    0000:0010 62 26 E6 D6 CC 2E 3C 3B-AB BA 00 00 26 06 66 88

    DS为 1 时,看 0000:0010(因为基础地址+偏移地址=物理地址,所以等价于 0001:0000),[0] 为 2662(注意高低位),[1] 为 E626,[2] 为 D6E6,以此类推

    下面的程序执行前,AX=0,BX=0,写出每条汇编指令执行完后相关寄存器中的值

    指令 结果
    mov ax,1
    mov ds,ax
    mov ax,[0000] ax = 2662H
    mov bx,[0001] bx = E626H
    mov ax,bx ax = E626H
    mov ax,[0000] ax = 2662H
    mov bx,[0002] bx = D6E6H
    add ax,bx ax = FD48H
    add ax,[0004] ax = 2C14H
    mov ax,0 ax = 0000H
    mov al,[0002] ax = E6H
    mov bx,0 bx = 0000H
    mov bl,[000c] bx = 26H
    add al,bl ax = 000CH
  2. 内存中的情况如图

    image-20241217170805432

    各寄存器的初始值:CS=2000h,IP=0,ds=1000h,ax=0,bx=0

    • 写出CPU执行的指令序列(用汇编指令写出)

      mov ax,6622h
      jmp 0ff0:0100
      mov ax,2000h
      mov ds,ax
      mov ax,[0008]
      mov ax,[0002]
    • 写出CPU执行每条指令后,CS、IP和相关寄存器的数值

      指令 cs ip ds ax bx
      初始 2000 0000 1000 0000 0000
      mov ax,6622 2000 0003 1000 6622 0000
      jmp 0ff0:0100 0ff0(1000) 0100(0000) 1000 6622 0000
      mov ax,2000 1000 0003 1000 2000 0000
      mov ds,ax 1000 0005 2000 2000 0000
      mov ax,[0008] 1000 0008 2000 c389 0000
      mov ax,[0002] 1000 000B 2000 ea66 0000

CPU提供的栈机制

8086提供入栈和出栈指令,最基本的两个是 PUSH(入栈)和 POP(出栈)

栈顶的栈地址存放在 SS 中,频移地址存放在 SP 中

任意时刻,SS:SP 指向栈顶元素

push ax	;将寄存器 ax 中的数据送入栈中
;先 SP=SP-2,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
;然后 将ax中的内容送入 SS:SP 指向的内存单元处,SS:SP 此时指向新栈顶

pop ax	;从栈顶取出数据送入 ax
;先 将 SS:SP 指向的内存单元处的数据送入 ax 中
;然后 SP=SP+2,SS:SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶

1730302045293

入栈时,栈顶从高地址向低地址方向增长

注意:栈空时,SS:SP指向栈空间最高地址单元的下一个单元,以上图为例就是 10010H

1730302400982

1000CH 处的 2266H 依然存在,但是已不在栈中,当再次入栈,SS:SP 移至 1000CH,并在里面写入新的数据,2266H 将被覆盖


8086不保证我们对栈的操作不会超界,所以入栈/出栈遇到栈满/栈空的情况会导致超界


push、pop指令

push 寄存器
pop 寄存器
push 段寄存器
pop 段寄存器
push 内存单元
pop 内存单元

栈段

分析:将 10000H~1FFFFH 这段空间当作栈段,初始状态栈是空的,此时,SS=1000H

栈空间为64KB,栈最底部的字单元地址为 1000:FFFE

而任意时刻,SS:SP 指向栈顶单元,当栈中只有一个元素的时候,SS=1000H,SP=FFFEH。

栈为空,就相当于栈中唯一的元素出栈,SP=SP+2,则 SP = FFFEH + 2 = 0

所以,栈为空的时候,SS=1000H,SP=0


那么,由于push、pop只修改 SP,栈顶的变化范围为 0~FFFFH,栈空SP=0,栈满SP=0,再次压栈则会覆盖原来栈中的内容,所以一个栈段的容量最大为64KB


检测点3.2

  1. 将10000H-1000FH中的8个字,逆序拷贝到20000H-2000FH中

    mov ax,1000H
    mov ds,ax
    
    mov ax,2000H 
    mov ss,ax    
    mov sp,10h   
    
    
    push [0]
    push [2]
    push [4]
    push [6]
    push [8]
    push [A]
    push [C]
    push [E]

    设置栈空间,设置sssp寄存器的值

    因为栈如果有一个元素,那么栈顶是2000EH,所以,2000EH+2就是20010H,去掉 ss 就是0010H

  2. 将10000H-1000FH中的8个字,逆序复制到20000H-2000FH中

    mov ax,2000H
    mov ds,ax
    
    mov ax,1000H
    mov ss,ax   
    mov sp,0    
    
    
    pop [E]
    pop [C]
    pop [A]
    pop [8]
    pop [6]
    pop [4]
    pop [2]
    pop [0]

实验二 用机器指令和汇编指令编程

mov ax,ffff
mov ds,ax

mov ax,2200
mov ss,ax

mov sp,0100

mov ax,[0]        ;AX = C0EAH         DS:0 = C0EAH
add ax,[2]         ;AX = C0FCH         DS:2 = 0012H    C0EAH + 0012H = C0FCH
mov bx,[4]        ;BX = 30F0H          DS:4 = 30F0H
add bx,[6]         ;BX = 6021H          DS:6 = 2F31H    30F0H + 2F31H = 6021H

push ax             ;SP = 0100H        修改的内存单元的地址是 220FEH 内容为 C0FCH
push bx             ;SP = 00FEH        修改的内存单元的地址是 220FCH 内容为 6021H
pop ax               ;SP = 0100H        ax= 6021H 
pop bx               ;SP = 0100H        bx= C0FCH 

push [4]             ;SP = 0100H        修改的内存单元的地址是 220FEH 内容为 30F0H
push [6] 
mov ax,[0]        ;AX = C0EAH         DS:0 = C0EAH
add ax,[2]         ;AX = C0FCH         DS:2 = 0012H    C0EAH + 0012H = C0FCH
mov bx,[4]        ;BX = 30F0H          DS:4 = 30F0H
add bx,[6]         ;BX = 6021H          DS:6 = 2F31H    30F0H + 2F31H = 6021H

img

push ax             ;SP = 0100H        修改的内存单元的地址是 22FEH 内容为 C0FCH
push bx             ;SP = 00FEH        修改的内存单元的地址是 22FCH 内容为 6021H
pop ax               ;SP = 00FCH        ax= 6021H 
pop bx               ;SP = 0100H        bx= C0FCH 

push [4]             ;SP = 0100H        修改的内存单元的地址是 220FEH 内容为 30F0H

img


源程序

assume cs:codesg	;将段寄存器和某一个具体的段相联系
codesg segment	;定义一个段,段的名称为"codesg",这个段从此开始

mov ax,0123h
mov bx,0456h
add ax,bx
add ax,ax

mov ax,4c00h
int 21h	;这两条指令实现程序返回

codesg ends	;名称为"codesg"的段到此结束
end	;标记整个程序的结束

汇编语言源程序中,包含汇编指令伪指令,前者会被编译成机器指令被CPU所执行;后者不被CPU所执行,由编译器来执行

伪指令:assume,段名 ends,end


使用 masm 编译,link 链接


实验三 编程、编译、连接、跟踪

  1. 将下面的程序保存为 t1.asm 文件,生成可执行文件 t1.exe

    assume cs:codesg
    codesg segment
    	mov ax,2000h
    	mov ss,ax
    	mov sp,10
    	pop ax
    	pop bx
    	push ax
    	push bx
    	pop ax
    	pop bx
    	mov ax,4c00h
    	int 21h
    codesg ends
    end
  2. Debug追踪执行过程

    img

    mov sp,10设置了栈顶指针指向 2000:A,初始 SP 为 000A

    img

    每次 pop 都使 SP+2,每次 push 都使 SP-2

    第一次 pop ax 将 SS:SP 指向的内存单元(这里内存单元的数据为 0000)送入 ax,于是 ax 变为 0000

    同理第一次 pop bx 也使 bx 变为 0000

    而第一次 push ax 则是把 ax 中的数据送入 2000:000C (先 SP-2 再送入)

    img

  3. 查看 PSP (程序段前缀)的内容

    程序加载后,ds中存放着内存区的段地址,这个内存区的偏移地址为0,即程序所在的内存区的地址是 ds:0,在这个内存中的前256 个字节中存放的是程序。故查看 PSP 中的内容只要输入ds:0 即 075C:0 即可

    img


[BX]和 loop 指令

定义描述性符号( )来表示一个寄存器或者内存单元的内容,如(ax)(ds)(20000H)

其中的元素可以有三种类型:寄存器名,段寄存器名,内存单元的物理地址


约定符号 idata 表示常量,即 mov ax,[idata] 就代表 mov ax,[1]mov ax,[2]

[BX]

同样表示内存单元

mov ax,[bx]

设 bx 中存放的数据作为一个偏移地址 EA,段地址 SA 默认在 ds 中,将 SA:EA 处的数据送入 ax 中

即:(ax)=((ds)*16+(bx))

mov [bx],ax

将 ax 中的数据送入内存 SA:EA 中

loop 指令

loop 标号

CPU 执行 loop 指令的时候,要进行两步操作:

  1. (cx) = (cx)-1
  2. 判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行

所以我们可以用 loop 实现循环功能,cx 中存放循环次数

编程计算 2^12,结果存在 ax 中

assume cs:code
code segment
mov ax,2

mov cx,11
s: add ax,ax
loop s

mov ax,4c00h
int 21h
code ends
end

总结:

  • 在 cx 中存放循环次数
  • loop 指令中的标号所标识地址要在前面
  • 要循环执行的程序段,要写在标号和 loop 指令的中间
mov cx,循环次数
s: 循环执行的程序段
loop s

段前缀

出现在访问内存单元的指令中,用于显式地给出内存单元的段地址的ds:cs:ss:es:,在汇编语言中称为段前缀

段前缀的使用:

考虑一个问题,将内存 ffff:0ffff:b 单元中的数据复制到 0:2000:20b 单元中

  1. 0:2000:20b 单元等同于 0020:00020:b 单元,它们描述的是同一段内存空间
  2. 复制的过程应用循环实现
  3. 在循环中,源始单元 ffff:X 和目标单元 0020:X 的偏移地址 X 是变量,使用 bx 来存放
  4. 用 0020:0~0020:b 描述方便使目标单元的偏移地址和源始单元的偏移地址从同一数值 0 开始

可以使用两个段寄存器分别存放源始单元 ffff:X 和目标单元 0020:X 的段地址来省略循环中需要重复做12次的设置ds程序段

assume cs:code
code segment
mov ax,0ffffh
mov ds,ax	;(ds)=0ffffh

mov ax,0020h
mov es,ax	;(es)=0020h

mov bx,0	;(bx)=0,此时 ds:bx 指向ffff:0,es:bx 指向 0020:0

mov cx,12
s:
mov dl,[bx]	;(dl)=((ds)*16+(bx)),将 ffff:bx 中的数据送入 dl
mov es:[bx],dl	;((es)*16+(bx))=(dl),将 dl 中的数据送入 0020:bx
inc bx	;(bx)=(bx)+1
loop s

mov ax,4c00h
int 21h
code ends
end

实验四 [bx]和loop的使用

  1. 编程:向内存 0:200H~0:23fH 依次传送数据 0~63(3FH)

    设置 ds 为 0020h,然后 ip 循环 3fh 即63次,且 bx 低8位恰好是我们要传送的数据

    assume cs:code
    code segment
    mov ax,0020h
    mov ds,ax
    
    mov cx,64
    mov bx,0
    s:
    mov ds:[bx],bl
    inc bx
    loop s
    
    mov ax,4c00h
    int 21h
    code ends
    end

    编译,Debug 启动程序,t命令持续执行,p命令跳过循环

    最后用d命令查看0:200处的内存

    img

  2. 下面程序的功能是将mov ax,4c00h之前的指令复制到内存 0:200 处

    那么就要先获取起始指令的地址

    assume cs:code
    code segment
    mov ax,cs
    mov ds,ax
    mov ax,0020h
    mov es,ax
    mov bx,0
    
    mov cx,17h
    s:
    mov al,[bx]
    mov es:[bx],al
    inc bx
    loop s
    
    mov ax,4c00h
    int 21h
    code ends
    end

    编译,debug执行

    img

    可以发现偏移地址从0000H~0017H存储了程序的执行代码。与程序执行代码存储的内存单元比较,发现是一样的

    那么复制的就是这一块代码,从cs:0复制到了0:200


包含多个段的程序

assume cs:code
code segment
	dw 0123H,0456H,0789H,0abch,0defh,0fedh,0cbah,0987H
	
	mov bx.0
	mov ax,0
	
	mov cx,8
	s:
	add ax,cs:[bx]
	add bx,2
	loop s

mov ax,4c00h
int 21h
code ends
end start

dw(define word),这里定义了8个字型数据,所占的内存空间大小为16个字节

程序在运行的时候 CS 中存放代码段的段地址,所以可以从 CS 中得到它们的段地址,用 dw 定义的数据处于代码段的最开始,所以偏移地址为0

实验五 编写、调试具有多个段的程序

  1. 编写下述程序并运行,然后用 Debug 加载、跟踪

    assume cs:code,ds:data,ss:stack
    data segment	;数据段
    	dw 0123H,0456H,0789H,0abch,0defh,0fedh,0cbah,0987H
    data ends
    stack segment	;栈段
    	dw 0,0,0,0,0,0,0,0
    stack ends
    code segment	;代码段
    start:	mov ax,stack
    		mov ss,ax
    		mov sp,16		
    		mov ax,data
    		mov ds,ax
    		push ds:[0]		
    		push ds:[2]		
    		pop ds:[2]		
    		pop ds:[0]
    		mov ax,4c00h
    		int 21h
    code ends
    end start

    img

    CPU 执行程序到 076E:001D,程序返回前,data 段中的数据不变

    img

    CPU 执行程序到 076E:001D,程序返回前,cs= 076E、ss= 076D、ds= 076C

    img

    设程序加载后,code 段的段地址为 X,则 data 段的段地址为 X-2,stack 段的段地址为 X-1

  2. 编写下述程序并运行,然后用 Debug 加载、跟踪

    assume cs:code,ds:data,ss:stack
    data segment		
    	dw 0123H,0456H
    data ends
    stack segment		
    	dw 0,0
    stack ends
    code segment		
    start:	mov ax,stack
    		mov ss,ax
    		mov sp,16		
    		mov ax,data
    		mov ds,ax
    		push ds:[0]	
    		push ds:[2]	
    		pop ds:[2]		
    		pop ds[0]		
    		mov ax,4c20h
    		int 21h
    code ends
    end start
  3. 编写下述程序并运行,然后用 Debug 加载、跟踪

    assume cs:code,ds:data,ss:stack
    code segment	
    start:	mov ax,stack
    		mov ss,ax
    		mov sp,16		
    		mov ax,data
    		mov ds,ax
    		push ds:[0]		
    		push ds:[2]		
    		pop ds:[2]		
    		pop ds:[0]		
    		mov ax,4c00h	
    		int 21h
    code ends
    data segment	
    	dw 0123H,0456H
    data ends
    stack segment	
    	dw 0,0
    stack ends
    end start
  4. 编写 code 段中的代码,将 a 段和 b 段中的数据依次相加,将结果存到 c 段中

    assume cs:code
    a segment		
    	db 1,2,3,4,5,6,7,8	
    a ends
    b segment		
    	db 1,2,3,4,5,6,7,8
    b ends
    c segment		
    	db 0,0,0,0,0,0,0,0
    c ends
    code segment	
    start:	
    	mov ax,a
    	mov ds,ax
    	mov ax,c
    	mov es,ax
    	mov cx,8
    	mov bx,0
    s1:	mov al,ds:[bx]
    	mov es:[bx],al
    	inc bx			
    	loop s1	
    	mov ax,b	
    	mov ds,ax	
    	mov cx,8	
    	mov bx,0	
    s2:	mov al,ds:[bx]
    	add es:[bx],al
    	inc bx			
    	loop s2		
    	mov ax,4c00h
    	int 21h		
    code ends
    end start
  5. 编写 code 段中的代码,用 push 指令将 a 段中的 word 数据,逆序存储到 b 段中

    参考 检测点3.2 里的做法,利用依次入栈的思路实现逆序存储

    assume cs:code
    a segment
    	dw 1,2,3,4,5,6,7,8
    a ends
    b segment
    	dw 0,0,0,0,0,0,0,0
    b ends
    code segment
    start:	
    	mov ax,a
    	mov ds,ax	
    	mov ax,b
    	mov ss,ax
    	mov sp,16
    	
    	mov cx,8
    	mov bx,0
    s:	push ds:[bx]
    	add bx,2
    	loop s
    	
    	mov ax,4c00h
    	int 21h
    code ends
    end start

数据处理

两个基本问题:

处理的数据在什么地方?

要处理的数据有多长?

接下来使用 reg 表示一个寄存器:ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di

sreg 表示一个段寄存器:ds、ss、cs、es

bx、si、di和bp

  • 在 8086 中,只有这4个寄存器可以用在[...]中来进行内存单元的寻址

    mov ax,[bx]
    mov ax,[bx+si]
    mov ax,[bx+di]
    mov ax,[bp]
    mov ax,[bp+si]
    mov ax,[bp+di]
  • [...]中,这4个寄存器可以单个出现,或只能以4种组合出现:bx 和 si、bx 和 di、bp 和 si、bp 和 di

    下面的指令是错误的:

    mov ax,[bx+bp]
    mov ax,[si+di]
  • 只要在[...]中使用寄存器 bp,而指令中没有显性地给出段地址,段地址就默认在 ss 中

    mov ax,[bp]	;(ax)=((ss)*16 + (bp))
    mov ax,[bp+idata]	;(ax)=((ss)*16 + (bp) + idata)
    mov ax,[bp+si]	;(ax)=((ss)*16 + (bp) + (si))
    mov ax,[bp+si+idata]	;(ax)=((ss)*16 + (bp) + (si) + idata)

数据位置的表达

  • 立即数(idata)

  • 寄存器

  • 段地址(SA)和偏移地址(EA)


寻址方式

寻址方式 含义 名称 常用格式
[idata] 直接寻址 [idata]
[bx] 寄存器间接寻址 [bx]
[si]
[di]
[bp]
[bx+idata] 寄存器相对寻址
[si+idata]
[di+idata]
[bp+idata]
[bx+si] 基址变址寻址
[bx+di]
[bp+si]
[bp+di]
[bx+si+idata] 相对基址变址寻址
[bx+di+idata]
[bp+si+idata]
[bp+di+idata]

div指令

除法指令

  • 除数:有 8 位和 16 位两种,在一个 reg 或内存单元中
  • 被除数:默认放在 AX 或 DX和AX 中,如果除数为 8 位,被除数则为 16 位,默认在 AX 中存放;如果除数为 16 位,被除数则为 32 位,在 DX 和 AX 中存放,DX 存放高 16 位,AX 存放低 16 位
  • 结果:如果除数为 8 位,则 AL 存储除法操作的商,AH 存储除法操作的余数;如果除数为 16 位,则 AX 存储除法操作的商,DX 存储除法操作的余数

编程,利用除法指令计算 100001/100:

被除数 100001 大于 65535,不能用 ax 寄存器存放,只能用 dx 和 ax 两个寄存器联合存放 100001,也就是说要进行 16 位的除法

除数 100 小于 255,可以在一个 8 位寄存器中存放,但是因为被除数是 32 位的,除数应为 16 位,所以要用一个 16 位寄存器来存放存放除数 100

mov dx,1
mov ax,86A1H	;(dx)*10000H+(ax)=100001
mov bx,100
div bx

伪指令dd

dd 是用来定义 dword (double word,双字)型数据的

data segment
db 1	;01H,在data:0处,占1个字节
dw 1	;0001H,在data:1处,占1个字
dd 1	;00000001H,在data:3处,占2个字
data ends

dup

操作符,用来处理数据的重复

db 3 dup (0)

定义了3个字节,它们的值都是0,相当于 db 0,0,0


转移指令的原理

操作符offset

jmp指令

jcxz指令

loop指令


CALL 和 RET 指令

都是转移指令,都修改IP,或同时修改 CS 和 IP

ret和retf

ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移:

  1. (IP) = ((ss)*16+(sp))
  2. (sp) = (sp)+2
pop IP

retf 指令用栈中的数据,修改 CS 和 IP 的内容,从而实现远转移

  1. (IP) = ((ss)*16+(sp))
  2. (sp) = (sp)+2
  3. (CS) = ((ss)*16+(sp))
  4. (sp) = (sp)+2
pop IP
pop CS

call指令

进行两步操作:

  1. 将当前的 IP 或 CS和IP 压入栈中
  2. 转移

mul指令

乘法指令

  • 两个相乘的数:要么都是 8 位,要么都是 16 位。如果是 8 位,一个默认放在 AL 中,另一个放在 8 位 reg 或内存字节单元中;如果是 16 位,一个默认在 AX 中,另一个放在 16 位 reg 或内存字单元中
  • 结果:8 位乘法,结果默认放在 AX 中;16 位乘法,结果高位默认在 DX 中存放,低位在 AX 中放
mul reg
mul 内存单元

计算 100*10,两个数均小于255,做 8 位乘法

mov al,100
mov bl,10
mul bl

结果:(ax) = 1000(03E8H)


标志(flag)寄存器

3种作用

  • 用来存储相关指令的某些执行结果
  • 用来为CPU执行相关指令提供行为依据
  • 用来控制CPU的相关工作方式

image-20241219010411176

ZF

零标志位,它记录相关指令执行后,其结果是否为0

如果结果为0,那么 zf=1;如果结果不为0,那么 zf=0

mov ax,1
sub ax,1

执行后,结果为0,则 zf=1

PF

奇偶标志位,它记录相关指令执行后,其结果的所有 bit 位中 1 的个数是否位偶数

如果 1 的个数位偶数,pf=1;如果为奇数,pf=0

mov al,1
add al,10

执行后,结果为 00001011B(二进制),其中有 3 个 1,则 pf=0

SF

符号标志位,它记录相关指令执行后,其结果是否为负

如果结果为负,sf=1;如果非负,sf=0

计算机中常用补码来表示有符号数据。计算机可以将它看作是有符号数,也可以看成是无符号数:

00000001B,可以看作无符号数 1,或有符号数 +1

10000001B,可以看作无符号数 129,或有符号数 -127


CPU在执行 add 等指令时,必然要影响到 SF 标志位的值,至于我们需不需要这种影响,那就看我们如何看待指令所进行的运算了

mov al,10000001B
add al,1

执行后,结果为 10000010B,sf=1,表示指令进行的是有符号数运算,那么结果为负

CF

进位标志位,一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值

OF

溢出标志位,一般情况下,OF 记录了有符号数运算的结果是否发生了溢出

如果发生溢出,OF=1;如果没有,OF=0

adc指令

带进位加法指令

sbb指令

带借位减法指令

cmp指令

比较指令,功能相当于减法指令,只是不保存结果。cmp 指令执行后,将对标志寄存器产生影响

mov ax,8
mov bx,3
cmp ax,bx

执行后:(ax)=8,zf=0,pf=1,sf=0,cf=0,of=0

  • 如果 (ax)=(bx),则 (ax)-(bx)=0,所以:zf=1;不等于0则zf=0
  • 如果 (ax)<(bx),则 (ax)-(bx)将产生借位,所以:cf=1

检测比较结果的条件转移指令

指令 含义 检测的相关标志位
je 等于则转移 zf=1
jne 不等于则转移 zf=0
jb 低于则转移 cf=1
jnb 不低于则转移 cf=0
ja 高于则转移 cf=0 且 zf=0
jna 不高于则转移 cf=1 或 zf=1

标志寄存器在 Debug 中的表示

输入r命令查看寄存器时的右下角那一排

标志 值为1的标记 值为0的标记
of OV NV
sf NG PL
zf ZR NZ
pf PE PO
cf CY NC
df DN UP

其它补充

image-20230505213645550

EAX:(针对操作数和结果数据的)累加器

EBX:(DS段的数据指针)基址寄存器

ECX:(字符串和循环操作的)计数器

EDX:(I/O指针)数据寄存器

ESI:(字符串操作源指针)源变址寄存器

EDI:(字符串操作目标指针)目的变址寄存器

ESP:(SS段中栈指针)栈指针寄存器 [指向栈顶]

EBP:(SS段中栈内数据指针)扩展基址指针寄存器 [栈帧寄存器、栈底指针寄存器]

应用寄存器时,其名称大小写是不敏感的

EBP

通常用作基址指针,基址指针是一个寄存器,用于指向当前函数的栈帧(stack frame)的基地址,以便在函数内部访问函数的局部变量、函数参数和返回地址等信息。

对应上面的 SS

通常,EBP相对于ESP的偏移量可以用来访问栈帧中的局部变量函数参数

例如,

偏移量为 -4 表示相对于EBP向左偏移4个字节,可以访问EBP指向的位置的前4个字节,通常用来访问函数的第一个参数

偏移量为 8 表示相对于EBP向右偏移8个字节,可以访问EBP指向的位置的后8个字节,通常用来访问函数的第一个局部变量

ESP

通常用作堆栈指针(Stack Pointer)。堆栈指针是一个寄存器,用于指向当前栈顶的地址,以便在函数调用和返回时动态地调整堆栈的大小和位置。

对应上面的 SP

在汇编语言中,使用ESP相对于EBP的偏移量来访问栈帧中的局部变量函数参数

例如,偏移量为-4表示相对于ESP向上偏移4个字节,可以访问栈顶位置的前4个字节,通常用来访问函数的返回地址

偏移量为8表示相对于ESP向下偏移8个字节,可以访问栈顶位置的后8个字节,通常用来访问函数的第一个局部变量


指令指针寄存器

EIP / RIP:保存CPU要执行的指令地址

image-20240311200516116

如图,RIP所保存的地址 401550 即接下来要执行的位置,此时向下运行则会变成 401551 main+1


程序状态与控制寄存器

  • EFLAGS:标志寄存器,32个位元的01控制
  • ZF(零标志器,运算结果为0时置1)
  • CF(进位标志,运算结果向最高位以上进位时置1)
  • OF(溢出标志)
  • AF(辅助进位标志,运算结果在第3位的时候置1)
  • SF(符号标志,有符号整型的符号位为1时置1)

立即数

指令中直接包含的、不需要从内存或寄存器中获取的常数

通常用于立即寻址(immediate addressing)方式,即直接将一个常数值作为操作数传递给指令


指令

基本格式:操作码 目的操作数 源操作数

cmp rax,19h

  1. PUSH/POP:压栈 / 出栈。PUSH 指令将数据压入堆栈,POP 指令将数据从堆栈中弹出

  2. CMP:比较两个值并设置标志位,以便后续的条件分支指令可以使用

    例如,CMP AX, BX:比较 AX 和 BX 的值

  3. MOV/CALL/RET/LEA/INT/EMD:传送 / 调用 / 返回 / 加载 / 中断 / 结束

  4. 运算指令:

    ADD/SUB/SHL/SHR/ROL/ROR:加 / 减 / 逻辑左移 / 逻辑右移 / 循环左移 / 循环右移

    INC/DEC:加一 / 减一

    AND/XOR/OR/NOT:与 / 异或 / 或 / 取反

    MUL/IMUL:无符号乘法、整数乘法

    DIV/IDIV:无符号除法、整数除法

  5. 跳转指令:

    JMP:无条件跳转到指定的地址。例如,JMP LABEL 将程序的控制权转移到标记为 LABEL 的位置

    JE/JNE:根据先前的 CMP 指令设置的标志位,执行条件跳转。JE 指令在相等时跳转,JNE 指令在不相等时跳转

  6. LOOP:根据计数器的值执行循环。LOOP 指令将计数器减少 1,然后跳转到指定的标记处,直到计数器为零。

db

表示定义一个字节(byte)类型的数据

反汇编出来的内容有db的即为字节码


条件分支指令

实现if语句功能

  1. JZ(Jump If Zero):如果零标志位(ZF)被设置,则跳转到指定的标签。
  2. JNZ(Jump If Not Zero):如果零标志位(ZF)未被设置,则跳转到指定的标签。
  3. JC(Jump If Carry):如果进位标志位(CF)被设置,则跳转到指定的标签。
  4. JNC(Jump If Not Carry):如果进位标志位(CF)未被设置,则跳转到指定的标签。

例:

MOV AX, 5 ; 将5赋值给AX寄存器
CMP AX, 0 ; 比较AX寄存器和0
JG greater_than_zero ; 如果AX大于0,跳转到greater_than_zero标签
JMP end ; 否则跳转到end标签

greater_than_zero:
    ; 如果执行到这里,说明AX大于0
    ; 在这里写需要执行的代码
    JMP end

end:
    ; 程序结束

栈帧

PUSH EBP          ;函数开始
MOV  EBP,ESP      ;将栈顶地址存入EBP中

....              ;函数执行,期间EBP地址不变

MOV  ESP,EBP      ;基准点地址给到ESP
POP EBP           ;栈状态恢复,弹出EBP
RETN              ;函

逆向题目

[HGAME 2022 week1]easyasm

16位DOS,只能看汇编,那就先看 Flow Chart

image-20250312002549557

开头挂载两个数据 dseg 和 seg001

dseg:0000 68 67 61 6D 65 7B 46 69 6C 6C+aHgameFillInYou db 'hgame{Fill_in_your_flag}',0

seg001:0000                               seg001 segment byte public 'UNK' use16
seg001:0000                               assume cs:seg001
seg001:0000                               assume es:nothing, ss:nothing, ds:dseg, fs:nothing, gs:nothing
seg001:0000 91                            db  91h
seg001:0001 61                            db  61h ; a
seg001:0002 01                            db    1
seg001:0003 C1                            db 0C1h
seg001:0004 41                            db  41h ; A
seg001:0005 A0                            db 0A0h
seg001:0006 60                            db  60h ; `
seg001:0007 41                            db  41h ; A
seg001:0008 D1                            db 0D1h
seg001:0009 21                            db  21h ; !
seg001:000A 14                            db  14h
seg001:000B C1                            db 0C1h
seg001:000C 41                            db  41h ; A
seg001:000D E2                            db 0E2h
seg001:000E 50                            db  50h ; P
seg001:000F E1                            db 0E1h
seg001:0010 E2                            db 0E2h
seg001:0011 54                            db  54h ; T
seg001:0012 20                            db  20h
seg001:0013 C1                            db 0C1h
seg001:0014 E2                            db 0E2h
seg001:0015 60                            db  60h ; `
seg001:0016 14                            db  14h
seg001:0017 30                            db  30h ; 0
seg001:0018 D1                            db 0D1h
seg001:0019 51                            db  51h ; Q
seg001:001A C0                            db 0C0h
seg001:001B 17                            db  17h
seg001:001C 00                            db    0
seg001:001D 00                            db    0
seg001:001E 00                            db    0
seg001:001F 00                            db    0
seg001:001F                               seg001 ends

很明显 seg001 就是密文

接下来分析逻辑

image-20250312003105029

第一次循环的 cmp 判断明显是向左走

xor     ax, ax ; 清空ax寄存器
mov     al, [si]
shl     al, 1
shl     al, 1
shl     al, 1
shl     al, 1 ; 左移4位
push    ax
xor     ax, ax
mov     al, [si]
shr     al, 1
shr     al, 1
shr     al, 1
shr     al, 1 ; 右移4位
pop     bx
add     ax, bx
xor     ax, 23
add     si, 1
cmp     al, es:[si-1]
jz      short loc_100DD

mov al, [si] 读取当前位置的字符到 ax,注意这里是 al 寄存器也就是 ax 的低8位,需要与 0xff 进行与运算才能取低8位;

然后 shl 左移4位,得到的结果压栈,shr 右移4位,把先前得到的结果出栈并与右移的结果相加,得到高4位与低4位交换的结果;

最后 ax 与 23 异或

设字符为 0xab,则左移4位(相当于乘16)得到 0xb,右移4位(相当于除以16)得到 0x0a,相加得到 0xba

那么构造逆向解密脚本:

import re
from Crypto.Util.number import *

asm = """
seg001:0000 91                            db  91h
seg001:0001 61                            db  61h ; a
seg001:0002 01                            db    1
seg001:0003 C1                            db 0C1h
seg001:0004 41                            db  41h ; A
seg001:0005 A0                            db 0A0h
seg001:0006 60                            db  60h ; `
seg001:0007 41                            db  41h ; A
seg001:0008 D1                            db 0D1h
seg001:0009 21                            db  21h ; !
seg001:000A 14                            db  14h
seg001:000B C1                            db 0C1h
seg001:000C 41                            db  41h ; A
seg001:000D E2                            db 0E2h
seg001:000E 50                            db  50h ; P
seg001:000F E1                            db 0E1h
seg001:0010 E2                            db 0E2h
seg001:0011 54                            db  54h ; T
seg001:0012 20                            db  20h
seg001:0013 C1                            db 0C1h
seg001:0014 E2                            db 0E2h
seg001:0015 60                            db  60h ; `
seg001:0016 14                            db  14h
seg001:0017 30                            db  30h ; 0
seg001:0018 D1                            db 0D1h
seg001:0019 51                            db  51h ; Q
seg001:001A C0                            db 0C0h
seg001:001B 17                            db  17h
"""
enc = re.findall("[0-9A-Fa-f]{2}h", asm)
hex_enc = [f"0x{i.strip('h')}" for i in enc]
flag = b""
for i in hex_enc:
    i = int(i, 16)
    tmp = i ^ 23
    flag += long_to_bytes((tmp << 4) + (tmp >> 4) & 0xff)
print(flag)