Я же писал: это значит, что memcpy (как и прочие функции) не обязана делать явных проверок своих аргументов. Если попросить memcpy скопировать один байт из одного мусорного указателя в другой, она может упасть (в случае NULL или совсем невалидных указателей) или действительно скопировать один байт, последствия чего в случае некорректного dst-указателя непредсказуемы.
n=0 — корректное значение длины, а ваша версия memcpy на нём сломается даже при корректных значениях указателей. Попробуйте ещё раз.
Более того, указатель на конец массива — это валидный указатель, поэтому memcpy вообще не имеет права разыменовывать переданные ей указатели при n=0.
Код типа
class MyContainer {
// если контейнер пуст, то указатель на данные держим нулевым
size_t Length = 0;
int* Data = nullptr;
...
void Append(const MyContainer& other) {
Realloc(Length + other.Length);
memcpy(Data + Length, other.Data, other.Length * sizeof(int));
Length += other.Length;
}
...
}
довольно типичен. Он работает на всех платформах, он не был сломан до того, как в gcc закоммитили изменение, его не может сломать корректная реализация memcpy, он не сломан при использовании компилятора, думающего о программистах, типа VC2015.
UB — это не какие-то законы природы, данные нам свыше. UB означает всего лишь «конструкции, которые стандарт пометил словами undefined behaviour». Часть из них действительно отражают то, как устроен компьютерный мир — в неинициализированной переменной может оказаться всё, что угодно, а если она размещена в регистре, то и Itanium-ный Not-a-Thing с исключением при чтении; аналогично при выходе за границы массива. Часть из них отражают законы физики Марса, и джинна выпустили из бутылки, когда фразу «это UB», после которой забыли написать «потому что на Марсе вот так» (signed overflow на системах со странными представлениями отрицательных чисел), начали интерпретировать «поэтому на Земле мы вам делать так тоже запретим» («ну если ваш код вдруг попадёт на Марс, он же уже сломан!»). А часть — просто неудачные формулировки, и интерпретировать фразу со смыслом «memcpy не обязана делать явных проверок своих аргументов на NULL» как «memcpy нельзя передавать NULL даже при копировании нуля байт» — расписка в бессилии сделать что-то приличными средствами.
Я писал конкретно про placement new и автоматический вызов деструктора. Это всё-таки пример, заворачивание в какую-нибудь параметризованную конструкцию дало бы лишь на одну конструкцию, в которую нужно вникать при чтении, больше.
Если линковщик видит два файла, в которых определена одна и та же функция, то действительно будет ошибка линковки. Фокус в том, чтобы линковщик не пытался смотреть на второй из них.
Выбор файлов для линковки схематично выглядит так:
— берём все obj-файлы, явно заданные в командной строке (при компиляции из IDE это файлы главного проекта, но не файлы в проектах-зависимостях);
— пока есть неразрешённые внешние символы, ищем их в lib-библиотеках и добавляем obj-файлы из lib-библиотек.
При нормальной компиляции линковщик ищет точку входа, находит её в libcmt.lib:exe_main.obj, видит зависимость от __telemetry_main_invoke_trigger и прочих, находит их в libvcruntime.lib:telemetry.obj, подключает telemetry.obj.
Если же явно реализовать функции в своём коде, то, увидев зависимость от __telemetry_main_invoke_trigger и прочих, линковщик обнаружит, что он уже знает реализации этих функций и в telemetry.obj просто не полезет.
Нюанс 1: должны быть реализованы действительно все функции из telemetry.obj, которые может вызвать внешний код. Если, условно, в следующем обновлении функция printf будет вызывать какой-нибудь __telemetry_printf_trigger, получится ошибка линковки из-за двойного определения.
Нюанс 2: если явная реализация находится во вспомогательной библиотеке, то какая именно реализация подхватится, может зависеть от порядка перечисления библиотек в командной строке. Причём навскидку я не в курсе, в каком порядке обрабатываются встроенные библиотеки и считается ли libvcruntime.lib встроенной.
У меня в анамнезе слишком много копания в коде, чтобы думать о clean-room, и слишком много программирования, чтобы руками заполнять таблицу, которую может сделать скриптик на десяток строчек кода. Но ваш подход тоже заслуживает внимания.
Тут выше назвали одну конкретную игру, Kami no Rhapsody, это оттуда. Движок версии 4.46.
Вы меня заинтриговали, скачал, посмотрел. А что там принципиально сложного? Ну, помимо очевидного замечания, что при количестве команд в несколько сотен выяснение всех деталей того, что делает каждая, потребует некоторой усидчивости?
Таблицы смещений в скриптах в количестве трёх штук — просто адреса команд с опкодами 0x71, 3, 0x8F. Первая — список возможных сообщений для хранения флагов прочитанности/непрочитанности, вторая — список возможных внешних вызовов из скрипта, третья — список возможных вызовов процедур. Подозреваю, что так сделано для большей стабильности сейвов: когда в сейвах стек вызовов хранится как массив индексов в отдельной таблице, а не прямо адресами внутри файла, это не будет плыть при каждом изменении скриптов.
Для правки текстов кучу смещений изменять не нужно, достаточно дописывать исправленные тексты в конец файла и править только одно смещение — собственно адрес текста.
Примитивный дизассемблер
from __future__ import print_function
import io, sys, struct
#table generated automatically, minor mistakes are possible
cmdsizes = (
0,1,1,3,3,1,5,3,3,1,5,23,1,9,25,3, #0x0
9,19,3,9,1,11,5,5,0,0,0,0,0,0,17,25, #0x10
13,5,5,5,5,7,9,9,9,0,9,11,11,25,11,9, #0x20
11,9,21,13,25,23,7,23,25,0,0,0,0,0,0,0, #0x30
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x40
7,7,7,7,7,5,7,7,7,7,7,7,7,7,7,7, #0x50
5,7,7,5,5,5,7,7,7,7,7,7,5,1,5,3, #0x60
11,3,3,21,3,3,3,3,3,7,7,5,1,5,3,3, #0x70
3,3,11,7,3,1,3,1,3,9,13,3,3,5,3,3, #0x80
15,3,5,1,1,5,1,11,0,0,0,0,0,0,0,0, #0x90
7,1,5,5,0,0,0,0,0,0,5,5,19,1,1,1, #0xa0
3,3,5,1,5,3,3,3,1,3,3,3,3,3,3,3, #0xb0
3,1,5,3,3,5,5,5,3,1,1,3,5,1,7,1, #0xc0
3,1,3,1,9,3,13,3,5,1,13,0,0,0,0,0, #0xd0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0xe0
0,0,0,0,0,0,0,0,0,0,1,5,1,5,3,1, #0xf0
1,1,7,3,1,3,3,5,3,5,5,5,5,3,5,3, #0x100
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x110
0,0,0,0,0,0,0,0,0,0,0,0,11,15,17,9, #0x120
3,3,3,5,7,5,5,3,5,7,13,15,3,7,5,7, #0x130
9,3,3,1,5,3,3,13,3,3,15,3,5,13,0,0, #0x140
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x150
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x160
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x170
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x180
5,5,5,7,7,7,7,3,7,1,3,1,1,5,5,5, #0x190
19,5,3,3,5,3,5,3,1,3,3,5,7,1,7,7, #0x1a0
7,3,3,1,1,3,3,3,5,5,5,3,1,3,5,1, #0x1b0
3,7,5,5,3,9,5,3,5,7,3,3,3,5,3,3, #0x1c0
7,11,5,11,9,1,5,5,7,5,0,0,0,0,0,0, #0x1d0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x1e0
0,0,0,0,1,1,1,5,9,7,3,17,3,9,11,9, #0x1f0
3,3,11,9,9,13,15,17,7,11,3,15,1,3,1,7, #0x200
3,3,5,7,5,5,5,9,9,9,9,3,1,5,13,15, #0x210
13,9,5,17,1,5,11,13,11,11,7,9,7,11,13,11, #0x220
3,9,9,11,11,11,9,5,3,13,5,15,1,1,5,5, #0x230
9,11,5,1,1,5,5,3,3,7,7,5,5,25,3,21, #0x240
21,25,3,5,11,13,11,11,5,1,3,3,17,7,11,9, #0x250
9,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x260
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x270
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x280
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x290
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x2a0
0,0,0,0,0,0,0,0,0,0,0,0,23,3,3,7, #0x2b0
7,3,13,5,1,5,5,9,9,7,3,3,3,3,3,3, #0x2c0
7,7,7,7,7,5,5,5,7,5,17,3,3,5,5,7, #0x2d0
7,7,7,7,7,3,5,5,3,3,3,3,5,3,3,23, #0x2e0
19,15,13,13,7,9,3,3,5,15,3,3,11,13,0,0, #0x2f0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x300
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x310
21,7,9,11,1,5,9,3,7,5,3,1,13,5,23,3, #0x320
5,9,9,11,3,9,11,9,13,15,13,9,5,7,11,7 #0x330
)
# list created manually, may be incomplete
# (cmd in oplabels[i]) == (i-th operand of cmd is an offset)
oplabels = (
(0x7B,0x8C,0x8F,0xD5),
(0x7B,0x8D,0x92,0x95,0xA0,0xA2,0xA3,0xCC,0xCE,0xFB),
(0xA0,0xCE,0xD4,0x102),
(0xD4,),
(0xD6,0x90),
(0xD6,0x90),
(0x90,),
)
with io.open(sys.argv[1], 'rb') as f:
header = struct.unpack('<4s4s6I', f.read(0x20))
if header[0] != b'SYS3' and header[0] != b'SYS4':
print("invalid signature")
sys.exit(1)
header2size = struct.unpack('<I', f.read(4))[0]
header2 = struct.unpack('<' + str(header2size // 4 - 1) + 'I', f.read(header2size - 4))
data = f.read()
# pass 1: prepare labels
pos = 0
maxAddr = 0
labels = set()
while pos < len(data) // 4:
cmd = struct.unpack_from('<I', data, pos * 4)[0]
if cmd >= len(cmdsizes) or cmdsizes[cmd] == 0:
break
for i in xrange((cmdsizes[cmd] - 1) // 2):
optype, operand = struct.unpack_from('<ii', data, (pos + 1 + 2 * i) * 4)
if i < len(oplabels) and cmd in oplabels[i] and optype == 0 and operand >= 0:
labels.add(operand)
maxAddr = max(maxAddr, operand)
pos += cmdsizes[cmd]
if cmd in (1,2,5) and pos > maxAddr:
break
# pass 2: print
print("\theader '%s', %d, %d, %d, %d, %d, %d" % header[1:])
pos = 0
unprintedLabels = labels
firstStringOffs, lastStringOffs = -1, -1
firstArrayOffs, lastArrayOffs = -1, -1
messageStarts = []
externalCalls = []
internalCalls = []
while pos < len(data) // 4:
if pos in labels:
print("loc_%08X:" % pos)
unprintedLabels.remove(pos)
cmd = struct.unpack_from('<I', data, pos * 4)[0]
if cmd >= len(cmdsizes) or cmdsizes[cmd] == 0:
print("\t[invalid command 0x%X]" % cmd)
break
if cmd == 0x71:
messageStarts.append(pos)
if cmd == 3:
externalCalls.append(pos)
if cmd == 0x8F:
internalCalls.append(pos)
cmdsize = cmdsizes[cmd]
cmdargs = struct.unpack_from('<' + str(cmdsize - 1) + 'i', data, (pos + 1) * 4)
print("\tcmd%03X" % cmd, end='')
for i in xrange((cmdsize - 1) // 2):
if i:
print(",", end='')
print(" ", end='')
if cmdargs[i * 2] == 0:
if i < len(oplabels) and cmd in oplabels[i] and cmdargs[i * 2 + 1] >= 0:
print("loc_%08X" % cmdargs[i * 2 + 1], end='')
elif cmd == 0x64 and i == 1:
# second arg of cmd064 is offset of array in data
offs = cmdargs[i * 2 + 1]
if lastArrayOffs == -1:
firstArrayOffs = offs
elif lastArrayOffs != offs:
print("[Warning: out-of-order array]");
arrsize = struct.unpack_from('<I', data, offs * 4)[0]
arr = struct.unpack_from('<' + str(arrsize) + 'i', data, (offs + 1) * 4)
lastArrayOffs = offs + 1 + arrsize
print('<', end='')
for j in xrange(arrsize):
if j:
print(",", end='')
print('%d' % arr[j], end='')
print('>', end='')
else:
print('%d' % cmdargs[i * 2 + 1], end='')
elif cmdargs[i * 2] == 2:
offs = cmdargs[i * 2 + 1] * 4
if lastStringOffs == -1:
firstStringOffs = offs // 4
elif lastStringOffs != offs // 4:
print("[Warning: out-of-order string: %08X instead of %08X]" % (offs // 4, lastStringOffs), end='');
decoded = b''
while True:
s = ord(data[offs]) ^ 0xFF
if s == 0:
break
decoded += chr(s)
offs += 1
lastStringOffs = (offs + 1) // 4 + 1
print(b'"' + decoded + b'"', end='')
else:
print('op%X[0x%X]' % (cmdargs[i * 2], cmdargs[i * 2 + 1]), end='')
print()
pos += cmdsize
# command 5 can be a normal command with size=1 or nofollow-command depending on ???
# commands 4,9,0x7C are actually nofollow, but codegen seems to treat them as normal ones
if cmd in (1,2,5) and pos > maxAddr:
break
if len(unprintedLabels):
print("Warning: not all labels were printed");
expectedEndAddr = min(len(data) // 4, header2[1], header2[3], header2[5])
if firstArrayOffs != -1:
if lastArrayOffs != expectedEndAddr:
print("Warning: range [%08X,%08X) was not printed" % (lastArrayOffs, expectedEndAddr))
expectedEndAddr = min(expectedEndAddr, firstArrayOffs)
if firstStringOffs != -1:
if lastStringOffs != expectedEndAddr:
print("Warning: range [%08X,%08X) was not printed" % (lastStringOffs, expectedEndAddr))
expectedEndAddr = min(expectedEndAddr, firstStringOffs)
if pos != expectedEndAddr:
print("Warning: range [%08X,%08X) was not printed" % (pos, expectedEndAddr))
if messageStarts != list(struct.unpack_from('<' + str(header2[0]) + 'i', data, header2[1] * 4)):
print("Warning: unexpected messageStarts array")
if externalCalls != list(struct.unpack_from('<' + str(header2[2]) + 'i', data, header2[3] * 4)):
print("Warning: unexpected externalCalls array")
if internalCalls != list(struct.unpack_from('<' + str(header2[4]) + 'i', data, header2[5] * 4)):
print("Warning: unexpected internalCalls array")
В принципе, при некотором желании выдачу дизассемблера выше можно даже скомпилировать назад с помощью fasmg, если пошаманить с его макросами.
AMD Phenom II P820 @1.80GHz
Visual C++ 2015, /Ox
Последовательная версия:
sign: 4.81 vs 5.14
abs: 3.61 vs 2.40
mini: 2.40 vs 12.01
maxi: 2.40 vs 12.00
minu: 2.40 vs 12.14
maxu: 2.53 vs 12.73
Хаотическая версия:
sign: 20.19 vs -0.00
abs: 18.07 vs 0.00
mini: 0.00 vs 9.60
maxi: -0.00 vs 12.01
minu: 0.00 vs 12.01
maxu: 0.04 vs 12.01
g++ 5.2.0, -std=c++11 -O3
Последовательная версия:
sign: 9.72 vs 9.59
abs: 7.20 vs 7.20
mini: 7.20 vs 14.50
maxi: 7.19 vs 9.88
minu: 7.20 vs 7.20
maxu: 7.20 vs 9.59
Хаотическая версия:
sign: 17.34 vs 2.56
abs: 16.28 vs 0.02
mini: -0.00 vs 0.00
maxi: 0.00 vs 3.61
minu: 20.99 vs 8.91
maxu: -0.00 vs 3.61
Но, надо отметить, определение empty в последовательной версии некорректно: что gcc, что clang успешно сворачивают цикл с empty в константу. gcc, кстати, в неправильную константу: https://godbolt.org/g/wibi2q.
Извините, ЧТО?
Я бы предположил, что вы спутали код загрузчика приложений (ntdll.dll/ld-linux.so) и код рантайма конкретной среды программирования, но код загрузчика приложений тоже не имеет никакого отношения к планировщику.
И не соблаговолит ли прилежный слушатель курса операционных систем и системного программирования просветить невежественную шпану типа меня, каким образом прослушивание такого курса избавляет от вопроса «что делает вызов __telemetry_main_invoke_trigger() непосредственно перед вызовом main()»?
Даже если мы линкуем рантайм статически, мы не можем знать, как поведёт себя логирование и телеметрия.
hello.exe из статьи — вполне готовый бинарник, на его месте может быть любая собранная программа. А logman/tracerpt могут быть запущены совершенно независимо от него и от ведома разработчика hello.exe. В том числе апдейтом Windows. В том числе не трогающем vcruntime140.dll или как она там называется.
Более того, указатель на конец массива — это валидный указатель, поэтому memcpy вообще не имеет права разыменовывать переданные ей указатели при n=0.
Код типа
довольно типичен. Он работает на всех платформах, он не был сломан до того, как в gcc закоммитили изменение, его не может сломать корректная реализация memcpy, он не сломан при использовании компилятора, думающего о программистах, типа VC2015.
UB — это не какие-то законы природы, данные нам свыше. UB означает всего лишь «конструкции, которые стандарт пометил словами undefined behaviour». Часть из них действительно отражают то, как устроен компьютерный мир — в неинициализированной переменной может оказаться всё, что угодно, а если она размещена в регистре, то и Itanium-ный Not-a-Thing с исключением при чтении; аналогично при выходе за границы массива. Часть из них отражают законы физики Марса, и джинна выпустили из бутылки, когда фразу «это UB», после которой забыли написать «потому что на Марсе вот так» (signed overflow на системах со странными представлениями отрицательных чисел), начали интерпретировать «поэтому на Земле мы вам делать так тоже запретим» («ну если ваш код вдруг попадёт на Марс, он же уже сломан!»). А часть — просто неудачные формулировки, и интерпретировать фразу со смыслом «memcpy не обязана делать явных проверок своих аргументов на NULL» как «memcpy нельзя передавать NULL даже при копировании нуля байт» — расписка в бессилии сделать что-то приличными средствами.
Выбор файлов для линковки схематично выглядит так:
— берём все obj-файлы, явно заданные в командной строке (при компиляции из IDE это файлы главного проекта, но не файлы в проектах-зависимостях);
— пока есть неразрешённые внешние символы, ищем их в lib-библиотеках и добавляем obj-файлы из lib-библиотек.
При нормальной компиляции линковщик ищет точку входа, находит её в libcmt.lib:exe_main.obj, видит зависимость от __telemetry_main_invoke_trigger и прочих, находит их в libvcruntime.lib:telemetry.obj, подключает telemetry.obj.
Если же явно реализовать функции в своём коде, то, увидев зависимость от __telemetry_main_invoke_trigger и прочих, линковщик обнаружит, что он уже знает реализации этих функций и в telemetry.obj просто не полезет.
Нюанс 1: должны быть реализованы действительно все функции из telemetry.obj, которые может вызвать внешний код. Если, условно, в следующем обновлении функция printf будет вызывать какой-нибудь __telemetry_printf_trigger, получится ошибка линковки из-за двойного определения.
Нюанс 2: если явная реализация находится во вспомогательной библиотеке, то какая именно реализация подхватится, может зависеть от порядка перечисления библиотек в командной строке. Причём навскидку я не в курсе, в каком порядке обрабатываются встроенные библиотеки и считается ли libvcruntime.lib встроенной.
Тут выше назвали одну конкретную игру, Kami no Rhapsody, это оттуда. Движок версии 4.46.
mov dword ptr [esi+ecx*4+5D804h], 5
, так чтозаполняет таблицу, за исключением нескольких сложных случаев, которые уже можно добить руками.
Таблицы смещений в скриптах в количестве трёх штук — просто адреса команд с опкодами 0x71, 3, 0x8F. Первая — список возможных сообщений для хранения флагов прочитанности/непрочитанности, вторая — список возможных внешних вызовов из скрипта, третья — список возможных вызовов процедур. Подозреваю, что так сделано для большей стабильности сейвов: когда в сейвах стек вызовов хранится как массив индексов в отдельной таблице, а не прямо адресами внутри файла, это не будет плыть при каждом изменении скриптов.
Для правки текстов кучу смещений изменять не нужно, достаточно дописывать исправленные тексты в конец файла и править только одно смещение — собственно адрес текста.
В принципе, при некотором желании выдачу дизассемблера выше можно даже скомпилировать назад с помощью fasmg, если пошаманить с его макросами.
Но, надо отметить, определение empty в последовательной версии некорректно: что gcc, что clang успешно сворачивают цикл с empty в константу. gcc, кстати, в неправильную константу: https://godbolt.org/g/wibi2q.
Я бы предположил, что вы спутали код загрузчика приложений (ntdll.dll/ld-linux.so) и код рантайма конкретной среды программирования, но код загрузчика приложений тоже не имеет никакого отношения к планировщику.
И не соблаговолит ли прилежный слушатель курса операционных систем и системного программирования просветить невежественную шпану типа меня, каким образом прослушивание такого курса избавляет от вопроса «что делает вызов __telemetry_main_invoke_trigger() непосредственно перед вызовом main()»?
hello.exe из статьи — вполне готовый бинарник, на его месте может быть любая собранная программа. А logman/tracerpt могут быть запущены совершенно независимо от него и от ведома разработчика hello.exe. В том числе апдейтом Windows. В том числе не трогающем vcruntime140.dll или как она там называется.