我与一位专家进行了接触,他据称拥有比我更高的编码技能,他对内联汇编的理解比我更好。
其中一个声明是,只要操作数作为输入约束出现,您就不需要将其列为clobber或指定寄存器已被内联程序集潜在地修改。当其他人试图获得有关memset
实现的帮助时,对话发生了,该实现是以以下方式有效编码的:
void *memset(void *dest, int value, size_t count)
{
asm volatile ("cld; rep stosb" :: "D"(dest), "c"(count), "a"(value));
return dest;
}
当我在没有告诉编译器的情况下评论这个问题时,专家的说法是告诉我们:
“c”(计数)已经告诉编译器c被重击了
我在专家自己的操作系统中找到了一个例子,他们用相同的设计模式编写类似的代码。它们使用Intel语法进行内联程序集。这个hobby操作系统代码在内核(ring0)上下文中操作。一个例子是缓冲区交换函数1:
void swap_vbufs(void) {
asm volatile (
"1: "
"lodsd;"
"cmp eax, dword ptr ds:[rbx];"
"jne 2f;"
"add rdi, 4;"
"jmp 3f;"
"2: "
"stosd;"
"3: "
"add rbx, 4;"
"dec rcx;"
"jnz 1b;"
:
: "S" (antibuffer0),
"D" (framebuffer),
"b" (antibuffer1),
"c" ((vbe_pitch / sizeof(uint32_t)) * vbe_height)
: "rax"
);
return;
}
volatile uint32_t *framebuffer;
volatile uint32_t *antibuffer0;
volatile uint32_t *antibuffer1;
int vbe_width = 1024;
int vbe_height = 768;
int vbe_pitch;
"add rbx, 4;"
"dec rcx;"
这些寄存器都不列为输入/输出,也不列为输出操作数。我相信需要修改这些约束,以通知编译器这些寄存器可能已经被修改/clobbread了。唯一一个我认为是正确的注册表是Rax。我的理解正确吗?我的感觉是RDI、RSI、RBX和RCX应该是输入/输出约束(使用+
修饰符)。即使有人试图辩称64位System V ABI调用约定将保存它们(假设编写此类代码的方式不佳),RBX是一个非易失性寄存器,在此代码中会发生变化。
由于地址是通过寄存器传递的(而不是内存约束),我认为编译器没有被告知这些指针所指向的内存已经被读取和/或修改是一个潜在的bug。我的理解正确吗?
RBX和RCX是硬编码寄存器。允许编译器通过约束自动选择这些寄存器不是很有意义吗?
如果假设在这里必须使用内联程序集(假设),对于这个函数,无bug的GCC内联程序集代码会是什么样子?这个函数是不是很好,而我只是不像专家那样理解GCC的扩展内联程序集的基础知识?
swap_vbufs
函数和相关变量声明被逐字复制,而没有版权持有者根据合理使用的许可,用于对大量作品的注释。你在所有方面都是正确的,这段代码对编译器来说充满了谎言,可能会咬你一口。例如使用不同的周围代码或不同的编译器版本/选项(特别是链接时优化以启用跨文件内联)。
swap_vbufs
看起来甚至不是很高效,我怀疑gcc在纯C版本中会做得同样或者更好。https://gcc.gnu.org/wiki/dontuseinlineasm。stosd
在Intel上是3个UOP,比常规的mov
-store+add rdi,4
差。使添加rdi,4
无条件将避免对else
块的需要,该块将在(希望)快速路径上放置一个额外的JMP
,因为在快速路径上没有MMIO存储到视频RAM中,因为缓冲区是相等的。
(lodsd
在Haswell和更新版本上只有2个UOP,所以如果您不关心IvyBridge或更旧版本,这是可以的)。
在内核代码中,我猜他们在回避SSE2,即使它是x86-64的基线,否则您可能会想要使用它。对于正常的内存目标,您只需使用rep movsd
或ERMSBrep movsb
来memcpy
即可,但我想这里的重点是尽可能通过检查视频RAM的缓存副本来避免MMIO存储。不过,使用movnti
的无条件流存储可能是有效的,除非视频RAM映射为UC(uncacheable)而不是WC。
很容易构造出在实际操作中确实中断的示例,例如,在同一函数中的内联asm语句之后再次使用相关的C变量。(或在内联asm的父函数中)。
要销毁的输入通常必须使用匹配的虚拟输出或带有C tmp var的RMW输出来处理,而不仅仅是“r”
。或“a”
。
“r”
或任何特定寄存器约束(如“d”
)意味着这是一个只读输入,编译器可以期望在之后不受干扰地找到该值。没有“输入我要破坏”的约束;你必须用一个虚拟输出或变量来合成它。
这都适用于支持GNU C内联asm语法的其他编译器(clang和ICC)。
摘自GCC手册:ExtendedASM
输入操作数:
(RAX
clobber会导致使用“a”
作为输入出错;clobber和操作数不能重叠。)
int plain_C(int in) { return (in+1) + in; }
// buggy: modifies an input read-only operand
int bad_asm(int in) {
int out;
asm ("inc %%edi;\n\t mov %%edi, %0" : "=a"(out) : [in]"D"(in) );
return out + in;
}
在Godbolt编译器资源管理器上编译
请注意,GCC的addl
将edi
用于in
,尽管内联asm将该寄存器用作输入。(因此中断,因为这个错误的内联asm修改了寄存器)。在本例中,它恰好保持in+1
。我使用了GCC9.1,但这不是新的行为。
## gcc9.1 -O3 -fverbose-asm
bad(int):
inc %edi;
mov %edi, %eax # out (comment mentions out because I used %0)
addl %edi, %eax # in, tmp86
ret
int safe(int in) {
int out;
int dummy;
asm ("inc %%edi;\n\t mov %%edi, %%eax"
: "=a"(out),
"=&D"(dummy)
: [in]"1"(in) // matching constraint, or "D" works.
);
return out + in;
}
# gcc9.1 again.
safe_asm(int):
movl %edi, %edx # tmp89, in compiler-generated save of in
# start inline asm
inc %edi;
mov %edi, %eax
# end inline asm
addl %edx, %eax # in, tmp88
ret
如果函数没有内联,也没有在asm语句之后使用输入变量,那么只要它是一个被调用破坏的寄存器,就可以逃过编译器的欺骗。
经常会发现有些人编写了不安全的代码,而这些代码恰好在他们使用的上下文中工作。他们也很少相信,在那种情况下,只需用一个编译器版本/选项来测试它就足以验证它的安全性或正确性。
但这不是asm的工作方式;编译器相信您能够准确地描述ASM的行为,并简单地对模板部分进行文本替换。
这也是正确的。寄存器输入操作数并不意味着指向的存储器也是输入操作数。在一个不能内联的函数中,这实际上并不能引起问题,但只要您启用链接时优化,跨文件内联和过程间优化就成为可能。
有一个已存在的通知性冲突,即内联程序集读取某个特定的内存区域未回答的问题。这个Godbolt链接展示了一些你可以揭示这个问题的方法,例如。
arr[2] = 1;
asm(...);
arr[2] = 0;
如果gcc假设arr[2]
不是asm的输入,而是arr
地址本身,它将执行死区消除并删除=1
赋值。(或者将其视为使用asm语句重新排序存储,然后将2个存储折叠到相同位置)。
数组很好,因为它表明即使“m”(*arr)
对指针也不起作用,只对实际数组起作用。输入操作数只会告诉编译器arr[0]
是输入,而不是arr[2]
。如果这是您的asm所读取的全部内容,那么这是一件好事,因为它不会阻碍其他部分的优化。
对于memset
示例,要正确声明指向的内存是输出操作数,请将指针强制转换为指向数组的指针并取消引用,告诉gcc整个内存范围都是操作数。*(char(*)[count])指针
。(您可以将[]
保留为空,以指定通过此指针访问的任意长度的内存区域。)
// correct version written by @MichaelPetch.
void *memset(void *dest, int value, size_t count)
{
void *tmp = dest;
asm ("rep stosb # mem output is %2"
: "+D"(tmp), "+c"(count), // tell the compiler we modify the regs
"=m"(*(char (*)[count])tmp) // dummy memory output
: "a"(value) // EAX actually is read-only
: // no clobbers
);
return dest;
}
包括一个使用虚拟操作数的asm注释,让我们看到编译器是如何分配它的。我们可以看到编译器使用AT&T语法选择(%RDI)
,因此它愿意使用一个寄存器,它也是一个输入/输出操作数。
对于不返回指针的void
函数(或者在内联到不使用返回值的函数中之后),在让rep stosb
销毁指针参数之前,它不必在任何地方复制指针参数。
我正试图使Filepond工作,但CSS中的这一行似乎破坏了它-在ul选择器中。 我试着对页面的整个部分进行核化,直到Filepond起作用,将目标锁定在css上,最后在ul{}中找到前面提到的行。我可以把其他的东西都抹掉,只留下那条线,而文件孔仍然坏了,所以我肯定这是问题所在,但我不知道是怎么回事。 我尝试使用Chrome的检查器功能查看运行时页面源代码,但在那里找不到溢出。 然后我使用Note
我的单元测试设置如下: 进口似乎是个问题,因为我试图使用桶: 在index.ts文件中,我正在执行以下操作:
这里是一个虚拟的*z++=*x++**y++指令。请注意,x、y和z指针寄存器必须指定为输入/输出,因为asm会修改它们。 在第一个示例中,在输入操作数中列出和有什么意义?同一份文件指出: 特别是,如果不将输入操作数指定为输出操作数,就无法指定输入操作数被修改。
但我得到的输出是:
我有一个简单的积垢项目称为过滤器。在这里,每个过滤器都被分配给一个带有外键的类别。我试图做的是循环遍历每个foregin键,以获得类别名称,而不是显示给用户的id。 我首先获取所有过滤器,并执行模具/转储以检查所有结果是否存在。 尝试将类别名称分配给正确的数组项时,出现以下错误: msgstr"间接修改App\Filter的重载元素没有效果" 所以为了检查发生了什么,我在foreach循环中死了/
我正在构建一个ANTLR4语法来解析数据源中的字符串--类似于StringTemplate(如果不是非常相似的话),只是我不喜欢这种语法,所以我正在编写自己的语法(也只是为了好玩和学习,因为这是我第一次使用/ANTLR)。我的语法现在看起来像这样(这是从我的实际情况中简化出来的,但我已经验证了它是一个“好例子”,并且展示了我所问的相同的问题): 这个语法工作得很好,允许我执行替换,例如: 结果是: