怎样用汇编语言在Linux的X64平台上编写HelloWorld屏幕打印

前言

     用汇编语言在Linux的X64平台上编写HelloWorld屏幕打印,这个问题似乎不是问题,从网上一搜就能找到。好,现在上网搜一搜。

section .data
msg db "Hello World!",0ah
len equ $-msg
global _start
_start:
mov eax,4 ;   4号调用
mov ebx,1 ;   ebx送1表示标准输出
mov ecx,msg ; 字符串的首地址送入ecx
mov edx,$len ;  字符串的长度送入edx
int 80h;    输出字串
mov eax,1;   1号调用
mov ebx,0; 返回0
int 80h ;    结束

     这是在百度搜索找到的,这段代码可以在X64平台上编译运行,但并不意味着它符合Linux 64位平台的系统调用规范,这是32位的代码,只不过能在64位系统上运行而已。那么64位的汇编代码怎么写?我们在后面慢慢论述。

 1. 把32位的代码改成64位代码

     首先,64位系统不再使用int 80h中断,而是用系统调用syscall,建议使用64位寄存器。屏幕打印的系统调用号不再是4,而是1,仍然用1表示标准输出,但不是送入ebx寄存器而是送入rdi寄存器。返回系统的调用号是60,不再是1号调用,并用rdi传出返回值而不是用ebx传送返回值。改了以后的代码如下:

section .data
msg db "Hello World!",0ah
len equ $-msg
global _start
_start:
mov rax, 1      ;1号调用
mov rdi, 1      ;rdi送1表示标准输出
mov rsi, msg    ;字符串的首地址送入rsi
mov rdx, $len   ;字符串的长度送入rdx
syscall         ;系统调用输出字串
mov rax, 60     ;系统返回,60号调用
mov rdi, 0      ;返回0
syscall         ;结束

    这样就改好了,但这样的代码框架老土,套路老旧,一点新意都没有,而且编译链接麻烦。难道没有更好的方法吗?想想看,在linux系统下有丰富的C语言库函数,我们可以用这些标准库函数,大大增强我们编程的表达能力,其次Linux系统的编译器是gcc,巧妙的利用gcc能大量节省开发的时间成本,第三,用at&t的语法风格有利于在逻辑思维上与C语言衔接。现在我们用这个思路把上面的代码再改改。

.section .data
    output:
        .asciz "hello, world!\n"
//.data ends
.section .text
.global main
main:
        leaq output, %rdi
        xor %rax, %rax
        call printf
    finish:
        movq $60, %rax
        xor %rdi, %rdi
        syscall
//.text ends
//end

这样代码看起来简洁多了,编译链接用gcc一步到位。

        gcc -no-pie -o test test.S

 因为用了data段存放静态数据,所以要用-no-pie选项来编译。

 ???不用静态数组行不行,不用data数据段不行吗???

试试看。 

2. 删除data数据段 

   在汇编里面能代替data数据段的还有bss段,这是为未初始化的数据准备的,可以利用这个段在里面定义一个字符串数组并为它保留16个byte的内存空间,在程序中动态给它赋值,最后用调用puts函数把字符串打印出来。现在可以写代码了,写出的代码如下:

.section .bss
    .lcomm output, 16
//.bss ends
.section .text
.global main
main:
        leaq output, %rdi
        movb $'H', (%rdi)
        movb $'e', 1(%rdi)
        movb $'l', 2(%rdi)
        movb $'l', 3(%rdi)
        movb $'o', 4(%rdi)
        movb $',', 5(%rdi)
        movb $' ', 6(%rdi)
        movb $'W', 7(%rdi)
        movb $'o', 8(%rdi)
        movb $'r', 9(%rdi)
        movb $'l', 10(%rdi)
        movb $'d', 11(%rdi)
        movb $'!', 12(%rdi)
        movb $'\n', 13(%rdi)
        movb $0, 14(%rdi)
        xor %rax, %rax
        call puts
    finish:
        movq $60, %rax
        xor %rdi, %rdi
        syscall
//.text ends
//end

 data段没有了,因为有临时数据段bss所以用gcc编译时还需要带上-no-pie这个选项。

        gcc -no-pie -o test test.S

 ???这样的代码还是不能令人满意!编译时带上烦人的-no-pie选项不说,编译出来的可执行文件都不是位置无关的。能去掉BSS段吗???

 试试看。

 3. 继续去掉BSS数据段

     没有了data数据段也没有bss段,我们可以利用栈空间里面的内存来存放数据。把“Hello, World!”这一串字符,一个字符一个字符地连续摆放到栈空间的内存中,关键是要找回这串字符的首地址。字符串在内存中如同生活在一维空间里的鱼,我不是想在这里论述大尾小尾的问题,问题是一维空间里的鱼是不会转身的,要搞清楚鱼嘴朝哪,在什么位置,才能钓上鱼。那么怎么钓这条鱼?我们用leaq指令把字符串的首地址传出来就钓上这条鱼了。现在可以写代码了,编写完成的代码如下:

.section .text
.global main
main:
        pushq %rbp
        movq %rsp, %rbp
        subq $48, %rsp
        movb $'H', -16(%rbp)
        movb $'e', -15(%rbp)
        movb $'l', -14(%rbp)
        movb $'l', -13(%rbp)
        movb $'o', -12(%rbp)
        movb $',', -11(%rbp)
        movb $' ', -10(%rbp)
        movb $'W', -9(%rbp)
        movb $'o', -8(%rbp)
        movb $'r', -7(%rbp)
        movb $'l', -6(%rbp)
        movb $'d', -5(%rbp)
        movb $'!', -4(%rbp)
        movb $'\n', -3(%rbp)
        movb $0, -2(%rbp)
        leaq -16(%rbp), %rdi
        xor %rax, %rax
        call puts
    finish:
        addq $48, %rsp
        popq %rbp
        movq $60, %rax
        xor %rdi, %rdi
        syscall
//.text ends
//end

 这下可以不用带-no-pie选项来编译了,象这样:

        gcc -o test test.S

 ???为什么要放在主程序中处理?为什么不能把字符摆放内存和打印的代码都放到子程序中???

 嗯, 这好办。

  4. 在子程序中处理字符串

     在子程序中要解决一个问题,子程序如何使用堆栈?要么共享主程序的堆栈,要么自己建堆栈。如果与主程序共享栈空间就要小心翼翼地解决内存冲突问题,自己建堆栈就省了很多麻烦,我们选择后者。改好的代码如下:

.section .text
.global main
main:
        pushq %rbp
        movq %rsp, %rbp
        subq $32, %rsp
        call hello_print
    finish:
        addq $32, %rsp
        popq %rbp
        movq $60, %rax
        xor %rdi, %rdi
        syscall
    hello_print:
        push %rbp
        mov %rsp, %rbp
        subq $48, %rsp
        movb $'H', -16(%rbp)
        movb $'e', -15(%rbp)
        movb $'l', -14(%rbp)
        movb $'l', -13(%rbp)
        movb $'o', -12(%rbp)
        movb $',', -11(%rbp)
        movb $' ', -10(%rbp)
        movb $'W', -9(%rbp)
        movb $'o', -8(%rbp)
        movb $'r', -7(%rbp)
        movb $'l', -6(%rbp)
        movb $'d', -5(%rbp)
        movb $'!', -4(%rbp)
        movb $'\n', -3(%rbp)
        movb $0, -2(%rbp)
        leaq -16(%rbp), %rdi
        xor %rax, %rax
        call puts        
        addq $48, %rsp
        pop %rbp
        ret
//.text ends
//end

嗯,这样主程序就显得简洁多了。还是如前面的一样编译:

        gcc -o test test.S

???虽然主程序是显得简洁了,但这样不是浪费了主程序的栈空间内存了吗?为什么不把“Hello, World!”这一串字符放一半在主程序中,另一半放在子程序中呢???

汗颜......好,试试看吧......

 5. 主程序和子程序各处理字符串的一半......

     字符串的一半在主程序当中,另一半在子程序当中,那么就要处理子程序如何调用主程序中的数据这个问题,我们把主程序一半字符串的首地址传给寄存器rdi,并由rdi带到子程序中来,再把子程序的那一半字符串包装成格式化字符串,然后交给printf函数打印,就像C语言这样:   

        char *w = "World!";
        printf("Hello, %s\n", w);

但无论怎么样,要先搞清楚鱼嘴在什么地方。现在可以编写代码了,写出的代码如下: 

