3

我正在学习用 x86 汇编(目前是 32 位)编写代码,我正在努力完全理解内存模型。特别令人困惑的是标签的语义、LEA 指令和可执行文件的布局。我编写了这个示例程序,以便我可以检查它在 gdb 中的运行情况。

; mem.s
SECTION .data
    msg: db "labeled string\n"
    db "unlabeled-string\n"
    nls: db 10,10,10,10,10,10,10,10
SECTION .text
global  _start
_start:
    ; inspect msg label, LEA instruction
    mov eax, msg
    mov eax, &msg
    mov eax, [msg]
    ; lea eax, msg (invalid instruction)
    lea eax, &msg
    lea eax, [msg]

    ; populate array in BSS section
    mov [arr], DWORD 1
    mov [arr+4], DWORD 2
    mov [arr+8], DWORD 3
    mov [arr+12], DWORD 4

    ; trying to print the unlabeled string
    mov eax, 4
    mov ebx, 1
    lea ecx, [msg+15]
    int 80H

    mov eax, 1      ; exit syscall
    mov ebx, 0      ; return value
    int 80H
SECTION .bss
    arr: resw 16

我已经组装并链接到:

nasm -f elf -g -F stabs mem.s
ld -m elf_i386 -o mem mem.o

GDB 会话:

(gdb) disas *_start
Dump of assembler code for function _start:
   0x08048080 <+0>: mov    $0x80490e4,%eax
   0x08048085 <+5>: mov    0x80490e4,%eax
   0x0804808a <+10>:    mov    0x80490e4,%eax
   0x0804808f <+15>:    lea    0x80490e4,%eax
   0x08048095 <+21>:    lea    0x80490e4,%eax
   0x0804809b <+27>:    movl   $0x1,0x8049110
   0x080480a5 <+37>:    movl   $0x2,0x8049114
   0x080480af <+47>:    movl   $0x3,0x8049118
   0x080480b9 <+57>:    movl   $0x4,0x804911c
   0x080480c3 <+67>:    mov    $0x4,%eax
   0x080480c8 <+72>:    mov    $0x1,%ebx
   0x080480cd <+77>:    lea    0x80490f3,%ecx
   0x080480d3 <+83>:    int    $0x80
   0x080480d5 <+85>:    mov    $0x1,%eax
   0x080480da <+90>:    mov    $0x0,%ebx
   0x080480df <+95>:    int    $0x80

检查“味精”值:

(gdb) b _start
Breakpoint 1 at 0x8048080
(gdb) run
Starting program: /home/jh/workspace/x86/mem_addr/mem
(gdb) p msg
# what does this value represent?
$1 = 1700946284
(gdb) p &msg
$2 = (<data variable, no debug info> *) 0x80490e4
# this is the address where "labeled string" is stored
(gdb) p *0x80490e4
# same value as above (eg: p msg)
$3 = 1700946284
(gdb) p *msg
Cannot access memory at address 0x6562616c
# NOTE: 0x6562616c is ASCII values of 'e','b','a','l'
# the first 4 bytes from msg: db "labeled string"... little-endian
(gdb) x msg
0x6562616c: Cannot access memory at address 0x6562616c
(gdb) x &msg
0x80490e4 <msg>:    0x6562616c
(gdb) x *msg
Cannot access memory at address 0x6562616c

一次单步执行一条指令:

(gdb) p $eax
$4 = 0
(gdb) stepi
0x08048085 in _start ()
(gdb) p $eax
$5 = 134516964
0x0804808a in _start ()
(gdb) p $eax
$6 = 1700946284
(gdb) stepi
0x0804808f in _start ()
(gdb) p $eax
$7 = 1700946284
(gdb) stepi
0x08048095 in _start ()
(gdb) p $eax
$8 = 134516964

该数组按预期填充了值 1,2,3,4:

# before program execution:
(gdb) x/16w &arr
0x8049104 <arr>:    0   0   0   0
0x8049114:  0   0   0   0
0x8049124:  0   0   0   0
0x8049134:  0   0   0   0
# after program execution
(gdb) x/16w &arr
0x8049104 <arr>:    1   2   3   4
0x8049114:  0   0   0   0
0x8049124:  0   0   0   0
0x8049134:  0   0   0   0

我不明白为什么在 gdb 中打印标签会导致这两个值。另外,如何打印未标记的字符串。提前致谢

4

2 回答 2

5

它有点令人困惑,因为 gdb 不理解标签的概念,实际上 - 它旨在调试用高级语言(通常是 C 或 C++)编写并由编译器编译的程序。因此,它尝试将它在二进制文件中看到的内容映射到高级语言概念——变量和类型——基于它对正在发生的事情的最佳猜测(在没有来自编译器的调试信息告诉它什么是继续)。

nasm 是做什么的

对于汇编器来说,标签是尚未设置的值——它实际上是在链接器运行时获得其最终值。通常,标签用于引用内存部分中的地址——实际地址将在链接器布置最终可执行映像时定义。汇编器生成重定位记录,以便链接器可以正确设置标签的使用。

所以当汇编器看到

mov eax, msg

它知道这msg是与数据段中的地址相对应的标签,因此它生成一条指令将该地址加载到eax中。当它看到

mov eax, [msg]

它会生成一条指令,从地址为 的内存中加载 32 位(寄存器 eax 的大小)msg。在这两种情况下,都会生成一个重定位,以便链接器可以插入最终地址msg

(除此之外——我不知道&nasm 是什么意思——它没有出现在我能看到的文档中的任何地方,所以我很惊讶它没有给出错误。但它看起来像是把它当作别名对于[])

现在 LEA 是一条有趣的指令——它与从内存中移动的格式基本相同,但它不是读取内存,而是将它本来要读取的地址存储到目标寄存器中。所以

lea eax, msg

没有意义——源是标签 (address) msg,它是一个(链接时间)常数,不在内存中的任何地方。

lea eax, [msg]

有效,因为源在内存中,所以它将源的地址粘贴到 eax 中。这与 的效果相同mov eax, msg。最常见的是,您只会看到lea用于更复杂的寻址模式,因此您可以利用 x86 AGU 来做一些有用的工作,而不仅仅是计算地址。例如:

lea eax, [ebx+4*ecx+32]

它在 AGU 中进行一次移位和两次加法,并将结果放入 eax 而不是从该地址加载。

gdb 是做什么的

在 gdb 中,当您键入时,p <expression>它会尽力评估<expression>其对 C/C++ 编译器对该表达式的含义的理解。所以当你说

(gdb) p msg

它看着msg并说“这看起来像一个变量,所以让我们获取该变量的当前值并打印它”。现在它知道编译器喜欢将全局变量放入 .data 段中,并且他们为那些与变量同名的变量创建符号。由于它将msg符号表中的符号视为.data段中的符号,因此它假定这是正在发生的事情,并获取该符号处的内存并打印它。现在它不知道那个变量是什么类型(没有调试信息),所以它猜测它是一个 32 位 int 并打印它。

所以输出

$1 = 1700946284

是 msg 的前 4 个字节,被视为整数。

因为p &msg它知道你想获取变量的地址msg,所以它直接从符号中给出地址。打印地址时,gdb 会打印它所拥有的关于这些地址的类型信息,因此会出现“数据变量,无调试信息”。

如果你愿意,你可以使用强制转换来指定 gdb 的类型,它会使用那个类型而不是它猜测的类型:

(gdb) p (char)msg
$6 = 108 'l'
(gdb) p (char [10])msg
$7 = "labeled st"
(gdb) p (char *)&msg
$8 = 0x80490e4 "labeled string\\nunlabeled-string\\n\n\n\n\n\n\n\n\n" <Address 0x804910e out of bounds>

请注意,在后一种情况下,字符串上没有 NUL 终止符,因此它会打印出整个数据段......


要使用 sys_write 打印未标记的字符串,您需要弄清楚字符串的地址和长度,这几乎是您拥有的。为了完整起见,您还应该检查返回值:

    mov ebx, 1           ; fd 1 (stdout)
    lea ecx, [msg+15]    ; address
    mov edx, 17          ; length
write_more:
    mov eax, 4           ; sys_write
    int 80H              ; write(1, &msg[15], 17)
    test eax, eax        ; check for error
    js error             ; error, eax = -ERRNO
    add ecx, eax
    sub edx, eax
    jg write_more        ; only part of the string was written
于 2012-07-12T22:29:13.823 回答
4

克里斯·多德...


(除此之外——我不知道 & 对 nasm 意味着什么——它没有出现在我能看到的文档中的任何地方,所以我很惊讶它没有给出错误。但它看起来像是把它当作一个[] 的别名)

哦,哦!您已经发现了秘密语法!很久以前,根据用户请求,“&”被添加到 Nasm(作为“[]”的别名)。它从未被记录在案。也从未删除。我会坚持使用“[]”。作为“无证”,它可能会消失。请注意,其含义与 gdb 的含义几乎“相反”!

可能会尝试“-F dwarf”而不是“-F stabs”。它应该是 gdb 使用的“本机”调试信息格式。(我自己从来没有注意到太大的不同)

最好的,

坦率

http://www.nasm.us

于 2012-07-13T08:05:45.180 回答