【问题标题】:Most basic x86 addition program最基本的x86加法程序
【发布时间】:2020-08-01 19:38:41
【问题描述】:

我编写了以下基本程序来添加两个数字,1+2,如下:

.globl main

main:

    # put 1 (1 byte int/char) into accumulator register
    mov     $1,     %eax

    # add 2 (1 byte int/char), storing result in accumulator
    add     $2,     %eax

    # move the result of the accumulator into Data register (input/output)
    mov     %eax,   %edx

    ret

编译后,它会返回预期的输出:

$ gcc d.s -o d2.out && ./d2.out; echo $?
3

我有几个关于这个程序的问题:

  • 这或多或少是一个 OK 程序,还是我误用了任何操作等?
  • 程序集文件是否总是必须有一个globl 函数,例如main,或者它是否可以,例如,删除main / .globl main 部分,然后“逐行运行代码”行”?
  • 最后,查找操作码的最佳资源是什么?我倾向于用谷歌搜索这些,它会返回不同的结果:如果有像 Python docs 这样的标准资源会很好,我可以在其中添加一个页面的书签并在那里查看所有内容。

【问题讨论】:

  • 1.) 干得好,但mov %eax,%edx 至少在这种情况下是不必要的。 2.) 您可以将其编译为平面二进制文件(不是目标文件),将其加载到 C 代码中,使其可执行并从那里运行。或者您可以使用调试器逐行运行代码。 3.) 我没有找到资源
  • 返回值进入 eax,而不是 edx。在这里设置任何值的 edx 是没有意义的。
  • @JCWasmx86 -- 我明白了。我一定把eax和edx搞混了。 %eax是程序返回控制后OS读取的寄存器和程序的返回码?
  • EAX 由 main 返回给 C 运行时。 C 运行时将值传递给 OS _exit 函数,可以在 EDI 中或在堆栈中,具体取决于您的系统。 shell 调用wait 来检索值,以便打印它。 (可能比你想知道的更多。)
  • @prl: nitpicks - 32 位 Linux 的系统调用 ABI 按 EBX、ECX、EDX 等顺序传递参数。 C 运行时实际上使用exit_group(2) 系统调用(x86-64 上的 EAX=231/syscall),而不是_exit(EAX=60/系统调用)。 (名为 _exit(2) actually uses exit_group 的 libc 包装函数)。并且这仅在进行清理(如刷新 stdio 缓冲区)之后,与exit(3) 库函数相同。 (glibc 的 _exit 在错误(包括 ENOSYS)来自 exit_group 时有趣地回退到 exit

标签: assembly x86 gnu-assembler att


【解决方案1】:

mov 到 EDX 是没有意义的,返回值寄存器是 AL / AX / EAX / RAX / RDX:RAX 对于 x86-64 上从 1 字节到 16 字节的宽度。 EDX 或 RDX 仅用于宽返回值,太宽而无法放入 RAX。 (或者在 32 位模式下,在 EDX:EAX 寄存器对中返回 64 位值,因为没有 RAX。)

这适用于所有标准 x86 32 位和 x86-64 调用约定,包括 GNU/Linux 上使用的 i386 和 x86-64 System V ABI。


如果您正在编写 main,或者您想从另一个文件调用的任何函数,它必须是 .globl 符号。(除非您使用 .include "foo.s" 而不是单独构建+链接。)这就是使它在符号表中可见的原因,以便链接器解析对它的引用。例如来自_start 的已编译代码中的call maincrt0.o 或其他内容,如果您运行gcc -v foo.S,您可以看到gcc 链接。 (这是一个过度简化;glibc 的 _start 实际上将 main 的地址作为 arg 传递给 __libc_start_main,它位于 libc.so.6 中,所以有一些来自 libc 的代码在 main 之前运行。参见 Linux x86 Program Start Up or - How the heck do we get to main()? )

如果您要在没有 CRT 的情况下制作静态可执行文件(定义 _start 而不是 main 并进行自己的 exit_group 系统调用),您可以只需将指令放入文件中,然后让链接器 (ld) 选择 .text 部分的顶部作为 ELF 入口点,如果它没有找到 _start 符号。 (使用readelf -a a.out 来查看此类信息。)

如果您只打算在 GDB 下运行该程序以单步执行一些您感兴趣的指令,您甚至可以省略 exit-cleanly 部分。 (为此,使用 GDB 的 starti 命令在第一条用户空间指令之前使用临时断点运行,因此您不必通过绝对地址手动设置断点(因为没有符号)。)

$ cat > foo.S
mov $1 + 2, %edi     # do the math at assemble time
mov $231, %eax         # _NR_exit_group
syscall

$ gcc -static -no-pie -nostdlib foo.S      # like as + ld manually
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000

$ ./a.out ; echo $?
3

$ strace ./a.out
execve("./a.out", ["./a.out"], 0x7ffe0706a3c0 /* 54 vars */) = 0
exit_group(3)                           = ?
+++ exited with 3 +++

如果您的系统是 32 位的,那么 as 默认为 32 位模式,请使用 32 位 int $0x80 和不同的寄存器。

最后,查找操作码的最佳资源是什么?

我通常将浏览器选项卡打开到https://www.felixcloutier.com/x86/,这是英特尔第 2 卷手册的 HTML 抓取。 original PDF 有一些关于如何阅读条目的介绍章节,所以如果您发现任何符号混淆,请查看。英特尔手册中有一些旧版的遗漏了 SIMD 指令,所以这对我来说毫无用处,但也许是初学者想要的。

其他资源链接自x86 tag wiki,包括http://ref.x86asm.net/coder64.html,它是按操作码而非助记符组织的,并有快速参考列提醒您指令是否读取或修改FLAGS,如果是,是哪个,以及类似的东西。

【讨论】:

  • 谢谢。 no-pie 是做什么的?当我运行时,我得到gcc: error: unrecognized command line option ‘-no-pie’。虽然删除它允许我链接它并返回正确的结果。
  • @David542:显然你有一个相当老的 GCC。请参阅32-bit absolute addresses no longer allowed in x86-64 Linux?What's the difference between "statically linked" and "not a dynamic executable" from Linux ldd? - 在发行版默认启用它之前它已经存在了一段时间。它生成一个传统的位置相关可执行文件,因此您在objdump -d 中看到的地址和启动程序之前的 GDB 反汇编,将匹配您在启动程序之后看到的地址,在链接时固定,而不是运行时
  • @David542:即使在现代系统上,-no-pie-static 也是多余的,当您使用 -nostdlib 时(不要链接 CRT 的 _start,也不要链接任何库)。
  • "[...] 供链接器解析对它的引用。那是"此处缺少句子的一部分。
  • @ecm:谢谢,被另一个想法分心了,忘了回来完成那个想法。多动症:P
【解决方案2】:

这或多或少是一个好的程序,还是我误用了任何操作等?

首先,是的。

然而,汇编就是为了效率,所以最后一条语句是不必要的:

mov     %eax,   %edx

汇编文件是否总是必须有一个全局函数,例如 main

不一定。例如,它可以是您可以从 C/C++ 代码调用的其他函数。但是,如果您想从中生成可执行文件,则需要 main_start 如果您使用 ld 作为链接器。

“逐行运行代码”?

为此,您需要一个调试器。如果您想学习汇编,这将是最重要的。你会想看看寄存器,看看值是如何变化的,标志发生了什么等等。我给了一个answer,它解释了一点关于如何设置调试器和单步调试你的代码。与gcc 组装时,您需要-g 标志来调试您的代码。

一个基本的例子:

  1. -g编译
gcc -g file.s -o file
  1. tui 模式启动gdb。
> gdb --tui ./file
> start           # this will automatically start the program and break at main:
> layout regs     # show registers at the top (you will need this a lot)
> n               # next instruction
> si              # step into, when you use functions, si into function

在gdb中按回车会自动再次执行上一条命令。这将使您不必一遍又一遍地输入n。更多命令:

> b 2      # break at line 2
> b func   # break at label func
> b main   # break at main

> print/x  $eax  # print value in eax in hex form, there are other /format specifiers, print/d (decimal), print/s string, print/t (binary)
> x/s $eax    # print string pointed to by eax

> info frame   # look at the current stack frame

这些是您需要的最常见的说明。您可以键入help command_name 以获取有关命令的更多信息。并且有各种备忘单等可以帮助您解决这个问题。

如果你愿意,你也可以得到一个 gui,我个人不太喜欢它们。结帐 Nemiver,这非常好。 gdbgui 可以使用 pip 进行设置,但它对于调试 asm 并不是很好,因为看寄存器很痛苦。我最喜欢 ddd,但它的 gui 是 1970 年代的所以...

最后,查找操作码的最佳资源是什么?

最好的资源是英特尔手册,但是如果您刚刚开始阅读,它们可能有点难以阅读。我会推荐Felix Cloutier's x86 asm referencex86标签wiki中有很多信息和参考的东西。

您可能还想阅读Calling Conventions for Linux 和查找Linux Syscalls,您将非常需要这些内容。如果您打算编程或只是想了解更多关于计算机的知识,我强烈建议您阅读Programming from the Ground Up 这本书,该书可免费获得并使用 AT&T 风格的程序集。然而它有点过时了,所以你必须用谷歌搜索。它有一个包含常见 x86 指令的附录,非常有帮助。

【讨论】:

  • 谢谢,这里有几个跟进。 %eax 是操作系统最后读取的寄存器吗?您能否展示使用调试器运行一两行 asm 的最基本示例?
  • 是的,%eax 中的值被返回给操作系统。
  • GDB n 是下一个源代码行。 ni 是下一个指令。这对于 asm 以外的语言很重要,或者如果您有宏,或者调试信息不​​完美。此外,在现代 GDB 中,您可以使用 start 来运行程序,并在 main 的顶部设置一个临时断点。或starti 在第一个用户空间指令之前停止,在入口点。
  • @Waqar 感谢您的建议。我得到了“从头开始编程”一书。这是否包括组装、链接和加载?还是只是写汇编?
  • 它涵盖了很多东西,组装,链接,cpu架构和其他东西。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-10-26
  • 1970-01-01
  • 2019-04-22
  • 2019-01-25
  • 1970-01-01
  • 2016-07-30
  • 1970-01-01
相关资源
最近更新 更多