二、2.打印和内联汇编

news2024/11/13 8:05:27

调用约定, calling conventions,从字面上理解,它是调用函数时的一套约定,是被调用代码的接口,它体现在

  • 参数的传递方式,是放在寄存器中?栈中?还是两者混合?

  • 参数的传递顺序,是从左到右传递?还是从右到左?

  • 是调用者保存寄存器环境,还是被调用者保存?保存哪些寄存器呢?


cdecl 调用约定由于起源于 C 语言,所以又称为 C 调用约定,是 C 语言默认的调用约定。 cdecl 的调
用约定意味着 :

  1. 调用者将所有参数从右向左入枝。
  2. 调用者清理参数所占的枝空间。

cdecl 调用约定最大的亮点是它允许函数中参数的数量不固定,我们熟识的 printf 函数,它能够支持变长参数,就是利用此 cdecl 调用约定的性质设计出来的


汇编语言和C语言混合编程可分为两大类。

  1. 单独的汇编代码文件与单独的C语言文件分别编译成目标文件后, 一起链接成可执行程序。

  2. 在C 语言中嵌入汇编代码,直接编译生成可执行程序。

本节所说的“汇编语言和 C语言混合编程”属于第 1 种,第 2种的内嵌汇编又称为内联汇编


系统调用是 Linux 内核提供的一套子程序,它和 Windows 的动态链接库 DLL 文件的功能一样,用来实现一系列在用户态不能或不易实现的功能,比如最常见的读写硬盘文件, 系统调用很像 BIOS 中断调用(在很久很久以前咱们有说过 BIOS 中断、 DOS中断等内容),只不过 系统调用的入口只有一个,即第 0x80 号中断,

为什么系统调用只有一个入口呢?中断的实现是要用到中断描述符表的,表中很多中断项(号)是被预留的,不能强占,所以 Linux 就选了一个可用的中断号作为所有系统调用的统一入口,具体的子功能在寄存器 eax 中单独指定。


调用“系统调用”有两种方式。

  1. 将系统调用指令封装为 c 库函数,通过库函数进行系统调用,操作简单。
  2. 不依赖任何库函数,直接通过汇编指令 int 与操作系统通信。

演示Linux系统调用

//代码 C_with_S_c.c
extern void asm_print(char*,int);
void c_print(char* str) (
    int len=O;
    while(str[len++]);
    asm_print(str, len);
}
;代码 C_with_S_S.S
section .data
str: db "asm_print says hello world !", 0xa, 0
;0xa 是换行符, o 是手工加上的字符串结束符\ 0 的 ASCII 码
str_len equ $-str

section .text
extern c_print
global _start
_start:
    ;;;;;;;;;调用 c 代码中的函数 c_print;;;;;;;;;;;
    push str ; 传入参数
    call c_print ;调用 c 函数
    add esp,4 ;回收钱空间

    ;;;;;;;;;;退出程序 ;;;;;;;;;;;;;;;;;;;
    mov eax,1 ;第 1 号子功能是 exit 系统调用
    int 0x80 ; 发起中断,通知 Linux 完成请求的功能

	;;;;;;;;;asm_print的实现;;;;;;;;;;;;;;
    global asm_print
    asm_print:    ;相当于 asm_print ( str, size)
    push ebp; 备份 ebp
    mov ebp,esp
    mov eax,4;第 4 号子功能是 write 系统调用
    mov ebx, 1;此项固定为文件描述符 1 ,标准输出( stdout )指向屏幕
    mov ecx, [ebp+8] ;第 1 个参数
    mov edx, [ebp+12];第 2 个参数
    int 0x80;发起中断,通知 Linux 完成请求的功能
    pop ebp;恢复 ebp
    ret

总结一下

  • 在汇编代码中导出符号供外部引用是用的关键字 global ,引用外部文件的符号是用的关键字extern
  • 在C代码中只要将符号定义为全局便可以被外部引用(一般情况下无需用额外关键宇修饰,具体请参考C语言手册),引用外部符号时用 extern 声明即可。

端口实际上就是 IO 接口电路上的寄存器,为了能访问到这些 CPU 外部的寄存器,计算机系统为这些寄存器统一编址, 一个寄存器被赋予一个地址,这些地址可不是我们所说的内存地址,内存地址是用来访问内存用的,其范围取决于地址总线的宽度,而寄存器的地址范围是 0~65535 (Intel 系统)。这些地址就是我们所说的端口号,用专门的 IO 指令 in 和 out 来读写这些寄存器。

给寄存器分组的原因是显卡(显示器的 IO 接口电路)上的寄存器太多了,如果一个寄存器就要占用一个系统端口的话,这得多浪费硬件资源,万一别的硬件也这么干,这 65536 个地址可就捉襟见肘了。

把每一个寄存器分组视为一个寄存器数组,提供一个寄存器用于指定数组下标,再提供一个寄存器用于对索引所指向的数组元素(也就是寄存器)进行输入输出操作。这样用这两个寄存器就能够定位寄存器数组中的任何寄存器啦。

这两个寄存器就是各组中的 Address Register 和 Data Register o Address Register 作为数组的索引 (下标) , Data Register 作为寄存器数组中该索引对应的寄存器,它相当于所对应的寄存器的窗口,往此窗口读写的数据都作用在索引所对应的寄存器上。

所以,对这类分组的寄存器操作方法是先在 Address Register 中指定寄存器的索引值,用来确定所操作的寄存器是哪个,然后在 Data Register 寄存器中对所索引的寄存器进行读写操作。


nasm -I include/ -o mbr.bin mbr.S
dd if=./mbr.bin of=/code/hd60M.img bs=512 count=1 conv=notrunc
nasm -I include/ -o loader.bin loader.S
dd if=./loader.bin of=/code/hd60M.img bs=512 count=1 seek=2 conv=notrunc

nasm -f elf -o lib/kernel/print.o lib/kernel/print.S
gcc -I lib/kernel/ -m32 -c -o kernel/main.o kernel/main.c
ld -Ttext 0xc0001500 -e main -m elf_i386 -s -o kernel.bin kernel/main.o lib/kernel/print.o
dd if=kernel.bin of=/code/hd60M.img bs=512 count=200 seek=9 conv=notrunc

在上面的链接阶段,目标文件链接顺序是 main.o 在前, print.o 在后。大家知道, main.c 文件中用到了 print.o 中的 put_char 函数,在链接顺序上,属于“调用在前,实现在后”的顺序。如果将 print.o 放在前面, main.o 放在后面,也就是实现在前,调用在后,此时生成的可执行文件起始虚拟地址并不准确,会有向后顺延的现象,并且 segment 的数量也不一样。原因是链接器对符号表的处理细节造成的,链接器主要工作就是整合目标文件中的符号,为其分配地址,让使用该符号的文件可以正确定位到它。由于我能力有限,只能大概说一下我的理解,链接器最先处理的目标文件是参数中从左边数第一个(咱们这里是 main.o ),对于里面找不到的符号(这里是 put_char),链接器会将它记录下来,以备在后面的目标文件中查找。如果将其顺序颠倒,势必导致在后面的目标文件中才出现找不到符号的情况,而该符号的实现所在的目标文件早己经过去了,这可能使链接器内部采用另外的处理流程,导致出来的可执行程序不太一样。


;print.S
TI_GDT equ  0
RPL0  equ   0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

section .data
put_int_buffer    dq    0     ; 定义8字节缓冲区用于数字到字符的转换

[bits 32]
section .text
;--------------------------------------------
;put_str 通过put_char来打印以0字符结尾的字符串
;--------------------------------------------
;输入:栈中参数为打印的字符串
;输出:无

global put_str
put_str:
;由于本函数中只用到了ebx和ecx,只备份这两个寄存器
   push ebx
   push ecx
   xor ecx, ecx		      ; 准备用ecx存储参数,清空
   mov ebx, [esp + 12]	      ; 从栈中得到待打印的字符串地址 
.goon:
   mov cl, [ebx]
   cmp cl, 0		      ; 如果处理到了字符串尾,跳到结束处返回
   jz .str_over
   push ecx		      ; 为put_char函数传递参数
   call put_char
   add esp, 4		      ; 回收参数所占的栈空间
   inc ebx		      ; 使ebx指向下一个字符
   jmp .goon
.str_over:
   pop ecx
   pop ebx
   ret

;------------------------   put_char   -----------------------------
;功能描述:把栈中的1个字符写入光标所在处
;-------------------------------------------------------------------   
global put_char
put_char:
   pushad	   ;备份32位寄存器环境
   ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印时都为gs赋值
   mov ax, SELECTOR_VIDEO	       ; 不能直接把立即数送入段寄存器
   mov gs, ax

;;;;;;;;;  获取当前光标位置 ;;;;;;;;;
   ;先获得高8位
   mov dx, 0x03d4  ;索引寄存器
   mov al, 0x0e	   ;用于提供光标位置的高8位
   out dx, al
   mov dx, 0x03d5  ;通过读写数据端口0x3d5来获得或设置光标位置 
   in al, dx	   ;得到了光标位置的高8位
   mov ah, al

   ;再获取低8位
   mov dx, 0x03d4
   mov al, 0x0f
   out dx, al
   mov dx, 0x03d5 
   in al, dx

   ;将光标存入bx
   mov bx, ax	  
   ;下面这行是在栈中获取待打印的字符
   mov ecx, [esp + 36]	      ;pushad压入4×8=32字节,加上主调函数的返回地址4字节,故esp+36字节
   cmp cl, 0xd				  ;CR是0x0d,LF是0x0a
   jz .is_carriage_return
   cmp cl, 0xa
   jz .is_line_feed

   cmp cl, 0x8				  ;BS(backspace)的asc码是8
   jz .is_backspace
   jmp .put_other	   
;;;;;;;;;;;;;;;;;;

 .is_backspace:		      
