Skip to content

Repository files navigation

UndocumentedProof

Проверка того, что в C# и .NET работает, хотя в документации этого нет. Четыре ключевых слова, замена типа у живого объекта, объект вместо условия и разбор одного ходячего заблуждения.

Замеров в проекте нет: всё, что он проверяет, либо работает, либо не работает, и числа тут ничего не добавят. Вместо замеров — отчёты и сверка с кодом возврата.

Статья: «А чё, так можно было? Недокументированный C#»

Что проверяется

Отчёт Что смотрим
keywords __makeref, __reftype, __refvalue — тип и значение переменной без GetType
arglist __arglist — метод с переменным числом аргументов, который выполняется не на всякой системе
swap замена указателя на таблицу методов: массив отвечает, что он строка
boolops перегрузка операторов true и false: объект стоит в условии
nullcall вызов метода у пустой ссылки: что падает, а что нет

Стенд

Четыре машины x64 под Windows, .NET 8, 9 и 10, сборки от 8.0.11 до 10.0.12. Выгрузки лежат в Results: Comp_1 — Intel Core i9-10900KF, Comp_2 — AMD Ryzen 9 5950X, Comp_3 — Intel Xeon W-2255, Comp_4 — Intel Xeon Silver 4314.

Результат одинаков на всех машинах и всех рантаймах. Отдельно снят прогон на Ubuntu 24.04 x64 — он отличается только отчётом arglist.

Результаты

Скрытые ключевые слова

Тип переменной без вызова GetType

  int       __reftype даёт Int32
  string    __reftype даёт String
  DateTime  __reftype даёт DateTime

Разница с GetType

  число до записи: 42
  после __refvalue: 100
  TypedReference.ToObject: Секрет

Переменное число аргументов

На Windows:

Система: Microsoft Windows 10.0.17763, X64

  вызов прошёл: Int32 = 1; String = два; Double = 3,5;

На Linux та же сборка:

Система: Ubuntu 24.04.4 LTS, X64

  вызов не прошёл
  тип исключения: System.InvalidProgramException
  сообщение:      Vararg calling convention not supported.

Причина — проверка в джите: соглашение вызова с переменным числом аргументов поддерживается только на Windows и только не на 32-битном Arm.

Замена типа у живого объекта

До замены
  массив отвечает: System.Int32[]
  строка отвечает: System.String

После замены
  массив отвечает: System.String
  содержимое как строка: ..o   (длина 3)

После возврата
  массив отвечает: System.Int32[]
  первый элемент:  111
  сборка мусора прошла, куча цела

Операторы true и false

  if (yes)                сработал
  if (no)                 не сработал
  while с объектом        оборотов 3
  тернарный оператор      ветка да

  yes && no               нет
  yes || no               да

Вызов у пустой ссылки

  ссылка пустая: True

  метод обращается к полю       NullReferenceException
  метод к полям не обращается   NullReferenceException
  метод расширения              выполнился, аргумент пустой

Как воспроизвести

Нужны SDK .NET 8, 9 и 10.

dotnet --list-sdks

Весь прогон одной командой:

all.bat

Вручную, без скрипта:

dotnet run -c Release -f net10.0 -- checks
dotnet run -c Release -f net10.0 -- keywords
dotnet run -c Release -f net10.0 -- arglist
dotnet run -c Release -f net10.0 -- swap
dotnet run -c Release -f net10.0 -- boolops
dotnet run -c Release -f net10.0 -- nullcall

Как устроены проверки

Скрытые ключевые слова компилятор принимает, а документация о них не упоминает. __makeref создаёт TypedReference — пару из указателя на данные и указателя на тип. __reftype возвращает тип из этой пары, __refvalue читает и пишет значение прямо в исходную переменную.

TypedReference — это ref struct, и отсюда все запреты: его нельзя сохранить в поле, положить в массив или передать в асинхронный метод.

__arglist объявляет метод с переменным числом аргументов без params и без массива. Вызов вынесен в отдельный тип и помечен [MethodImpl(MethodImplOptions.NoInlining)]: джит компилирует метод целиком, поэтому при встроенном вызове он откажется от всего метода, куда тот попал, и отчёт упал бы, не выведя ни строки.