.section .text
.global main
main:
        pushq %rbp
        movq %rsp, %rbp
        subq $48, %rsp
        movb $'W', -12(%rbp)
        movb $'o', -11(%rbp)
        movb $'r', -10(%rbp)
        movb $'l', -9(%rbp)
        movb $'d', -8(%rbp)
        movb $'!', -7(%rbp)
        movb $0, -6(%rbp)
        push %rdi
        leaq -12(%rbp), %rdi
        call hello_print
        pop %rdi
    finish:
        addq $48, %rsp
        popq %rbp
        movq $60, %rax
        xor %rdi, %rdi
        syscall
    hello_print:
        push %rbp
        mov %rsp, %rbp
        subq $48, %rsp
        movq %rdi, -24(%rbp)
        movb $'H', -12(%rbp)
        movb $'e', -11(%rbp)
        movb $'l', -10(%rbp)
        movb $'l', -9(%rbp)
        movb $'o', -8(%rbp)
        movb $',', -7(%rbp)
        movb $' ', -6(%rbp)
        movb $'%', -5(%rbp)
        movb $'s', -4(%rbp)
        movb $'\n', -3(%rbp)
        leaq -12(%rbp), %rdi
        movq -24(%rbp), %rsi
        xor %rax, %rax
        call printf
        addq $48, %rsp
        pop %rbp
        ret
//.text ends
//end

 用gcc编译还是如前面一样:

        gcc -o test test.S

???这样处理似乎不错,但是一个一个地把字符摆入内存不是很麻烦吗?为什么不一次就把字符串输入内存中呢???

好,试一下吧,如果这样的话代码的可读性就没有这么好了。

 6.把字符串打包成64位立即数

     我们先数一下字符串字符的个数。“Hello, World!\n”有14个字符,还要在末尾加一个“\0”,一共就有15个字符,可以打包成两个64位立即数,并把这两立即数放入内存中,然后找到字符串的首地址并把它传送出来,如同下钩把鱼钩住,还是跟前面一样要搞清楚鱼嘴在什么地方。现在可以编写代码了,写好的代码如下:

.section .text
.global main
main:
        pushq %rbp
        movq %rsp, %rbp
        subq $48, %rsp
        movq $0x00202c6f6c6c6548, %rax
        movq %rax, -18(%rbp)
        movq $0x000a21646c726f57, %rax
        movq %rax, -11(%rbp)
        leaq -18(%rbp), %rdi
        xor %rax, %rax
        call puts
    finish:
        addq $48, %rsp
        popq %rbp
        movq $60, %rax
        xor %rdi, %rdi
        syscall
//.text ends
//end

 用gcc编译运行还是打印出:Hello, World!

 

???为什么要两次放入内存?这不符合题目的要求,要一次放入内存中,一次,一次,记住是一次!???

天啊......试试看吧......

 7. 把字符串一次放入内存,打包成128位数据? 

    首先感谢英国,我们大声喊“Hello, World!”的时候,这些字母都不会超过十六个,否则把“Hello, World!”一次放入内存中,这辈子都不可能完成。

    “Hello, World!”加上回车符,再加上结尾的“\0”才15个字符,可以打包成128位无符整数,问题是现在的64位系统只允许立即数是64位宽度的,但幸运的是X64的CPU里面处理多媒体的寄存器可以处理128位无符整数,把64位的立即数通过64位通用寄存器传送给xmm0和xmm1,再把xmm0和xmm1合成128位无符整数,然后再传入栈空间的内存中,再把内存中的字符串首地址传送出来,这样就完成了符合题目要求的既定目标。我们用这段代码来结束这篇文章。

.section .text
.global main
main:
        pushq %rbp
        movq %rsp, %rbp
        subq $48, %rsp
        movq $0x202c6f6c6c654800, %rax
        movq %rax, %xmm0
        movq $0x000a21646c726f57, %rax
        movq %rax, %xmm1
        movlhps %xmm1, %xmm0
        movdqu %xmm0, -24(%rbp)
        leaq -23(%rbp), %rdi
        xor %rax, %rax
        call puts
    finish:
        addq $48, %rsp
        popq %rbp
        movq $60, %rax
        xor %rdi, %rdi
        syscall
//.text ends
//end

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值