;;;;;;;;;;;;       backspace的一点说明	     ;;;;;;;;;;
; 当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
; 但有可能在键入backspace后并不再键入新的字符,这时在光标已经向前移动到待删除的字符位置,但字符还在原处,
; 这就显得好怪异,所以此处添加了空格或空字符0
   dec bx
   shl bx,1
   mov byte [gs:bx], 0x20		  ;将待删除的字节补为0或空格皆可
   inc bx
   mov byte [gs:bx], 0x07
   shr bx,1
   jmp .set_cursor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

 .put_other:
   shl bx, 1				  ; 光标位置是用2字节表示,将光标值乘2,表示对应显存中的偏移字节
   mov [gs:bx], cl			  ; ascii字符本身
   inc bx
   mov byte [gs:bx],0x07		  ; 字符属性
   shr bx, 1				  ; 恢复老的光标值
   inc bx				  ; 下一个光标值
   cmp bx, 2000		   
   jl .set_cursor			  ; 若光标值小于2000,表示未写到显存的最后,则去设置新的光标值
					  ; 若超出屏幕字符数大小(2000)则换行处理
 .is_line_feed:				  ; 是换行符LF(\n)
 .is_carriage_return:			  ; 是回车符CR(\r)
					  ; 如果是CR(\r),只要把光标移到行首就行了。
   xor dx, dx				  ; dx是被除数的高16位,清0.
   mov ax, bx				  ; ax是被除数的低16位.
   mov si, 80				  ; 由于是效仿linux,linux中\n便表示下一行的行首,所以本系统中,
   div si				  ; 把\n和\r都处理为linux中\n的意思,也就是下一行的行首。
   sub bx, dx				  ; 光标值减去除80的余数便是取整
					  ; 以上4行处理\r的代码

 .is_carriage_return_end:                 ; 回车符CR处理结束
   add bx, 80
   cmp bx, 2000
 .is_line_feed_end:			  ; 若是LF(\n),将光标移+80便可。  
   jl .set_cursor

;屏幕行范围是0~24,滚屏的原理是将屏幕的1~24行搬运到0~23行,再将第24行用空格填充
 .roll_screen:				  ; 若超出屏幕大小,开始滚屏
   cld  
   mov ecx, 960				  ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次 
   mov esi, 0xb80a0			  ; 第1行行首
   mov edi, 0xb8000			  ; 第0行行首
   rep movsd				  

;;;;;;;将最后一行填充为空白
   mov ebx, 3840			  ; 最后一行首字符的第一个字节偏移= 1920 * 2
   mov ecx, 80                            ;一行是80字符(160字节),每次清空1字符(2字节),一行需要移动80次
 .cls:
   mov word [gs:ebx], 0x0720              ;0x0720是黑底白字的空格键
   add ebx, 2
   loop .cls 
   mov bx,1920				  ;将光标值重置为1920,最后一行的首字符.

 .set_cursor:   
					  ;将光标设为bx值
;;;;;;; 1 先设置高8位 ;;;;;;;;
   mov dx, 0x03d4			  ;索引寄存器
   mov al, 0x0e				  ;用于提供光标位置的高8位
   out dx, al
   mov dx, 0x03d5			  ;通过读写数据端口0x3d5来获得或设置光标位置 
   mov al, bh
   out dx, al

;;;;;;; 2 再设置低8位 ;;;;;;;;;
   mov dx, 0x03d4
   mov al, 0x0f
   out dx, al
   mov dx, 0x03d5 
   mov al, bl
   out dx, al
 .put_char_done: 
   popad
   ret

;--------------------   将小端字节序的数字变成对应的ascii后,倒置   -----------------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x,如打印10进制15时,只会直接打印f,不会是0xf
;------------------------------------------------------------------------------------------
global put_int
put_int:
   pushad
   mov ebp, esp
   mov eax, [ebp+4*9]		       ; call的返回地址占4字节+pushad的8个4字节
   mov edx, eax
   mov edi, 7                          ; 指定在put_int_buffer中初始的偏移量
   mov ecx, 8			       ; 32位数字中,16进制数字的位数是8个
   mov ebx, put_int_buffer

;将32位数字按照16进制的形式从低位到高位逐个处理,共处理8个16进制数字
.16based_4bits:			       ; 每4位二进制是16进制数字的1位,遍历每一位16进制数字
   and edx, 0x0000000F		       ; 解析16进制数字的每一位。and与操作后,edx只有低4位有效
   cmp edx, 9			       ; 数字0~9和a~f需要分别处理成对应的字符
   jg .is_A2F 
   add edx, '0'			       ; ascii码是8位大小。add求和操作后,edx低8位有效。
   jmp .store
.is_A2F:
   sub edx, 10			       ; A~F 减去10 所得到的差,再加上字符A的ascii码,便是A~F对应的ascii码
   add edx, 'A'

;将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区put_int_buffer
;高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序.
.store:
; 此时dl中应该是数字对应的字符的ascii码
   mov [ebx+edi], dl		       
   dec edi
   shr eax, 4
   mov edx, eax 
   loop .16based_4bits

;现在put_int_buffer中已全是字符,打印之前,
;把高位连续的字符去掉,比如把字符000123变成123
.ready_to_print:
   inc edi			       ; 此时edi退减为-1(0xffffffff),加1使其为0
.skip_prefix_0:  
   cmp edi,8			       ; 若已经比较第9个字符了,表示待打印的字符串为全0 
   je .full0 
;找出连续的0字符, edi做为非0的最高位字符的偏移
.go_on_skip:   
   mov cl, [put_int_buffer+edi]
   inc edi
   cmp cl, '0' 
   je .skip_prefix_0		       ; 继续判断下一位字符是否为字符0(不是数字0)
   dec edi			       ;edi在上面的inc操作中指向了下一个字符,若当前字符不为'0',要恢复edi指向当前字符		       
   jmp .put_each_num

.full0:
   mov cl,'0'			       ; 输入的数字为全0时,则只打印0