Выполняется такой вызов не везде, поэтому сверка не требует одного исхода, а проверяет, что исход совпал с ожидаемым для этой системы. Каждый отчёт выводит систему и архитектуру.

Замена указателя на таблицу методов трогает живой объект в куче. Оставлять объект в таком виде нельзя: сборщик мусора прочитает его поля по описанию чужого типа и уронит процесс. Поэтому замена и возврат идут в одном методе, между ними нет ни выхода, ни вызова сборки мусора, а сразу после возврата отчёт вызывает сборку и убеждается, что процесс не упал.

Содержимое объекта с заменённой таблицей выводится с заменой непечатаемых символов: там необработанная память, и показать её как есть нельзя.

Операторы true и false перегружаются только парой. Вместе с оператором & они дают короткий && для своего типа: & считает результат, а true и false решают, нужно ли вообще вычислять правый операнд.

Вызов метода у пустой ссылки проверяется на трёх случаях: метод с обращением к полю, метод без обращения и метод расширения.

Что проверялось и чем

Что проверялось Чем
Скрытые слова дают тот же тип, что GetType отчёт checks сравнивает __reftype с typeof
__refvalue пишет в исходную переменную отчёт checks читает переменную после записи
Исход вызова с __arglist совпал с ожидаемым для системы отчёт checks сравнивает результат с проверкой из target.h
Замена типа обратима и не роняет кучу отчёт checks возвращает указатель и вызывает сборку мусора
Объект работает как условие отчёт checks проверяет обе ветки
Обычный метод у пустой ссылки падает отчёт checks проверяет метод с полем и без поля
Поведение не зависит от сборщика мусора режим DOTNET_gcServer=1 для замены типа
Поведение не зависит от многоуровневой компиляции режим DOTNET_TieredCompilation=0 для __arglist
Поведение не зависит от версии .NET 8, 9 и 10
Поведение зависит от системы отчёты выводят систему и архитектуру, __arglist работает только на Windows

Сверка возвращает код 1 при расхождении, но прогон на этом не останавливается: отчёты нужны в любом случае. Батник в конце сообщает, что сверка не прошла, и указывает, в каком файле смотреть.

Все проверки сняты на x64. Замена таблицы методов держится на том, что указатель на неё записан в начале объекта, — на другой разрядности смещения другие.

Отчёты

Results\Comp_N\
    checks_netN.0.txt            сверка, код возврата
    keywords_netN.0.txt          скрытые ключевые слова
    arglist_netN.0.txt           метод с переменным числом аргументов
    swap_netN.0.txt              замена типа у живого объекта
    boolops_netN.0.txt           операторы true и false
    nullcall_netN.0.txt          вызов метода у пустой ссылки
    swap_netN.0_servergc.txt     замена типа на серверном сборщике
    arglist_netN.0_notiered.txt  то же без многоуровневой компиляции

Что где лежит

UndocumentedProof.csproj     многоцелевой: net8.0, net9.0, net10.0
UndocumentedProof.slnx
Program.cs                   точка входа, разбор аргументов
README.md
all.bat                      весь прогон одной командой
Types\
    Shapes.cs                класс с операторами true и false, класс для пустой ссылки
    Vararg.cs                метод с переменным числом аргументов
    Innards.cs               чтение и запись указателя на таблицу методов
Diagnostics\
    Checks.cs                сверка, код возврата
    Keywords.cs              скрытые ключевые слова
    ArgList.cs               переменное число аргументов
    MethodTableSwap.cs       замена типа
    BoolOps.cs               операторы true и false
    NullCall.cs              вызов у пустой ссылки
Results\
    Comp_1 .. Comp_4\        выгрузки прогона, по папке на машину

Ссылки

Границы

Проект трогает внутренности рантайма и держится на том, что указатель на таблицу методов записан в начале объекта. Microsoft этого нигде не обещала: реализацию могут изменить, и тогда приём перестанет работать. Сверка это заметит.

__makeref, __reftype, __refvalue и __arglist в документации не описаны. Их могут убрать в любой версии компилятора без предупреждения — поэтому отчёты и запускаются на трёх рантаймах.

About

Четыре ключевых слова C#, которых нет в документации, замена типа у живого объекта и разбор мифа про вызов метода у null. Отчёты на .NET 8/9/10, четыре машины.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages