4_汇编编程小结_补充算术操作指令

汇编编程示例

处理命令行参数的示例

汇编程序启动的时候, 你可以在命令行里面给它传入参数, 现在我们看看怎么在汇编里处理这个参数.

.text
.globl _start

_start:
    popl    %ecx    # argc
vnext:
    popl    %ecx    # argv
    test    %ecx, %ecx  # 空指针表明结束
    jz  exit
    movl    %ecx, %ebx
    xorl    %edx, %edx
strlen:
    movb    (%ebx), %al
    inc %edx
    inc %ebx
    test    %al, %al
    jnz strlen
    movb    $10, -1(%ebx)   # $10 为换行键
    movl    $4, %eax    # 系统调用号 (sys_write)
    movl    $1, %ebx    # 文件描述符 (stdout)
    int $0x80
    jmp vnext

exit:
    movl    $1, %eax    # 系统调用号 (sys_exit
    xorl    %ebx, %ebx  # 退出代码
    int $0x80

以下为代码详细注释


popl %ecx # argc, 我们从这个注释就可以看出来, 当一个程序通过命令行启动, 传输给它的参数 (argc, argv) 还是保存在栈中. 从命令行来看, 它的传输也还是从右往左压栈. 所以这第一个参数最后压栈, 最先出栈. 然后我就知道 ecx 里面存着当前有多少参数的数值.

如果你 C 程序写熟了的话, int main 第一个参数就是 int argc, 表示你这 main 函数里面一共有多少个参数.


然后我们进入一个循环 vnext, 继续 popl. 现在我知道有多少个参数了, 然后我再 pop 下一个. 下一个 argv 是什么意思呢, 我们再回忆下 C 里面的 main 函数.

C 里面 main 函数还有个参数就是 char **argv, 实际上里面就指向了一系列的参数, 指向了一个参数表, 就是每个参数在里面都用一个字符串的形式来存储的, 我们就指向它这个地址.


test %ecx, %ecx

jz exit

然后如果是 ecx 是个空指针 null, 就结束, 说明我们没有其他的参数.


退出的话呢, 就用系统调用 sys_exit. ebx 退出代码就是 0 了, 自己和自己进行异或嘛, 就退出了.


如果不是空指针的话, 那就一行行来.

实际上就是 movl %ecx, %ebx, 然后 xorl %edx, %edx edx 清零.

ecx 里面存储着参数列表当前的起始地址, 这个地址给放到 ebx 里面去


放进去之后, 我们就一个一个地把这个字符串参数, 把它里面的每个字节一个个都给它取出来, 因为命令行参数最终都转换成一个字符串的形式, 然后取出来, 就是 movb (%ebx), %al

ebx 取第一个放到 al 里面去


inc %edx, edx 相当于计数, 看你当前字符串参数, 里面有多少 byte


inc %ebx, 相当于指针往后面挪了一下


test %al, %al, 如果 al 为 0, 就说明当前字符串参数就结束了

如果不结束的话, 继续循环

现在结束了, 我们相当与把当前 argv 的长度计算出来了, 计算出来的长度值实际上放到了 edx 里面了


然后在里面添加一个换行符 jnz strlen movb $10, -1(%ebx), 现在字符串参数结束了, 我就把 $10 挪过去放到你字符串的尾巴上, $10 正好是换行符


然后我通过 write 系统调用, 在标准输出上面, 把你的命令行参数打出来, 打出来之后再跳回去

我们这就把它一个个的命令行参数给一个个地打出来

调用 lib_c 库函数

这个程序能够打印出你当前使用的处理器生产厂商它的 id 是什么.

.section    .data
output:
    .asciz  "The processor vendor id is '%s'\n" # 这里需要说明, 我们用的不是 .ascii 而是 .asciz, 就是说这还是字符串, 但是我默认在你字符串后面加一个 0. 因为我们后面调用 printf, C 语言里面字符串默认找 0 作为结尾.
.section    .bss    # 可读可写并且没有初始化的数据段.
# 实际上数据段里面分成各种各样的类型, .bss 是说它是一个全局的数据段, 大家各个模块都可见, 但是这个全局数据段是没有经过初始化的. 这种初始化和未初始化的数据段是分开的, 稍后解答为什么区分开.
    .lcomm  buffer,12   # 在这个数据段里声明一个长度为 12 的 buffer
.section    .text
.globl  _start
_start:
    movl    $0, %eax
    cpuid
    mov $buffer, %edi   # buffer 就是上面我们指明的数据段的地址, 注意它前面有 $ 符号, 就相当于把这个地址本身放到 edi 里面去.
    movl    %ebx, (%edi)
    movl    %edx, 4(%edi)
    movl    %ecx, 8(%edi)   # 我们把生成的这三个信息, 挨个存到 edi 指向的那块内存区域里面去
    push    $buffer # printf 规格化的一个字符串
    push    $output # 转义字符序列
    call    printf  # 调用 printf
    addl    $8, %esp    # 因为是调用 C 库函数, 所以遵循 C 的调用约定
    push    $0
    call    exit    # 调用 libc 里面的 exit 函数

cpuid 是一条汇编指令, 它是请求处理器的特定信息, 并且把信息返回到特定寄存器的低级指令.

它是用单一寄存器值作为输入, 实际上就是 %eax 寄存器, %eax 用于指明 cpuid 到底要生成什么信息. 取回来的信息根据 %eax 的值, 在 %ebx, %ecx, %edx 寄存器中存放关于处理器的不同信息.

当前我们要取 CPU 生产厂家的 id, 那就是填充 0, 之后就把厂商 id 字符串返回到 %ebx %edx %ecx 寄存器中, 每个里面自低向高存放 4 个字节 :

1) %ebx 包含字符串的最低 4 个字节

2) %edx 包含字符串的中间 4 个字节

3) %ecx 包含字符串的最高 4 个字节

as -o cpuid.o cpuid.s --32

ld -lc -dynamic-linker /lib/ld-linux.so.2 -o cpuid cpuid.o -m elf_i386

因为我们调用的 libc 函数, .lc 就是去连接 libc 库, 我们采用这种动态链接的方式, 生成一个执行文件