.put_each_num:
   push ecx			       ; 此时cl中为可打印的字符
   call put_char
   add esp, 4
   inc edi			       ; 使edi指向下一个字符
   mov cl, [put_int_buffer+edi]	       ; 获取下一个字符到cl寄存器
   cmp edi,8
   jl .put_each_num
   popad
   ret

内联汇编称为 inline assembly, GCC 支持在 C代码中直接嵌入汇编代码,所以称为 GCC inline assembly.

大家知道,C语言不支持寄存器操作,汇编语言可以,所以自然就想到了在 C 语言中嵌入内联汇编提升“战斗力”的方式,通过内联汇编,程序员可以实现 C 语言无法表达的功能,这样使开发能力大为提升。

内联汇编按格式分为两大类, 一类是最简单的基本内联汇编,另一类是复杂一些的扩展内联汇编,内联汇编中所用的汇编语言,其语法是 AT&T,并不是咱们熟悉的 Intel 汇编语法, GCC 只支持它,所以咱们还得了解下 AT&T。

image-20230810173738290

在 Intel 语法中,立即数就是普通的数字,如果让立即数成为内存地址,需要将它用中括号括起来,“[立即数]”这样才表示以“立即数”为地址的内存。

而 AT&T 认为,内存地址既然是数字,那数字也应该被当作内存地址,所以,数字被优先认为是内存地址,也就是说,操作数若为数字,则统统按以该数字为地址的内存来访问 。 这样,立即数的地位比较次要了,如果想表示成单纯的立即数,需要额外在前面加个前缀$。

在 AT&T 中的内存寻址还是挺独特的,它的内存寻址有固定的格式。
segreg (段基址): base_address(offset_address,index,size)
该格式对应的表达式为:segreg: base_address + offset_address + index * size.
此表达式的格式和 Intel 32 位内存寻址中的基址变址寻址类似, Intel 的格式:segreg:[base+index * size+offset]
不过与 Intel 不同的是 AT&T 地址表达式的值是内存地址,直接被当作内存来读写,而不是普通数字。

注意,格式中没有的部分也要保留逗号来占位 。 一共有 4 种变址寻址组合,下面各举个例子。

  1. 直接寻址:无 base_address,无 offset_address
    movl %eax, (,%esi,2)
    功能是将 eax 的值写入 esi * 2 所指向的内存。
  2. 寄存器间接寻址:无 base_address,有 offset_address
    movl %eax,(%ebx, %esi, 2)
    功能是将 eax 的值写入 ebx + esi * 2 所指向的内存。
  3. 寄存器相对寻址:有 base_address,无 offset_address
    movl %eax, base value(, %esi, 2)
    功能是将 eax 的值写入 base_value + esi * 2吃所指向的内存 。
  4. 变址寻址:有 base_address,有 offset_address
    movl %eax, base_value(%ebx, %esi, 2)
    功能是将 eax 的值写入 base_value + ebx + esi * 2所指向的内存。

基本内联汇编是最简单的内联形式,其格式为:asm [volatile] (“assembly code”)
各关键字之间可以用空格或制表符分隔,也可以紧凑挨在一起不分隔,各部分意义如下:

关键字 asm 用于声明内联汇编表达式,这是内联汇编固定的部分,不可少。关键宇 volatile 是可选项,它告诉 gcc:“不要修改我写的汇编代码,请原样保留”。 asm和volatile都是GCC的宏定义

“ assembly code”是咱们所写的汇编代码,它必须位于圆括号中,而且必须用双引号引起来。这是格
式要求,只要满足了这个格式 asm [volatile] (“”), assembly code 甚至可以为空。
下面说下 assembly code 的规则。

  1. 指令必须用双引号引起来,无论双引号中是一条指令或多条指令。
  2. 一对双引号不能跨行,如果跨行需要在结尾用反斜杠’'转义。
  3. 指令之间用分号’; ‘、换行符’\n’或换行符加制表符’\n’'\t’分隔。
char* str="hello,world\n";
int count = 0;
void main() {
    asm("pusha;         \
        movl $4, %eax ; \
        movl $1, %ebx;  \
        movl str, %ecx; \
        movl $12, %edx ;\
        int $0x80;      \
        mov %eax, count;\
        popa            \
     	");
}

在基本内联汇编中,若要引用 变量,只能将它定义为全局变量。如果定义为局部变量,链接时会找不到这两个符号,这就是基本内联汇编的局限性


扩展内联汇编 asm[ volatile ] ( “assemly code”:output : input : clobber/modify )

和前面的基本内联汇编相比,扩展内联汇编在圆括号中变成了4 部分,多了 output 、 input 和clobber/modify 三项。其中的每一部分都可以省略,甚至包括 assembly code 。省略的部分要保留冒号分隔符来占位,如果省略的是后面的一个或多个连续的部分,分隔符也不用保留,比如省略了 clobber/modify,不需要保留 input 后面的冒号。

assembly code:还是用户写入的汇编指令,和基本内联汇编一样。

汇编代码的运行是需要输入参数的,其运行之后也可产出结果。 input 和 output 正是 C 为汇编提供输入参数和存储其输出的部分,这是汇编与 C 交互的关键,我们之前的讨论就通过这两项解决。

output: 用来指定汇编代码的数据如何输出给 C 代码使用。内嵌的汇编指令运行结束后,如果想将运行结果存储到 C 变量中,就用此项指定输出的位置。 output 中每个操作数的格式为:"操作数修饰符约束名"(C变量名)
其中的引号和圆括号不能少,操作数修饰符通常为等号’=’‘。多个操作数之间用逗号’,'分隔。

input: 用来指定中数据如何输入给汇编使用。要想让汇编使用 C 中的变量作为参数,就要在此指定 。 input 中每个操作数的格式为:"[操作数修饰符]约束名"(C变量名)
其中的引号和圆括号不能少,操作数修饰符为可选项。多个操作数之间用逗号’,’分隔。

单独强调一下,以上的 output() 和 input() 括号中的是C代码中的变量, output(C变量) 和 input(C变量)就像C语言中的函数,将变量(值或变量地址)转换成汇编代码的操作数。

clobber/modify :汇编代码执行后会破坏一些内存或寄存器资源,通过此项通知编译器,可能造成寄存器或内存数据的破坏,这样 gcc 就知道哪些寄存器或内存需要提前保护起来,后面会展开细说。

assembly eode 中引用的所有操作数其实是经过 gcc 转换后的复本,“原件”都是在 output 和 input 括号中的 c 变量,


约束的作用域是 input 和 output 部分,咱们看看这些约束是怎么体现的,约束分为四大类。

  • 寄存器约束
    寄存器约束就是要求 gee 使用哪个寄存器,将 input 或 output 中变量约束在某个寄存器中。常见的寄
    存器约束有:
    a:表示寄存器 eax/ax/al
    b:表示寄存器 ebx/bx/bl
    c:表示寄存器 eex/ex/cl
    d:表示寄存器 edx/dx/dl
    D :表示寄存器 edi/di
    S :表示寄存器 esi/si
    q :表示任意这 4 个通用寄存器之-:eax/ebx/ecx/edx
    r :表示任意这 6 个通用寄存器之一: eax/ebx/ecx/edx/esi/edi
    g :表示可以存放到任意地点(寄存器和内存)。相当于除了同q一样外,还可以让 gcc 安排在内存中
    A :把 eax和 edx 组合成 64 位整数
    e :表示浮点寄存器
    t :表示第 1 个浮点寄存器
    u:表示第 2 个浮点寄存器

  • 内存约束
    内存约束是要求 gcc 直接将位于 input 和 output 中的C变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是 C 变量的指针。
    m :表示操作数可以使用任意一种内存形式。
    o :操作数为内存变量,但访问它是通过偏移量的形式访问,即包含 offset address 的格式。

  • 立即数约束
    立即数即常数,此约束要求 gcc 在传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码 。由于立即数不是变量,只能作为右值,所以只能放在 input 中。
    i:表示操作数为整数立即数
    F:表示操作数为浮点数立即数
    I :表示操作数为 0~ 31 之间的立即数
    J:表示操作数为 0~ 63 之间的立即数
    N:表示操作数为 0~255 之间的立即数
    O:表示操作数为 0~32 之间的立即数
    X:表示操作数为任何类型立即数

  • 通用约束
    0~ 9:此约束只用在 input 部分,但表示可与 output 和 input 中第 n 个操作数用相同的寄存器或内存 。

