Рассмотрим следующую простую программу:
#include <cstring>
#include <cstdio>
#include <cstdlib>
void replace(char *str, size_t len) {
for (size_t i = 0; i < len; i++) {
if (str[i] == '/') {
str[i] = '_';
}
}
}
const char *global_str = "the quick brown fox jumps over the lazy dog";
int main(int argc, char **argv) {
const char *str = argc > 1 ? argv[1] : global_str;
replace(const_cast<char *>(str), std::strlen(str));
puts(str);
return EXIT_SUCCESS;
}
Он берет (необязательную) строку в командной строке и печатает ее, символы /
заменяются на _
. Эта функциональность замены реализована функцией c_repl
1. Например, a.out foo/bar
печатает:
foo_bar
Элементарные вещи до сих пор, верно?
Если вы не указываете строку, она удобно использует глобальную строку: быстрый коричневый лис перепрыгивает через ленивую собаку, которая не содержит символов /
, и поэтому не подвергается какой-либо замене.
Конечно, строковые константы являются const char[]
, поэтому мне нужно сначала отбросить константу - которую вы видите const_cast
. Поскольку строка никогда не изменяется, у меня сложилось впечатление, что это законно.
gcc и clang компилируют двоичный файл с ожидаемым поведением, с передачей или без передачи строки в командной строке. ICC аварийно завершает работу, когда вы не предоставляете строку, однако:
icc -xcore-avx2 char_replace.cpp && ./a.out
Segmentation fault (core dumped)
Основной причиной является основной цикл для c_repl
который выглядит следующим образом:
400c0c: vmovdqu ymm2,YMMWORD PTR [rsi]
400c10: add rbx,0x20
400c14: vpcmpeqb ymm3,ymm0,ymm2
400c18: vpblendvb ymm4,ymm2,ymm1,ymm3
400c1e: vmovdqu YMMWORD PTR [rsi],ymm4
400c22: add rsi,0x20
400c26: cmp rbx,rcx
400c29: jb 400c0c <main+0xfc>
Это векторизованный цикл. Основная идея заключается в том, что 32 байта загружаются, а затем сравниваются с символом /
, образуя значение маски с байтом, установленным для каждого совпадающего байта, а затем существующая строка смешивается с вектором, содержащим 32 символа _
, эффективно заменяя только символы /
Наконец, обновленный регистр записывается обратно в строку с vmovdqu YMMWORD PTR [rsi],ymm4
.
В этом последнем хранилище происходит сбой, поскольку строка .rodata
только для чтения и размещается в разделе двоичного .rodata
, который загружается с использованием страниц только для чтения. Конечно, хранилище было логичным "без операции", записывая обратно те же символы, которые оно прочитало, но процессору все равно!
Законен ли мой код C++, и поэтому я должен винить icc за его неправильную компиляцию, или я куда-то вхожу в болото UB?
1 Тот же сбой из-за той же проблемы происходит с std::replace
для std::string
а не с моим "C-подобным" кодом, но я хотел максимально упростить анализ и сделать его полностью автономным.