约束的作用是让 C 代码的操作数变成汇编代码能使用的操作数,所有的约束形式其实都是给汇编用的。故,约束是 C 语言中的操作数(变量或立即数〉与汇编代码中的操作数之间的映射,它告诉 gcc,同一个操作数在两种环境下如何变换身份,如何对接沟通。编译过程中 C 代码是要先变成汇编代码的,内联汇编中的约束就相当于 gcc 让咱们指定 C 中数据的编译形式。

在内联汇编中assembly eode 中用到的操作数,都是位于 output 和 input 中 C 操作数的副本,多数通过赋值的方式传给汇编代码,或者顶多是通过指针的形式,当操作数的副本在汇编中处理完成后,又重新赋值给 C 操作数。


对比基本内联汇编和扩展内联汇编

// 基本内联汇编
#include<stdio.h>
int in_a = 1, in_b = 2, out_sum;
void main() {
    asm (" pusha;       \
    movl in_a, %eax;    \
    movl in_b, %ebx;	\
    add! %ebx, %eax;	\
    movl %eax, out _sum; \
    popa");
    printf("sum is %d\n", out_sum);
 }
// 扩展内联汇编
#include<stdio.h>
void main() {
    int in_a = 1, in b = 2, out_sum;
    asm ("addl %%ebx, %%eax":"=a"(out_sum):"a"(in_a), "b"(in_b)); // "=a"意思是 out_sum=eax
    // 扩展内联汇编中寄存器前缀是两个%
    printf ("sum is %d\n", out_sum);
}

output 和 input 中用的约束都是同一个,这是否会存在顺序混乱、数据覆盖?其实只要清楚操作数(约束对应的寄存器或内存)被赋值的顺序就明白了,肯定永远是输入(input)中的汇编操作数优先被赋值,汇编代码经过运行,最后才是为输出(output)中的汇编操作数赋值。


#include<stdio.h>
void main() {
    int in_a = 1, in_b = 2;
    printf("in_b is %d\n", in_b);
    asm ("movb %b0, %1;" :: "a"(in_a), "m"(in_b)); // in_b中的立即数数值当作汇编指令的操作数
    printf ("in_b now is %d\n", in_b);
}

序号占位符是对在 output 和 input 中的操作数,按照它们从左到右出现的次序从 0 开始编号,一直到9,也就是说最多支持 10 个序号占位符。操作数用在 assembly code 中,引用它的格式是%0~9 。在操作数自身的序号前面加 1 个百分号’%’便是对相应操作数的引用。一定要切记,占位符指代约束所对应的操作数,也就是在汇编中的操作数,并不是圆括号中的 C 变量。

第 5 行对寄存器础的引用:%b0,这是用的 32 位数据的低 8 位,在这里就是指 al 寄存器。如果不显式加字符’b’,编译器也会按照低 8 位来处理,但它会发出警告。我们可以在%和序号之间插入字符h来表示操作数为由ah(第 8~ 15 位〉,或者插入字符b来表示操作数为 al(第 0~7 位〉。

asm ("addl %%ebx, %%eax":"=a"(out_sum): "a"(in_a), "b"(in_b));
//等价于
asm("addl %2, %1":"=a"(out_sum): "a"(in_a), "b"(in_b));
/*其中:
"=a"(out_sum)序号为0, %0 对应的是 eax。
"a"(in_a)序号为1, %1 对应的是 eax 。
"b"(in_b)序号为2, %2 对应的是 ebx 。*/

名称占位符与序号占位符不同,序号占位符靠本身出现在 output 和 input 中的位置就能被编译器辨识出来。而名称占位序需要在 output 和 input 中把操作数显式地起个名字,它用这样的格式来标识操作数:[名称]"约束名"(C 变量)
这样,该约束对应的汇编操作数便有了名字,在 assembly code 中引用操作数时,采用%[名称]的形式就可以了。

#include<stdio.h>
void main() {
    int in a = 18, in b = 3, out = 0;
    asm ("divb %[divisor];movb %%al, %[result]"\
        :[result]"=m"(out) \
        :"a"(in_a), [divisor]"m"(in_b) \
    );
    printf ("result is %d\n", out);
}

在约束中还有操作数类型修饰符,用来修饰所约束的操作数:内存、寄存器,分别在 output和 input
中有以下几种。
在 output 中有以下 3 种。

  1. =:表示操作数是只写,相当于为 output 括号中的 C 变量赋值,如=a(c_var),此修饰符相当于 c_var=eax 。
  2. +:表示操作数是可读写的,告诉 gcc 所约束的寄存器或内存先被读入,再被写入。
  3. &:表示此 output 中的操作数要独占所约束(分配)的寄存器,只供 output 使用,任何 input 中所分配的寄存器不能与此相同。注意,当表达式中有多个修饰符时,&要与约束名挨着,不能分隔。

在 input 中:
%:该操作数可以和下一个输入操作数互换。

一般情况下, input 中的 C 变量是只读的, output 中的 C 变量是只写的。
修饰符’=‘只用在 output 中,表示 C 变量是只写的,功能相当于 output 中的 C 变量=约束的汇编操作数,如"=a"(c_var),相当于 c_var=eax 的值。
修饰符’+'也只用在 output 中,但它具备读、写的属性,也就是它既可作为输入,同时也可以作为输出,所以省去了在 input 中声明约束。

#include <stdio.h>
void main(){
    int in_a = 1, in_b = 2 ;
    asm ("addl %%ebx, %%eax;": "+a"(in_a): "b"(in_b));
    printf ("in_a is %d\n", in_a);
}

修饰符’&'用来表示此寄存器只能分配给 output 中的某个 C 变量使用,不能再分给 input 中某变量了。函数在执行完成时,返回值会存储在寄存器 eax 中。通常我们会将返回值获取到 C 变量中再处理。如果是让 gcc 自由分配寄存器, gcc 有可能把 eax 分配出去另做他用,有可能的一种情况是先调用函数,函数返回后又执行了其他操作,这个操作中用到了 gcc 分配的 eax 寄存器,于是函数的返回值便被破坏

#include <stdio .h>
void main () {
    int ret_cnt = 0, test = 0;
    char* fmt = "hello,world\n"; //共 12 个字符
    asm ("pushl %1; \
        call printf; \    // 执行后eax值为12
        addl $4, %%esp; \
        movl $6, %2"\     // 这里实际gcc分配eax给%2,覆盖了原本要输出的12
        : "=a"(ret_cnt) \ // 要通过eax做输出
        : "m"(fmt), "r"(test) \
    ) ;
     printf ("the number of bytes written is %d\n", ret_cnt);
 }

使用’&'可以让gcc不去破坏要作为输出的寄存器

#include <stdio .h>
void main () {
    int ret_cnt = 0, test = 0;
    char* fmt = "hello,world\n"; //共 12 个字符
    asm ("pushl %1; \
        call printf; \    // 执行后eax值为12
        addl $4, %%esp; \
        movl $6, %2"\     // 这里实际gcc分配eax给%2,覆盖了原本要输出的12
        : "=&a"(ret_cnt) \ // 要通过eax做输出
        : "m"(fmt), "r"(test) \
    ) ;
     printf ("the number of bytes written is %d\n", ret_cnt);
 }

clobber/modify 部分,这部分用于通知 gcc ,我们修改了哪些寄存器或内存。

由于我们在 C 程序中嵌入了汇编代码,这必然会造成一些资源的破坏,本来嘛,人家 C 代码翻译后也要用到寄存器,突然来了一堆抢寄存器用的汇编指令,这肯定会使 gcc 重新为 C 代码安排寄存器等资源。为了解决资源冲突,我们得让 gcc 知道,我们改变了哪些寄存器或内存,这样 gcc 才能合理安排。

如果在 output 和 input 中通过寄存器约束指定了寄存器, gcc 必然会知道这些寄存器会被修改,所以,需要在 clobber/modify 中通知的寄存器肯定不是在 output 和 input 中出现过的 。

明显的改变 gcc 当然能扫描出来。可“暗处”的指令就无法保证了,比如在汇编中调用了一个函数,该函数内部会修改一些资源,或者该函数中又调用了其他函数,这保不准在哪一层调用有修改资源的代码,简直无法跟踪。所以必须要人为显式地告诉 gcc 我们动了哪些资源,这个资源就是寄存器和内存。

asm ("movl %%eax, %0; movl %%eax,%%ebx": "=m"(ret_value) :: "bx"); // 即使寄存器只变动一部分,它的整体也会全跟着受影响,所以只写bx就可以

如果我们的内联汇编代码修改了标志寄存器 eflag 中的标志位,同样需要在 clobber/modfyi命中用"cc"声明。

如果我们在 output 中使用了内存约束, gcc 自然会得到哪块内存被修改。但如果被修改的内容井未在output 中,我们就需要用” memory ” 告诉 gcc 啦 。

另外一个用” memory”声明的原因就是清除寄存器缓存。

内存相对寄存器来说还是比较慢的, gcc 为了提速,编译中有时会把内存中的数据缓存到寄存器,之后的处理都是直接读取寄存器。编译过程中编译器无法检测到内存的变化,只有编译出来的程序在实际运行中才会出现变量的值被改变,也就是出现了内存变化的情况。 编译器编译程序时,将变量的值缓存到寄存器。程序在运行时,当变量所在的内存有变化,寄存器中的缓存还是旧数据,运行结果肯定就错了。

我们就可以在内联汇编中的 clobber/modify 部分用” memory”声明,通知编译器变量所在的内存数据变啦,这样它就会从内存再读取一次新数据啦。


机器模式用来在机器层面上指定数据的大小及格式。

h -输出寄存器高位部分中的那一字节对应的寄存器名称,如 ah 、 bh 、 ch、dh。
b -输出寄存器中低部分 1 字节对应的名称,如 al 、 bl 、 cl 、 dl 。
w -输出寄存器中大小为 2 个宇节对应的部分,如 ax 、 bx 、 ex 、 dx 。
k -输出寄存器的四字节部分,如 eax 、 ebx 、 ecx, edx 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/910003.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Docker 的数据管理与镜像

Docker 的数据管理与镜像 一、Docker 的数据管理1.数据卷2.数据卷容器3.端口映射4.容器互联&#xff08;使用centos镜像&#xff09; 二、Docker 镜像的创建1.基于现有镜像创建2.基于本地模板创建3.基于Dockerfile 创建 三、Dockerfile 操作常用的指令&#xff1a;1.FROM 镜像2…

RTSP/Onvif流媒体服务器EasyNVR安防视频平台一直提示网络请求失败的问题解决方案

EasyNVR平台优秀的视频能力在于通过RTSP/ONVIF协议&#xff0c;将前端接入设备的音视频资源进行采集&#xff0c;并转码成适合全平台、全终端分发的视频流格式&#xff0c;包括RTMP、RTSP、FLV、HLS、WebRTC等格式。 有用户反馈&#xff0c;EasyNVR使用过程中&#xff0c;突然提…

飞书小程序开发

1.tt.showModal后跳转页面 跳转路径要为绝对路径&#xff0c;相对路径跳转无响应。 2.手机息屏后将不再进入onload()生命周期&#xff0c;直接进入onshow()生命周期。 onLoad()在页面初始化的时候触发&#xff0c;一个页面只调用一次。 onShow()在切入前台时就会触发&#x…

代码随想录算法训练营day41 | 343. 整数拆分,96. 不同的二叉搜索树

目录 343. 整数拆分 96. 不同的二叉搜索树 343. 整数拆分 类型&#xff1a;动态规划 难度&#xff1a;medium 思路&#xff1a; dp[i]所用的拆分方法至少已经拆分了两次&#xff0c;比如dp[2]1&#xff0c;小于2&#xff0c;在大于2的数中&#xff0c;最后的2是不会拆的。 …

大数据平台需要做等保测评吗?怎么做?

大数据时代的数据获取方式、存储规模、访问特点、关注重点都有了很大不同&#xff0c;所以保证大数据平台数据安全尤其重要。因此不少人在问&#xff0c;大数据平台需要做等保测评吗&#xff1f;怎么做&#xff1f; 大数据平台需要做等保测评吗&#xff1f; 大数据平台是需要做…

sheel

资料来源&#xff1a; 尚硅谷 2-sheel脚本入门 脚本以#!bin/bash 开头 3-变量 3-1自定义变量 在bash中&#xff0c;变量默认类型为字符串类型&#xff0c;无法直接进行数值运算 c12 echo $c 12 使用 export 可把变量提升为全局环境变量 export 变量名 3-2特殊变量 3-2-1 $n …

posstgresql多表连接

posstgresql多表连接 内连接左外连接右外连接总结 交叉连接九九乘法表 自然连接自连接 内连接 内连接用于返回两个表中匹配的数据行&#xff0c;使用关键字INNER JOIN表示&#xff0c;也可以简写成JOIN select -- 内连接 d.department_id ,e.first_name ,d.department_name…

星球转债上市价格预测

星球转债 基本信息 转债名称&#xff1a;星球转债&#xff0c;评级&#xff1a;A&#xff0c;发行规模&#xff1a;6.2亿元。 正股名称&#xff1a;星球石墨&#xff0c;今日收盘价&#xff1a;29.46元&#xff0c;转股价格&#xff1a;33.12元。 当前转股价值 转债面值 / 转股…

限制 el-input 输入 emoji

1. 电脑如何输入 emoji 表情 ? 快捷键 win; 或 win. 2. 代码实现 <template><el-input v-model"input" placeholder"请输入内容" input"inputChange"></el-input> </template><script> export default {name: D…

mysql数据库root密码遗忘后,修改root密码

目录 方式一&#xff1a; 方式二&#xff1a; 2.1 也可以像我这样&#xff0c;普通用户登录进去后 2.2 执行如下命令&#xff0c;将已知的user1的加密密文更新到root中 2.3 查询数据库 2.4 用root用户登录 2.5 登录正常&#xff0c;但这会root登录进去后&#xff0c;无法…

Java中异常的详细讲解与细节讨论

用一个代码引出异常为什么要使用异常 代码&#xff1a; public static void main(Sting args[]){int n1 1;int n2 0;System.out.println(n1/n2);//这里会抛ArihmaticException,因为除数不能为0,若未用异常处理机制则程序直接结束&#xff0c;后面的代码将不执行。这样很不好…

Java【SpringBoot】SpringBoot 和 Spring 有什么区别? SpringBoot有哪些优点?

文章目录 前言一、Spring 特点二、SpringBoot 特点和优点总结 前言 各位读者好, 我是小陈, 这是我的个人主页, 希望我的专栏能够帮助到你: &#x1f4d5; JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等 &#x1f4d7; Java数据结构: 顺序表, …

二级MySQL(四)——数据表的增删改查

首先认识数据类型&#xff1a; VERCHAR&#xff08;n&#xff09;最长长度为n的&#xff0c;可变长度的&#xff0c;字符串类型 CHAR&#xff08;n&#xff09;固定长度的字符串类型 TIME&#xff1a;时间内类型 DTAE&#xff1a;日期类型 INT&#xff1a;普通大小的整数 …

svg图片如何渲染到页面,以及svg文件的上传

svg图片渲染到页面的几种方式 背景&#x1f7e1;require.context获取目录下的所有文件&#x1f7e1;方式1: 直接在html中渲染&#x1f7e1;方式: 发起ajax请求&#xff0c;获取SVG文件 背景 需要实现从本地目录下去获取所有的svg图标进行预览&#xff0c;将选中的图片显示在另…

合并jar包导致gradle传递依赖失效

目录 零、背景一、合并jar包1.1、自定义一组jar包1.2、自定义合并jar的任务1.3、定义打包jar的任务 二、发布jar包2.1、未合并jar包之前的合并方式2.2、合并jar包之后的合并方式 三、发现问题3.1、确定gradle中的依赖关系3.2、对比maven是否缺失依赖3.3、对比合并前后的pom.xml…

【生态经济学】利用R语言进行经济学研究技术——从数据的收集与清洗、综合建模评价、数据的分析与可视化、因果推断等方面入手

查看原文>>>如何快速掌握利用R语言进行经济学研究技术——从数据的收集与清洗、综合建模评价、数据的分析与可视化、因果推断等方面入手 近年来&#xff0c;人工智能领域已经取得突破性进展&#xff0c;对经济社会各个领域都产生了重大影响&#xff0c;结合了统计学、…

Midjourney API 国内申请及对接方式

在人工智能绘图领域&#xff0c;想必大家听说过 Midjourney 的大名吧&#xff01; Midjourney 以其出色的绘图能力在业界独树一帜。无需过多复杂的操作&#xff0c;只要简单输入绘图指令&#xff0c;这个神奇的工具就能在瞬间为我们呈现出对应的图像。无论是任何物体还是任何风…

7-10 查验身份证

分数 15 全屏浏览题目 切换布局 作者 陈越 单位 浙江大学 一个合法的身份证号码由17位地区、日期编号和顺序编号加1位校验码组成。校验码的计算规则如下&#xff1a; 首先对前17位数字加权求和&#xff0c;权重分配为&#xff1a;{7&#xff0c;9&#xff0c;10&#xff0c…

No mapping found for HTTP request with URI

参考: 参考地址 说明 ssm老项目,接过来别人的项目 临时建了一个Controller方便测试用的,结果访问掉不通,报: No mapping found for HTTP request with URIxxxx 这样的错误 解决办法 看了下web,xml配置 在 webmvc-config.xml 配置文件里面添加了几行配置 说明: com.iph.h…

如何使用 JavaScript Promise – 回调、异步等待和 Promise 方法解释

在本教程中,您将学习有关在 JavaScript 中使用 Promise 和 async/await 所需了解的所有内容。 那么让我们开始吧。 为什么在 JavaScript 中使用 Promise? ES6 引入了 Promise 作为原生实现。在 ES6 之前,我们使用回调来处理异步操作。 让我们了解什么是回调以及 Promise…