Русский
Русский
English
Статистика
Реклама

Virtual machine

Umka. Жизнь статической типизации в скриптовом языке

21.06.2020 14:07:34 | Автор: admin


В своё время посты на Хабре и Reddit о статически типизированном скриптовом языке Umka вызвали весьма активную дискуссию.

Прошедшие полтора месяца позволили мне избавиться от некоторых заблуждений, развить язык и дать чуть более вразумительные ответы на вопросы публики. Как и следовало ожидать, наиболее серьёзное испытание выпало на долю самой концепции статической типизации. Она осталась в основе языка, но потребовала компромиссов в частности, для корректной сборки мусора.

Появились первые замеры быстродействия интерпретатора в сравнении с Wren и Python их результаты внушают оптимизм. Наконец, родился более реалистичный пример использования Umka по его основному назначению, т. е. как встраиваемого языка.

Информация о типах во время исполнения программы (RTTI). Проект начинался с радикального отказа от хранения типов данных при исполнении программы. Вся информация о типах терялась после компиляции в байт-код виртуальной машины. В принципе, статическая типизация позволяет это сделать, а заодно избавляет от странных трюков вроде упаковки данных в NaN, к которой, например, прибегают создатели JavaScript и Wren ради увеличения быстродействия. Однако обнаружились два случая, в которых пришлось использовать RTTI:

  • Приведение интерфейсного типа данных к конкретному прямой аналог утверждения типа (type assertion) в Go, а также, отчасти, оператора dynamic_cast в C++. Оно требуется и при сборке мусора, содержащегося в данных, приведённых к интерфейсному типу.
  • Сборка мусора, связанного с динамическими структурами данных вроде списков и деревьев.

Быстродействие. Изначально Umka никак не предназначался для установления рекордов быстродействия. Безразличие публики к медлительности Python наводило на мысль, что скорость вовсе не то качество, которого в первую очередь ожидают от скриптового языка. Однако успех LuaJIT и активная реклама Wren заставили задуматься. После этого меня уже не удивляло, что и ранние публикации про Umka вызвали вопросы о быстродействии, хотя мне по-прежнему интересно, от кого в первую очередь исходит спрос на скорость. От разработчиков игр?

Пока полный набор тестов не готов, я могу поделиться лишь предварительными результатами замеров. В численных задачах (например, задаче многих тел) Umka надёжно опережает Python, а если в задаче активно используется цикл for, то Umka даёт выигрыш даже по сравнению с Wren, который позиционируется автором чуть ли не как самый быстрый скриптовый язык после LuaJIT. Наглядным примером служит перемножение больших матриц:


Умножение матриц 400 x 400 (AMD A4-3300M @ 1.9 GHz, Windows 7)

Очевидно, в пользу Umka здесь сыграла поддержка традиционных статических массивов и более низкоуровневая организация цикла for, не содержащая вызовов методов.

Задачи с интенсивной сборкой мусора (например, создание и обход двоичных деревьев) вызывают много сомнений по поводу эквивалентности сравниваемых алгоритмов. Например, известная реализация двоичных деревьев на Python возвращает содержимое узлов россыпью и выглядит так, будто в принципе допускает размещение всего дерева на стеке вообще без использования кучи и сборки мусора. Однако она, по-видимому, требует динамической типизации и не может быть точно воспроизведена на Umka. Если же потребовать возвращать узлы в виде структур, как в Umka (а за неимением структур приходится требовать объекты), то быстродействие Python сразу же падает в 3-4 раза. Вариант на Umka вдвое отстаёт от первой реализации и вдвое опережает вторую. Какое сравнение корректнее не знаю.

Взаимодействие с внешним кодом. Коль скоро язык рассматривается как встраиваемый, понадобился более или менее реалистичный пример взаимодействия кода на C и Umka. В нём средствами игровой библиотеки raylib формируется трёхмерная сцена, а наполнение сцены определяется внешним скриптом на Umka. В примере можно найти и вызов функций Umka из кода на C, и вызов функций C из Umka. Статическая типизация языка Umka позволила естественным образом формировать на нём структуры данных, непосредственно воспринимаемые библиотекой raylib.


Пример трёхмерной сцены, содержимое которой задаётся скриптом на Umka

Обобщённые типы и функции (generics). Как только читатель улавливает сходство Umka с Go, пускай даже синтаксическое следует вопрос о поддержке generic'ов. Работа в этом направлении пока не вышла из стадии обзора подходов. Конечно, хотелось бы воспользоваться предложениями разработчиков Go, однако сосуществование в их головах интерфейсов и контрактов всегда отпугивало, как странное дублирование понятий. К удивлению и радости, в только что вышедшей новой редакции черновика контракты исчезли по тем же причинам, о которых размышлял и я. Пока generic'ов в Umka нет, остаётся пользоваться, как и в Go, пустыми интерфейсами interface{}.

Документация. Полная спецификация Umka ещё в работе, но уже написана грамматика и расширен обзорный тур по основным возможностям языка.
Подробнее..

Проверка QEMU с помощью PVS-Studio

04.09.2020 10:14:53 | Автор: admin
image1.png

QEMU достаточно известное приложение для эмуляции. Статический анализ может помочь разработчикам таких сложных проектов, как QEMU, отлавливать ошибки на раннем этапе и в целом повысить его качество и надёжность. В этой статье будет проверен исходный код приложения QEMU на потенциальные уязвимости и ошибки с помощью инструмента статического анализа PVS-Studio.

QEMU является свободным ПО, предназначенным для эмуляции аппаратного обеспечения различных платформ. Оно позволяет запускать приложения и операционные системы на отличающихся от целевой аппаратных платформах, например, приложение, написанное для MIPS запустить для архитектуры x86. QEMU также поддерживает эмуляцию разнообразных периферийных устройств, например, видеокарты, usb и т.д. Проект достаточно сложный и достойный внимания, именно такие проекты представляют интерес для статического анализа, поэтому было решено проверить его код с помощью PVS-Studio.

Об анализе


Исходный код проекта можно получить из зеркала на github. Проект достаточно объемный и может компилироваться для различных платформ. Для более легкой проверки кода воспользуемся системой мониторинга компиляции PVS-Studio. Эта система предназначена для очень простой интеграции статического анализа практически в любую сборочную платформу. Система основана на отслеживании вызовов компилятора во время сборки и позволяет собрать всю информацию для последующего анализа файлов. Другими словами, просто запускаем сборку, PVS-Studio собирает необходимую информацию, а после запускаем анализ все просто. Подробности можно почитать по ссылке выше.

После проверки анализатор обнаружил множество потенциальных проблем. Для диагностик общего назначения (General Analysis) было получено: 1940 уровня High, 1996 уровня Medium, 9596 уровня Low. После просмотра всех предупреждений было решено остановиться на диагностиках для первого уровня достоверности (High). Таких предупреждений нашлось достаточно много (1940), но большая часть предупреждений либо однотипна, либо связана с многократным использованием подозрительного макроса. Для примера рассмотрим макрос g_new.

#define g_new(struct_type, n_structs)                        _G_NEW (struct_type, n_structs, malloc)#define _G_NEW(struct_type, n_structs, func)       \  (struct_type *) (G_GNUC_EXTENSION ({             \    gsize __n = (gsize) (n_structs);               \    gsize __s = sizeof (struct_type);              \    gpointer __p;                                  \    if (__s == 1)                                  \      __p = g_##func (__n);                        \    else if (__builtin_constant_p (__n) &&         \             (__s == 0 || __n <= G_MAXSIZE / __s)) \      __p = g_##func (__n * __s);                  \    else                                           \      __p = g_##func##_n (__n, __s);               \    __p;                                           \  }))

На каждое использование этого макроса анализатор выдает предупреждение V773 (Visibility scope of the '__p' pointer was exited without releasing the memory. A memory leak is possible). Макрос g_new определен в библиотеке glib, он использует макрос _G_NEW, а этот макрос в свою очередь использует другой макрос G_GNUC_EXTENSION, говорящий компилятору GCC пропускать предупреждения о нестандартном коде. Именно этот нестандартный код и вызывает предупреждение анализатора, обратите внимание на предпоследнюю строку. В целом же макрос является рабочим. Предупреждений этого типа нашлось 848 штук, то есть почти половина срабатываний приходится всего лишь на одно единственное место в коде.

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

Таким образом, большое количество предупреждений не всегда говорит о плохом качестве кода. Тем не менее есть некоторые по-настоящему подозрительные места. Что же, давайте приступим к разбору предупреждений.

Предупреждение N1

V517 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. Check lines: 2395, 2397. megasas.c 2395

#define MEGASAS_MAX_SGE 128             /* Firmware limit */....static void megasas_scsi_realize(PCIDevice *dev, Error **errp){  ....  if (s->fw_sge >= MEGASAS_MAX_SGE - MFI_PASS_FRAME_SIZE) {    ....  } else if (s->fw_sge >= 128 - MFI_PASS_FRAME_SIZE) {    ....  }  ....}

Любые использования "магических" чисел в коде всегда вызывают подозрение. Здесь два условия, и на первый взгляд они кажутся разными, но, если посмотреть значение макроса MEGASAS_MAX_SGE, то окажется, что условия дублируют друг друга. Скорее всего, здесь опечатка и вместо 128 должно стоять другое число. Конечно, это проблема всех "магических" чисел, достаточно просто опечататься при их использовании. Применение макросов и констант сильно помогает разработчику в этом случае.

Предупреждение N2

V523 The 'then' statement is equivalent to the 'else' statement. cp0_helper.c 383

target_ulong helper_mftc0_cause(CPUMIPSState *env){  ....  CPUMIPSState *other = mips_cpu_map_tc(env, &other_tc);  if (other_tc == other->current_tc) {    tccause = other->CP0_Cause;  } else {    tccause = other->CP0_Cause;  }  ....}

В рассматриваемом коде тела then и else условного оператора идентичны. Здесь, скорее всего, copy-paste. Просто скопировали тело then ветвления, а исправить забыли. Можно предположить, что вместо объекта other необходимо было использовать env. Исправление этого подозрительного места могло бы выглядеть следующим образом:

if (other_tc == other->current_tc) {  tccause = other->CP0_Cause;} else {  tccause = env->CP0_Cause;}

Однозначно сказать, как должно быть на самом деле могут только разработчики этого кода. Еще похожее место:

  • V523 The 'then' statement is equivalent to the 'else' statement. translate.c 641

Предупреждение N3

V547 Expression 'ret < 0' is always false. qcow2-cluster.c 1557

static int handle_dependencies(....){  ....  if (end <= old_start || start >= old_end) {    ....  } else {    if (bytes == 0 && *m) {      ....      return 0;           // <= 3    }    if (bytes == 0) {      ....      return -EAGAIN;     // <= 4    }  ....  }  return 0;               // <= 5}int qcow2_alloc_cluster_offset(BlockDriverState *bs, ....){  ....  ret = handle_dependencies(bs, start, &cur_bytes, m);  if (ret == -EAGAIN) {   // <= 2    ....  } else if (ret < 0) {   // <= 1    ....  }}

Здесь анализатор обнаружил, что условие (комментарий 1) никогда не выполнится. Значение переменной ret инициализируется результатом выполнения функции handle_dependencies, эта функция возвращает только 0 или -EAGAIN (комментарии 3, 4, 5). Чуть выше, в первом условии, мы проверили значение ret на -EAGAIN (комментарий 2), поэтому результат выполнения выражения ret < 0 будет всегда ложным. Возможно, раньше функция handle_dependencies и возвращала другие значения, но потом в результате, например, рефакторинга поведение поменялось. Здесь надо просто завершить рефакторинг. Похожие срабатывания:

  • V547 Expression is always false. qcow2.c 1070
  • V547 Expression 's->state != MIGRATION_STATUS_COLO' is always false. colo.c 595
  • V547 Expression 's->metadata_entries.present & 0x20' is always false. vhdx.c 769

Предупреждение N4

V557 Array overrun is possible. The 'dwc2_glbreg_read' function processes value '[0..63]'. Inspect the third argument. Check lines: 667, 1040. hcd-dwc2.c 667

#define HSOTG_REG(x) (x)                                             // <= 5....struct DWC2State {  ....#define DWC2_GLBREG_SIZE    0x70  uint32_t glbreg[DWC2_GLBREG_SIZE / sizeof(uint32_t)];              // <= 1  ....}....static uint64_t dwc2_glbreg_read(void *ptr, hwaddr addr, int index,                                 unsigned size){  ....  val = s->glbreg[index];                                            // <= 2  ....}static uint64_t dwc2_hsotg_read(void *ptr, hwaddr addr, unsigned size){  ....  switch (addr) {    case HSOTG_REG(0x000) ... HSOTG_REG(0x0fc):                      // <= 4        val = dwc2_glbreg_read(ptr, addr,                              (addr - HSOTG_REG(0x000)) >> 2, size); // <= 3    ....  }  ....}

В этом коде есть потенциальная проблема с выходом за границу массива. В структуре DWC2State определен массив glbreg,состоящий из 28 элементов (комментарий 1). В функции dwc2_glbreg_read по индексу обращаются к нашему массиву (комментарий 2). Теперь обратите внимание, что в функцию dwc2_glbreg_read в качестве индекса передают выражение (addr HSOTG_REG(0x000)) >> 2 (комментарий 3), которое может принимать значение в диапазоне [0..63]. Для того чтобы в этом убедиться, обратите внимание на комментарии 4 и 5. Возможно, тут надо скорректировать диапазон значений из комментария 4.

Еще похожие срабатывания:

  • V557 Array overrun is possible. The 'dwc2_hreg0_read' function processes value '[0..63]'. Inspect the third argument. Check lines: 814, 1050. hcd-dwc2.c 814
  • V557 Array overrun is possible. The 'dwc2_hreg1_read' function processes value '[0..191]'. Inspect the third argument. Check lines: 927, 1053. hcd-dwc2.c 927
  • V557 Array overrun is possible. The 'dwc2_pcgreg_read' function processes value '[0..127]'. Inspect the third argument. Check lines: 1012, 1060. hcd-dwc2.c 1012

Предупреждение N5

V575 The 'strerror_s' function processes '0' elements. Inspect the second argument. commands-win32.c 1642

void qmp_guest_set_time(bool has_time, int64_t time_ns,                         Error **errp){  ....  if (GetLastError() != 0) {    strerror_s((LPTSTR) & msg_buffer, 0, errno);    ....  }}

Функция strerror_s возвращает текстовое описание кода системной ошибки. Её сигнатура выглядит так:

errno_t strerror_s( char *buf, rsize_t bufsz, errno_t errnum );

Первый параметр это указатель на буфер, куда будет скопировано текстовое описание, второй параметр размер буфера, третий код ошибки. В коде в качестве размера буфера передается 0, это явно ошибочное значение. Кстати, есть возможность узнать заранее сколько байт надо выделять: надо просто вызвать strerrorlen_s, которая возвращает длину текстового описания ошибки. Это значение можно использовать для выделения буфера достаточного размера.

Предупреждение N6

V595 The 'blen2p' pointer was utilized before it was verified against nullptr. Check lines: 103, 106. dsound_template.h 103

static int glue (    ....    DWORD *blen1p,    DWORD *blen2p,    int entire,    dsound *s    ){  ....  dolog("DirectSound returned misaligned buffer %ld %ld\n",        *blen1p, *blen2p);                         // <= 1  glue(.... p2p ? *p2p : NULL, *blen1p,                            blen2p ? *blen2p : 0); // <= 2....}

В этом коде значение аргумента blen2p сначала используется (комментарий 1), а потом проверяется на nullptr (комментарий 2). Это крайне подозрительное место выглядит так, как будто просто забыли вставить проверку перед первым использованием (комментарий 1). Как вариант исправления просто добавить проверку:

dolog("DirectSound returned misaligned buffer %ld %ld\n",      *blen1p, blen2p ? *blen2p : 0);

Тут еще возникает вопрос по поводу аргумента blen1p. Вероятно, он тоже может быть нулевым указателем, и тут тоже надо будет добавить проверку. Еще несколько подобных срабатываний:

  • V595 The 'ref' pointer was utilized before it was verified against nullptr. Check lines: 2191, 2193. uri.c 2191
  • V595 The 'cmdline' pointer was utilized before it was verified against nullptr. Check lines: 420, 425. qemu-io.c 420
  • V595 The 'dp' pointer was utilized before it was verified against nullptr. Check lines: 288, 294. onenand.c 288
  • V595 The 'omap_lcd' pointer was utilized before it was verified against nullptr. Check lines: 81, 87. omap_lcdc.c 81

Предупреждение N7

V597 The compiler could delete the 'memset' function call, which is used to flush 'op_info' object. The RtlSecureZeroMemory() function should be used to erase the private data. virtio-crypto.c 354

static void virtio_crypto_free_request(VirtIOCryptoReq *req){  if (req) {    if (req->flags == CRYPTODEV_BACKEND_ALG_SYM) {      ....      /* Zeroize and free request data structure */      memset(op_info, 0, sizeof(*op_info) + max_len); // <= 1      g_free(op_info);    }    g_free(req);  }}

В этом фрагменте кода вызывается функция memset для объекта op_info (комментарий 1), после этого op_info сразу удаляется, то есть, другими словами, после очистки этот объект нигде больше не модифицируется. Это как раз тот самый случай, когда в процессе оптимизации компилятор может удалить вызов memset. Чтобы исключить подобное потенциальное поведение, можно воспользоваться специальными функциями, которые компилятор никогда не удаляет. См. также статью "Безопасная очистка приватных данных".

Предупреждение N8

V610 Unspecified behavior. Check the shift operator '>>'. The left operand is negative ('number' = [-32768..2147483647]). cris.c 2111

static voidprint_with_operands (const struct cris_opcode *opcodep,         unsigned int insn,         unsigned char *buffer,         bfd_vma addr,         disassemble_info *info,         const struct cris_opcode *prefix_opcodep,         unsigned int prefix_insn,         unsigned char *prefix_buffer,         bfd_boolean with_reg_prefix){  ....  int32_t number;  ....  if (signedp && number > 127)    number -= 256;            // <= 1  ....  if (signedp && number > 32767)    number -= 65536;          // <= 2  ....  unsigned int highbyte = (number >> 24) & 0xff;  ....}

Так как переменная number может иметь отрицательное значение, побитовый сдвиг вправо является неуточнённым поведением (unspecified behavior). Чтобы убедиться в том, что рассматриваемая переменная может принять отрицательное значение, обратите внимание на комментарии 1 и 2. Для устранения различий поведения вашего кода на различных платформах, таких случаев нужно не допускать.

Еще предупреждения:

  • V610 Undefined behavior. Check the shift operator '<<'. The left operand is negative ('(hclk_div 1)' = [-1..15]). aspeed_smc.c 1041
  • V610 Undefined behavior. Check the shift operator '<<'. The left operand '(target_long) 1' is negative. exec-vary.c 99
  • V610 Undefined behavior. Check the shift operator '<<'. The left operand is negative ('hex2nib(words[3][i * 2 + 2])' = [-1..15]). qtest.c 561

Также есть несколько предупреждений такого же типа, только в качестве левого операнда выступает -1.

V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. hppa.c 2702

int print_insn_hppa (bfd_vma memaddr, disassemble_info *info){  ....  disp = (-1 << 10) | imm10;  ....}

Другие подобные предупреждения:

  • V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. hppa.c 2718
  • V610 Undefined behavior. Check the shift operator '<<'. The left operand '-0x8000' is negative. fmopl.c 1022
  • V610 Undefined behavior. Check the shift operator '<<'. The left operand '(intptr_t) 1' is negative. sve_helper.c 889

Предупреждение N9

V616 The 'TIMER_NONE' named constant with the value of 0 is used in the bitwise operation. sys_helper.c 179

#define HELPER(name) ....enum {  TIMER_NONE = (0 << 30),        // <= 1  ....}void HELPER(mtspr)(CPUOpenRISCState *env, ....){  ....  if (env->ttmr & TIMER_NONE) {  // <= 2    ....  }}

Можно легко убедиться, что значение макроса TIMER_NONE равно нулю (комментарий 1). Далее этот макрос используется в побитовой операции, результат которой всегда будет 0. Как итог, тело условного оператора if (env->ttmr & TIMER_NONE) никогда не выполнится.

Предупреждение N10

V629 Consider inspecting the 'n << 9' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. qemu-img.c 1839

#define BDRV_SECTOR_BITS   9static int coroutine_fn convert_co_read(ImgConvertState *s,                   int64_t sector_num, int nb_sectors, uint8_t *buf){  uint64_t single_read_until = 0;  int n;  ....  while (nb_sectors > 0) {    ....    uint64_t offset;    ....    single_read_until = offset + (n << BDRV_SECTOR_BITS);    ....  }  ....}

В этом фрагменте кода над переменной n, имеющей 32-битный знаковый тип, выполняется операция сдвига, потом этот 32-битный знаковый результат расширяется до 64-битного знакового типа, и далее, как беззнаковый тип, складывается с беззнаковой 64-битной переменной offset. Предположим, что на момент выполнения выражения переменная n имеет некоторые значимые старшие 9 бит. Мы выполняем операцию сдвига на 9 разрядов (BDRV_SECTOR_BITS), а это, в свою очередь, является неопределенным поведением, тогда в качестве результата мы можем получить выставленный бит в старшем разряде. Напомним, что этот бит в знаковом типе отвечает за знак, то есть результат может стать отрицательным. Так как переменная n знакового типа, то при расширении будет учтен знак. Далее результат складывается с переменной offset. Из этих рассуждений нетрудно увидеть, что результат выполнения выражения может отличаться от предполагаемого. Одним из возможных вариантов решения является замена типа переменной n на 64-битный беззнаковый тип, то есть на uint64_t.

Вот еще похожие срабатывания:

  • V629 Consider inspecting the '1 << refcount_order' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. qcow2.c 3204
  • V629 Consider inspecting the 's->cluster_size << 3' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. qcow2-bitmap.c 283
  • V629 Consider inspecting the 'i << s->cluster_bits' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. qcow2-cluster.c 983
  • V629 Consider inspecting the expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. vhdx.c 1145
  • V629 Consider inspecting the 'delta << 2' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. mips.c 4341

Предупреждение N11

V634 The priority of the '*' operation is higher than that of the '<<' operation. It's possible that parentheses should be used in the expression. nand.c 310

static void nand_command(NANDFlashState *s){  ....  s->addr &= (1ull << s->addrlen * 8) - 1;  ....}

Тут просто подозрительное место. Непонятно, что программист хотел сделать вначале: сдвиг или умножение. Даже если здесь нет ошибки, все равно надо еще раз взглянуть на код и правильно расставить скобочки. Тут как раз одно из таких мест, которые должны смотреть разработчики, чтобы убедиться в корректности своего алгоритма. Другие такие места:

  • V634 The priority of the '*' operation is higher than that of the '<<' operation. It's possible that parentheses should be used in the expression. exynos4210_mct.c 449
  • V634 The priority of the '*' operation is higher than that of the '<<' operation. It's possible that parentheses should be used in the expression. exynos4210_mct.c 1235
  • V634 The priority of the '*' operation is higher than that of the '<<' operation. It's possible that parentheses should be used in the expression. exynos4210_mct.c 1264

Предупреждение N12

V646 Consider inspecting the application's logic. It's possible that 'else' keyword is missing. pl181.c 400

static void pl181_write(void *opaque, hwaddr offset,                        uint64_t value, unsigned size){  ....  if (s->cmd & PL181_CMD_ENABLE) {    if (s->cmd & PL181_CMD_INTERRUPT) {      ....    } if (s->cmd & PL181_CMD_PENDING) { // <= else if      ....    } else {      ....    }    ....  }  ....}

В этом коде, судя по форматированию, прямо напрашивается использование else if вместо if. Возможно, здесь забыли дописать else. Тогда вариант исправления может быть такой:

} else if (s->cmd & PL181_CMD_PENDING) { // <= else if

Однако есть вероятность, что с этим кодом все в порядке, и тут некорректное форматирование текста программы, которое сбивает с толку. Тогда код мог бы выглядеть так:

if (s->cmd & PL181_CMD_INTERRUPT) {  ....}if (s->cmd & PL181_CMD_PENDING) { // <= if  ....} else {  ....}

Предупреждение N13

V773 The function was exited without releasing the 'rule' pointer. A memory leak is possible. blkdebug.c 218

static int add_rule(void *opaque, QemuOpts *opts, Error **errp){  ....  struct BlkdebugRule *rule;  ....  rule = g_malloc0(sizeof(*rule));                   // <= 1  ....  if (local_error) {    error_propagate(errp, local_error);    return -1;                                       // <= 2  }  ....  /* Add the rule */  QLIST_INSERT_HEAD(&s->rules[event], rule, next);   // <= 3  ....}

В данном коде выделяется объект rule (комментарий 1) и добавляется в список для последующего использования (комментарий 3), но в случае ошибки происходит возврат из функции без удаления ранее созданного объекта rule (комментарий 2). Здесь надо просто правильно обработать ошибку: удалить ранее созданный объект, иначе будет утечка памяти.

Предупреждение N14

V781 The value of the 'ix' index is checked after it was used. Perhaps there is a mistake in program logic. uri.c 2110

char *uri_resolve_relative(const char *uri, const char *base){  ....  ix = pos;  if ((ref->path[ix] == '/') && (ix > 0)) {  ....}

Здесь анализатор обнаружил потенциальный выход за границу массива. Сначала читается элемент массива ref->path по индексу ix, а потом ix проверяется на корректность (ix > 0). Правильным решением тут будет поменять эти действия местами:

if ((ix > 0) && (ref->path[ix] == '/')) {

Таких мест нашлось несколько:

  • V781 The value of the 'ix' index is checked after it was used. Perhaps there is a mistake in program logic. uri.c 2112
  • V781 The value of the 'offset' index is checked after it was used. Perhaps there is a mistake in program logic. keymaps.c 125
  • V781 The value of the 'quality' variable is checked after it was used. Perhaps there is a mistake in program logic. Check lines: 326, 335. vnc-enc-tight.c 326
  • V781 The value of the 'i' index is checked after it was used. Perhaps there is a mistake in program logic. mem_helper.c 1929

Предупреждение N15

V784 The size of the bit mask is less than the size of the first operand. This will cause the loss of higher bits. cadence_gem.c 1486

typedef struct CadenceGEMState {  ....  uint32_t regs_ro[CADENCE_GEM_MAXREG];}....static void gem_write(void *opaque, hwaddr offset, uint64_t val,        unsigned size){  ....  val &= ~(s->regs_ro[offset]);  ....}

В этом коде выполняется побитовая операция с объектами разных типов. Левый операнд это аргумент val, имеющий 64-битный беззнаковый тип. В качестве правого операнда выступает полученное значение элемента массива s->regs_ro по индексу offset, имеющий 32-битный беззнаковый тип. Результат операции в правой части (~(s->regs_ro[offset])) является 32-битным беззнаковым типом, и перед побитовым умножением он расширится до 64-битного типа нулями, то есть после вычисления всего выражения обнулятся все старшие биты переменной val. Такие места всегда выглядят подозрительными. Тут можно только порекомендовать разработчикам еще раз пересмотреть этот код. Еще похожее:

  • V784 The size of the bit mask is less than the size of the first operand. This will cause the loss of higher bits. xlnx-zynq-devcfg.c 199
  • V784 The size of the bit mask is less than the size of the first operand. This will cause the loss of higher bits. soc_dma.c 214
  • V784 The size of the bit mask is less than the size of the first operand. This will cause the loss of higher bits. fpu_helper.c 418

Предупреждение N16

V1046 Unsafe usage of the 'bool' and 'unsigned int' types together in the operation '&='. helper.c 10821

static inline uint32_t extract32(uint32_t value, int start, int length);....static ARMVAParameters aa32_va_parameters(CPUARMState *env, uint32_t va,                                          ARMMMUIdx mmu_idx){  ....  bool epd, hpd;  ....  hpd &= extract32(tcr, 6, 1);}

В этом фрагменте кода происходит операция побитового И над переменной hpd, имеющей тип bool, и результатом выполнения функции extract32, имеющим тип uint32_t. Так как битовое значение булевой переменной может быть только 0 или 1, то результат выражения будет всегда false, если младший бит, возвращаемый функцийе extract32, равен нулю. Давайте рассмотрим это на примере. Предположим, что значение hpd равно true, а функция вернула значение 2, то есть в двоичном представлении операция будет выглядеть так 01 & 10 = 0, а результат выражения будет равен false. Скорее всего, программист хотел выставлять значение true, если функция возвращает что-то отличное от нуля. По всей видимости, надо исправить код так, чтобы результат выполнения функции приводился к типу bool, например, так:

hpd = hpd && (bool)extract32(tcr, 6, 1);

Заключение


Как видно, анализатор нашел много подозрительных мест. Возможно, найденные потенциальные проблемы пока никак себя не проявляют, но их наличие не может не тревожить, так как они способны выстрелить в самый неожиданный момент. Просмотреть все подозрительные места заранее и исправить лучше, чем чинить бесконечный поток багов. Очевидно, что для сложных проектов, подобных этому, статический анализ может принести ощутимую пользу, особенно если организовать регулярную проверку проекта. Если Вы хотите попробовать PVS-Studio для своего проекта, вы можете скачать анализатор и получить бесплатный триальный ключ на этой странице.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Evgeniy Ovsannikov. Checking QEMU using PVS-Studio.
Подробнее..

Обзор программы JPoint 2021 воркшопы, Spring, игра вдолгую

24.03.2021 18:04:11 | Автор: admin


Близится новый JPoint, и мы готовы подробно рассказать о его программе. В этом посте мы разделили доклады по тематическим блокам: можно и быстро понять что вообще будет, и узнать конкретику. А во вступлении упомянем отдельные моменты:


  • Пришла весна, то есть самое время поговорить о Spring. О нём будет четыре доклада, в том числе большое двухчастное выступление Евгения Борисова. Для него мы даже продлили JPoint на пятый день получился специальный день Борисова :)
  • Онлайн-формату подходят воркшопы. Поэтому в отдельных случаях можно будет не просто любоваться слайдами: спикер будет выполнять конкретные задачи на практике, объясняя всё происходящее и отвечая на вопросы зрителей.
  • Есть доклады не строго про Java, а про то, как успешно разрабатывать на длинной дистанции (чтобы всё радовало не только на стадии прототипа, а годы спустя): как делать проекты поддерживаемыми, не плодить велосипеды, работать с легаси.
  • Ну и никуда не девается привычное. Знакомые темы: что у Java внутри, тулинг/фреймворки, языковые фичи, JVM-языки. Спикеры, посвятившие теме годы жизни: от технического лида Project Loom Рона Пресслера до главного Spring-адвоката Джоша Лонга. Возможность как следует расспросить спикера после доклада. И уточки для отладки методом утёнка!

Оглавление


Воркшопы
VM/Runtime
Тулинг и фреймворки
Spring
JVM-языки
Люби свою IDE
Жизнь после прототипа




Воркшопы


Воркшоп: Парное программирование, Андрей Солнцев, Антон Кекс


Парное программирование знакомо многим, но вот много ли вы знаете людей, использующих его на постоянной основе? А вот Андрей Солнцев и Антон Кекс действительно практикуют его постоянно и уверены, что подобные практики из мира экстремального программирования помогают работать эффективнее.


Они и раньше об этом говорили, а теперь попробуют показать наглядно. В ходе воркшопа они вдвоём напишут небольшое приложение по TDD (с помощью тестов) и в ходе этого вы лично убедитесь, что с таким подходом разработка идёт быстрее и проще. Или не убедитесь. Интересно будет посмотреть, какую часть аудитории получится убедить.




Воркшоп: Строим Бомбермена с RSocket, Олег Докука, Сергей Целовальников


Олег Докука и Сергей Целовальников на небольшом игровом примере продемонстрируют практический опыт использования RSocket-Java и RSocket-JS.


Кому, как не им, демонстрировать возможности RSocket: Олег лидер этого проекта, а Сергей ведущий разработчик компании Canva, которая одной из первых внедрила его в продакшн. Если у вас есть вопросы по этой технологии у них точно есть ответы.




VM/Runtime


CRIU and Java opportunities and challenges, Christine H Flood


Зачем каждый раз тратить драгоценные секунды на запуск приложения, если можно один раз запустить, сохранить чекпоинт и открывать его уже в запущенном виде, в полной боевой готовности? Причём открывать при необходимости на нескольких машинах сразу?


О том, как использовать Checkpoint Restore в Java, расскажет Кристин Флад из Red Hat, которая работает над языками и рантаймами уже более двадцати лет.




Real World JFR: Experiences building and deploying a continuous profiler at scale, Jean-Philippe Bempel


JDK Flight Recorder позволяет профилировать непрерывно и прямо в продакшне. Но это же не может быть бесплатно, да? Важно понимать, чем придётся пожертвовать и какие будут накладные расходы.


Разобраться в этом поможет Жан-Филипп Бемпель он принимал непосредственное участие в реализации непрерывной профилировки в JFR.




GC optimizations you never knew existed, Igor Henrique Nicacio Braga, Jonathan Oommen


Какой JPoint без докладов про сборщики мусора! Тут выступление для тех, кто уже что-то знает по теме объяснять совсем азы не станут. Но и загружать суперхардкором с первой минуты тоже не станут. Сначала будет подготовительная часть, где Игор Брага и Джонатан Оммен рассмотрят два подхода к GC в виртуальной машине OpenJ9: balanced и gencon.


А вот потом полезут глубже и рассмотрят оптимизации, которые там применяются. Для обычного разработчика эти оптимизации могут быть незаметными, но они способны ощутимо сказываться на производительности поэтому стоит понимать, как они работают и в чём выгода от них для Java-приложений.




Adding generational support to Shenandoah GC, Kelvin Nilsen


И ещё о сборке мусора. На JPoint 2018 о Shenandoah GC рассказывал Алексей Шипилёв (Red Hat), а теперь доклад от совсем другого спикера Келвина Нилсена из Amazon, где тоже работают над этим сборщиком мусора.


Подход Shenandoah позволяет сократить паузы на сборку мусора менее чем до 10 миллисекунд, но за это приходится расплачиваться большим размером хипа (потому что его утилизация оказывается заметно ниже, чем у традиционных GC). А можно ли сделать так, чтобы и волки были сыты, и овцы целы? В Amazon для этого решили добавить поддержку поколений, и в докладе поделятся результатами.




Производительность: Нюансы против очевидностей, Сергей Цыпанов


Производительность тема, где с простым соседствует сложное. Даже в простых с виду частях кода могут обнаруживаться сюрпризы и подводные камни, а то, что казалось очевидным улучшением, может оборачиваться ухудшением производительности. Всё это и будет рассматривать в своём докладе Сергей Цыпанов, в своём профессиональном развитии сосредоточенный как раз на задачах производительности.




Why user-mode threads are (often) the right answer, Ron Pressler


Многопоточное программирование в Java поддерживается с версии 1.0, но за 25 лет в этой части языка почти ничего не поменялось, а вот требования выросли. Серверам требуется работать с сотнями тысяч, и даже миллионами потоков, а стандартное решение в JVM на тредах операционной системы не может так масштабироваться, поэтому Project Loom это одна из самых долгожданных фич языка.


Ранее у нас уже был доклад про Loom от Алана Бейтмана (мы делали расшифровку для Хабра), а теперь и technical lead этого проекта Рон Пресслер рассмотрит разные решения для работы с многопоточностью и подход, который используется в Loom.




Тулинг и фреймворки


Как мы строили высокопроизводительную систему на Akka с нуля: Джентльменский набор и грабли, Кирилл Данилов


Кирилл расскажет про опыт создания платежной системы с использованием Akka от обучения с нуля до построения кластера и интеграции этой платформы с более привычными и удобными в своей нише технологиями, например, Spring Boot, Hazelcast, Kafka.


Будет полезно тем, кто хочет ознакомиться с моделью акторов и с Akka и тем, кому интересны нюансы разработки финансовых систем с большой пропускной способностью.




Jakarta EE 9 and beyond, Ivar Grimstad, Tanja Obradovi


Jakarta EE 9 несет множество изменений, которые затронут большое количество библиотек и фреймворков для Java. Чтобы понять, как эти изменения отразятся на ваших проектах, приходите на доклад Ивана Гримстада и Тани Обрадович.


Ивар Jakarta EE Developer Advocate, а Таня Jakarta EE Program Manager, поэтому вы узнаете о самых важных изменениях и планах на будущее из первых рук.




Чтения из Cassandra внутреннее устройство и производительность, Дмитрий Константинов


В качестве хранилища данных очень часто используется Cassandra, и всем, кто ее использует, будет полезно знать, как она устроена внутри, и от чего зависит её производительность на чтение.


Об этом расскажет системный архитектор и практикующий разработчик из NetCracker Дмитрий Константинов.




The DGS framework by Netflix GraphQL for Spring Boot made easy, Paul Bakker


В Netflix разработали DGS Framework для работы с GraphQL. Он работает поверх graphql-java и позволяет работать с GraphQL, используя привычные модели Spring Boot. И, что приятно, он опенсорсный, стабильный и готов к использованию в продакшне.


Пол Баккер один из авторов DGS. Он расскажет и про GraphQL, и про то, как работать с DGS, и про то, как это используется в Netflix.




Качественный код в тестах не просто приятный бонус, Sebastian Daschner


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


Предыдущие доклады Себастьяна на наших конференциях были англоязычными, но участвуя в российских мероприятиях, он так влился в местное Java-сообщество, что теперь попробует провести доклад на русском!




Why you should upgrade your Java for containers, Ben Evans


Статистика от New Relic говорит, что примерно 62% Java на продакшне в 2021 запущено в контейнерах. Но в большинстве из этих случаев до сих пор используют Java 8 а эта версия подходит для контейнеризации не лучшим образом. Почему? Бен Эванс рассмотрит, в чём проблемы с ней, что улучшилось с Java 11, и как измерить эффективность и расходы.


Хотя в основном речь пойдёт о проблемах, актуальных именно в случае с контейнерами, часть сказанного будет применима и к Java-разработке в целом.




Разошлись как в море корабли: Кафка без Zookeeper, Виктор Гамов


Совсем скоро придет тот день, о котором грезили Kafka-опсы и Apache Kafka больше не будет нуждаться в ZooKeeper! С KIP-500 в Kafka будет доступен свой встроенный механизм консенсуса (на основе алгоритма Raft), полностью удалив зависимость от ZooKeeper. Начиная с Apache Kafka 2.8.0. вы сможете получить доступ к новому коду и разрабатывать свои приложения для Kafka без ZooKeeper.


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




Spring


Spring Data Рostроитель (Spark it!), Евгений Борисов


Товарищ, знай! Чтоб использовать Spark,
Scala тебе не друг и не враг.
Впрочем, и Spark ты можешь не знать,
Spring-data-spark-starter лишь надо создать!


Этот доклад не про Spark и не про Big Data. Его скорее можно отнести к серии потрошителей и построителей. Что будем строить и параллельно потрошить сегодня? Spring Data. Она незаметно просочилась в большинство проектов, подкупая своей простотой и удобным стандартом, который избавляет нас от необходимости каждый раз изучать новый синтаксис и подходы разных механизмов работы с данными.


Хотите разобраться, как Spring Data творит свою магию? Давайте попробуем написать свой аналог. Для чего ещё не написана Spring Data? JPA, Mongo, Cassandra, Elastic, Neo4j и остальные популярные движки уже имеют свой стартер для Spring Data, а вот Spark, как-то забыли. Давайте заодно исправим эту несправедливость. Не факт, что получится что-то полезное, но как работает Spring Data мы точно поймём.




Spring Cloud в эру Kubernetes, Алексей Нестеров


Когда-то давно, много JavaScript-фреймворков назад, когда микросервисы еще были монолитами, в мире существовало много разных инструментов для разработки Cloud Native приложений. Spring Cloud был одним из главных в реалиях Spring и объединял в себе целый набор полезных проектов от Netflix, команды Spring и многих других вендоров.


Казалось бы, в наши дни, когда весь мир захвачен Kubernetes и он уже давно стал универсальным решением любой проблемы, важность Spring Cloud должна неизбежно сойти на нет. Но не все так просто, и в этом докладе Алексей покажет, какие компоненты Spring Cloud могут быть полезны в Kubernetes, чем эти два проекта друг друга дополняют, в каких аспектах пересекаются, ну и самое главное, постарается ответить на вопрос в чем же ценность Spring Cloud в эру Kubernetes?




Reactive Spring, Josh Long


Джош Лонг расскажет про фичи Spring Framework 5.0 для реактивного программирования: Spring WebFlux, Spring Data Kay, Spring Security 5.0, Spring Boot 2.0, Spring Cloud Finchley и это только часть!


Может показаться многовато для одного доклада, но мы-то знаем, что Джош Spring Developer Advocate с 2010 года. Уж кто-кто, а он-то знает, как рассказать всё быстро и по делу.




Inner loop development with Spring Boot on Kubernetes, David Syer


Мы живем во время облачных технологий и чтобы эффективнее перейти от принципа works on my machine к works on my/dev cluster нужен набор инструментов для автоматизация загрузки кода на лету.
Доклад Дэвида Сайера будет про то, как и с помощью каких инструментов Spring Boot и Kubernetes построить этот процесс удобно.
Ускорение первой фазы доставки это тот DevOps, который нужен разработчикам, поэтому всем, кто живет в k8s или хотя бы делает системы из нескольких компонентов этот доклад пригодится.




Люби свою IDE


IntelliJ productivity tips The secrets of the fastest developers on Earth, Victor Rentea


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


Виктор Рента покажет много трюков для работы с IntelliJ IDEA, которые помогут писать код быстрее, экспериментировать больше и делать откат результатов неудачных экспериментов проще.




Многоступенчатые рефакторинги в IntelliJ IDEA, Анна Козлова


В IntelliJ IDEA есть ограниченное количество основных рефакторингов: Rename, Move, Inline, Extract. Пользователи часто просят добавить еще, но чаще всего это можно сделать комбинацией уже существующих, просто это не всегда очевидно.


На JPoint 2021 вы сможете получить практические рекомендации по рефакторингу от человека, который разрабатывает рефакторинги: о самых важных приемах расскажет коммитер 1 в IntelliJ IDEA Community Edition Анна Козлова.




С какими языками дружат IDE?, Петр Громов


Разработчики всё чаще выбирают языки с выразительным и кратким синтаксисом, совсем не задумываясь на тем, как долго их код будет компилироваться, и сможет ли IDE помочь им с рефакторингом. В докладе Петра Громова будут интересные идеи о сложности современных языков программирования с точки зрения разработчиков IDE.


Рекомендуем всем, кому интересны механизмы IDE, языки, парсеры, DSL и сложные синтаксические конструкции в современных языках программирования.




Java и JVM-языки


Type inference: Friend or foe?, Venkat Subramaniam


Не все могут объяснять так, как это делает Венкат Субраманиам, поэтому мы любим приглашать его на конференции.


На JPoint 2021 он выступит с докладом про type inference. Хотя тема сама по себе не новая, нюансов в ней хватает, а развитие языков делает её лишь более актуальной (вспоминается доклад Романа Елизарова с TechTrain, где он рассматривал, как ситуация с типами и их выводом менялась со временем). Так что стоит лучше понять, в чём вывод типов помогает, а в чём мешает для этого и рекомендуем сходить на этот доклад.




Babashka: A native Clojure interpreter for scripting, Michiel Borkent


Babashka интерпретатор Clojure для скриптов. Он мгновенно запускается, делая Clojure актуальной заменой для bash. У Babashka из коробки есть набор полезных библиотек, дающих доступ из командной строки к большому количеству фич Clojure и JVM. Сам интерпретатор написан на Clojure и скомпилирован с помощью GraalVM Native Image. В докладе работу с ним наглядно покажут с помощью демо.




Getting the most from modern Java, Simon Ritter


Недавно вышла JDK 16, и это значит, что мы получили 8 (прописью: ВОСЕМЬ) версий Java менее чем за четыре года. Разработчики теперь получают фичи быстрее, чем когда-либо в истории языка.


Так что теперь попросту уследить бы за всем происходящим. Если вы до сих пор сидите на Java 8, на что из появившегося позже стоит обратить внимание и чем это вам будет полезно? В этом поможет доклад Саймона Риттера, где он поговорит о некоторых нововведениях JDK 12-15 и о том, когда их исследовать, а когда нет:


  • Switch expressions (JDK 12);
  • Text blocks (JDK 13);
  • Records (JDK 14);
  • Pattern matching for instanceof (JDK 14);
  • Sealed classes and changes to Records (JDK 15).


Про Scala 3, Олег Нижников


Обзор языка Scala 3 и грядущей работы по переходу. Обсудим, в какую сторону двигается язык, откуда черпает вдохновение, и пройдёмся по фичам.




Java Records for the intrigued, Piotr Przybyl


В Java 14 появились в превью-статусе Records, а с Java 16 они стали стандартной фичей. Для многих это было поводом сказать что-то вроде Lombok мёртв или не нужна больше кодогенерация JavaBeans. Так ли это на самом деле? Что можно сделать с помощью Records, а чего нельзя? Что насчёт рефлексии и сериализации? Разберём в этом докладе.




Жизнь после прототипа


Восстанавливаем утраченную экспертизу по сервису, Анна Абрамова


Представьте ситуацию: вы приходите на работу в компанию, где до вас уже написано многое на Java и вспомогательных технологиях. У вас горят глаза, чешутся руки и вы хотите работать. И тут вас кидают в море легаси, где вместо документации крошки на клавиатуре от вашего предшественника.


Анна Абрамова на примере вымышленной системы расскажет, какими методами можно восстановить сакральные знания.




Что такое Работающий Продукт и как его делать, Антон Кекс


Антон Кекс регулярно сталкивается с проектами, у которых проблемы даже с элементарным запуском. Поэтому целый доклад на JPoint он посвятит тому, как делать на самом деле работающие продукты, которыми можно пользоваться и легко поддерживать как создателю, так и его коллегам.


Если ваша точка зрения не совпадет можно будет всё обсудить с Антоном в дискуссионной зоне. Вероятно, там будет жарко.




Enum в API коварство иллюзорной простоты, Илья Сазонов и Фёдор Сазонов


Вы уверены, что если добавить один маленький enum в API, то ничего страшного не произойдет? Или наоборот уверены, что так делать не стоит, но никто вас не слушает?
Рекомендуем вам доклад Ильи и Федора Сазоновых, пропитанный тяжелой болью по поводу бесконечных обновлений контрактов микросервисов.
Обычно подобные темы не выходят за пределы локального холивара в курилке, но нельзя же вечно добавлять новые значения в enum?




Dismantling technical debt and hubris, Shelley Lambert


Многие разработчики любят строить велосипеды, потому что свой велосипед он же надежнее, быстрее и удобнее, чем всё остальное. А потом все эти велосипеды складываются в один большой технический долг, с которым нужно что-то делать.
Шелли Ламберт научит вас не писать собственные решения для всего подряд, а правильно выбирать существующие.




Подводя итог


Надеемся, по программе ясность появилась, но напоследок подчеркнём, что онлайн-конференция это не только слушать доклады. Это ещё и:
Вопросы спикерам в чатах и видеосозвонах
Общение зрителей (на нашей виртуальной площадке можно подойти к другому зрителю и увидеть его по видеосвязи)
Виртуальные стенды партнёров с различными активностями


Напоминаем, поучаствовать во всём этом можно будет с 13 по 17 апреля в онлайне. Вся дополнительная информация и билеты на сайте.

Подробнее..

Перевод Как написать (игрушечную) JVM

15.12.2020 20:09:12 | Автор: admin
Статья про KVM оказалась интересной для читателей, поэтому сегодня публикуем новый перевод статьи Serge Zaitsev: как работает Java Virtual Machine под капотом.

Нравится нам это или нет, но Java один из наиболее распространенных и широко используемых языков программирования. Однако не каждый Java-разработчик настолько любопытен, чтобы заглянуть под капот и посмотреть, как устроена JVM.

Я попытаюсь написать игрушечную (и неполную) JVM, чтобы показать основные принципы ее работы. Надеюсь, эта статья вызовет у вас интерес и вдохновит на дальнейшее изучение JVM.

Наша скромная цель


Начнем с простого:

public class Add {  public static int add(int a, int b) {    return a + b;  }}

Мы компилируем наш класс с javac Add.java, и в результате получается Add.class. Этот class файл является бинарным файлом, который JVM может выполнять. Всё, что нам осталось сделать, создать такую JVM, которая бы выполняла его корректно.

Если мы заглянем внутрь Add.class с помощью шестнадцатеричного дампа мы, скорее всего, будем не слишком впечатлены:

00000000 ca fe ba be 00 00 00 34 00 0f 0a 00 03 00 0c 07 |.......4........|
00000010 00 0d 07 00 0e 01 00 06 3c 69 6e 69 74 3e 01 00 |........<init>..|
00000020 03 28 29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 |.()V...Code...Li|
00000030 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 00 03 |neNumberTable...|
00000040 61 64 64 01 00 05 28 49 49 29 49 01 00 0a 53 6f |add...(II)I...So|
00000050 75 72 63 65 46 69 6c 65 01 00 08 41 64 64 2e 6a |urceFile...Add.j|
00000060 61 76 61 0c 00 04 00 05 01 00 03 41 64 64 01 00 |ava........Add..|
00000070 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 |.java/lang/Objec|
00000080 74 00 21 00 02 00 03 00 00 00 00 00 02 00 01 00 |t.!.............|
00000090 04 00 05 00 01 00 06 00 00 00 1d 00 01 00 01 00 |................|
000000a0 00 00 05 2a b7 00 01 b1 00 00 00 01 00 07 00 00 |...*............|
000000b0 00 06 00 01 00 00 00 01 00 09 00 08 00 09 00 01 |................|
000000c0 00 06 00 00 00 1c 00 02 00 02 00 00 00 04 1a 1b |................|
000000d0 60 ac 00 00 00 01 00 07 00 00 00 06 00 01 00 00 |`...............|
000000e0 00 03 00 01 00 0a 00 00 00 02 00 0b |............|


Хотя мы еще не видим здесь четкой структуры, нам нужно найти способ ее разобрать: что значит ()V и (II)I, <init> и почему файл начинается с cafe babe?

Вероятно, вы знакомы с другим способом выгрузить class файлы, часто он оказывается более полезным:

$ javap -c Add
Compiled from "Add.java"
public class Add {
public Add();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
}


Теперь мы видим наш класс, его конструктор и метод. И конструктор, и метод содержат несколько инструкций. Становится более-менее ясно, что делает наш метод add(): он загружает два аргумента (iload_0 и iload_1), суммирует их и возвращает результат. JVM это стековая машина, поэтому в ней нет регистров, все аргументы инструкций хранятся во внутреннем стеке, и результаты также помещаются в стек.

Class loader


Как нам добиться того же результата, который показала программа javap? Как разобрать class файл?

Если обратиться к спецификации JVM, мы узнаем о структуре файлов формата class. Он всегда начинается с 4-байтовой подписи (CAFEBABE), затем 2 + 2 байта для версии. Звучит просто!

Поскольку нам пришлось бы читать байты, short, int и байтовые последовательности из бинарного файла, начнем реализацию нашего загрузчика следующим образом:

type loader struct {r   io.Readererr error}func (l *loader) bytes(n int) []byte {b := make([]byte, n, n)        // мы не хотим обрабатывать ошибки на каждом этапе,        // поэтому просто сохраним первую найденную ошибку до конца        // и ничего не делаем, если ошибка была найденаif l.err == nil {_, l.err = io.ReadFull(l.r, b)}return b}func (l *loader) u1() uint8  { return l.bytes(1)[0] }func (l *loader) u2() uint16 { return binary.BigEndian.Uint16(l.bytes(2)) }func (l *loader) u4() uint32 { return binary.BigEndian.Uint32(l.bytes(4)) }func (l *loader) u8() uint64 { return binary.BigEndian.Uint64(l.bytes(8)) }// Usage:f, _ := os.Open("Add.class")loader := &loader{r: f}cafebabe := loader.u4()major := loader.u2()minor := loader.u2()

Затем спецификация сообщает нам, что необходимо распарсить пул констант. Что это? Так называется специальная часть class файла, которая содержит константы, необходимые для запуска класса. Все строки, числовые константы и ссылки хранятся там, и каждая имеет уникальный индекс uint16 (таким образом, класс может иметь до 64К констант).

В пуле есть несколько типов констант, каждый из которых содержит свой набор значений. Нам могут быть интересны:

  • UTF8: простой строковый литерал
  • Class: индекс строки, содержащей имя класса (косвенная ссылка)
  • Name and type: индекс имени типа и дескриптор, используемый для полей и методов
  • Field and method references: индексы, относящиеся к классам и константам типа name and type.

Как видите, константы в пуле часто ссылаются друг на друга. Поскольку мы пишем JVM на языке Go и здесь нет объединения типов (union types), давайте создадим тип Const, который будет содержать в себе различные возможные поля констант:

type Const struct {Tag              byteNameIndex        uint16ClassIndex       uint16NameAndTypeIndex uint16StringIndex      uint16DescIndex        uint16String           string}type ConstPool []Const

Затем, следуя спецификации JVM, мы могли бы извлечь данные пула констант следующим образом:

func (l *loader) cpinfo() (constPool ConstPool) {constPoolCount := l.u2()// Валидные индексы пула констант начинаются с 1for i := uint16(1); i < constPoolCount; i++ {c := Const{Tag: l.u1()}switch c.Tag {case 0x01: // UTF8 string literal, 2 bytes length + datac.String = string(l.bytes(int(l.u2())))case 0x07: // Class indexc.NameIndex = l.u2()case 0x08: // String reference indexc.StringIndex = l.u2()case 0x09, 0x0a: // Field and method: class index + NaT indexc.ClassIndex = l.u2()c.NameAndTypeIndex = l.u2()case 0x0c: // Name-and-typec.NameIndex, c.DescIndex = l.u2(), l.u2()default:l.err = fmt.Errorf("unsupported tag: %d", c.Tag)}constPool = append(constPool, c)}return constPool}

Сейчас мы сильно упрощаем себе задачу, но в реальной JVM нам пришлось бы обрабатывать типы констант long и double единообразно, вставляя дополнительный неиспользуемый постоянный элемент, как сообщает нам спецификация JVM (поскольку постоянные элементы считаются 32-битными).

Чтобы упростить получение строковых литералов по индексам, реализуем метод Resolve(index uint16) string:

func (cp ConstPool) Resolve(index uint16) string {if cp[index-1].Tag == 0x01 {return cp[index-1].String}return ""}

Теперь нам нужно добавить похожие помощники для анализа списка интерфейсов, полей и методов классов и их атрибутов:

func (l *loader) interfaces(cp ConstPool) (interfaces []string) {interfaceCount := l.u2()for i := uint16(0); i < interfaceCount; i++ {interfaces = append(interfaces, cp.Resolve(l.u2()))}return interfaces}// Тип field используется и для полей и методовtype Field struct {Flags      uint16Name       stringDescriptor string Attributes []Attribute }// Атрибуты содержат дополнительную информацию о полях и классах// Самый полезный - это атрибут Code, который содержит актуальный байтовый кодtype Attribute struct {Name stringData []byte}func (l *loader) fields(cp ConstPool) (fields []Field) {fieldsCount := l.u2()for i := uint16(0); i < fieldsCount; i++ {fields = append(fields, Field{Flags:      l.u2(),Name:       cp.Resolve(l.u2()),Descriptor: cp.Resolve(l.u2()),Attributes: l.attrs(cp),})}return fields}func (l *loader) attrs(cp ConstPool) (attrs []Attribute) {attributesCount := l.u2()for i := uint16(0); i < attributesCount; i++ {attrs = append(attrs, Attribute{Name: cp.Resolve(l.u2()),Data: l.bytes(int(l.u4())),})}return attrs}

И поля, и методы представлены как Fields, это очень удобно, позволяет сэкономить время. Наконец, мы можем собрать всё это вместе и полноценно распарсить наш класс:

type Class struct {ConstPool  ConstPoolName       stringSuper      stringFlags      uint16Interfaces []stringFields     []FieldMethods    []FieldAttributes []Attribute}func Load(r io.Reader) (Class, error) {loader := &loader{r: r}c := Class{}loader.u8()           // magic (u32), minor (u16), major (u16)cp := loader.cpinfo() // const pool infoc.ConstPool = cpc.Flags = loader.u2()             // access flagsc.Name = cp.Resolve(loader.u2())  // this classc.Super = cp.Resolve(loader.u2()) // super classc.Interfaces = loader.interfaces(cp)c.Fields = loader.fields(cp)    // fieldsc.Methods = loader.fields(cp)   // methodsc.Attributes = loader.attrs(cp) // methodsreturn c, loader.err}

Теперь, если мы посмотрим на полученную информацию о классе, мы увидим, что у него нет полей и два метода <init>:()V и add:(II)I. Что за римские числа со скобками? Это дескрипторы. Они определяют, какие типы аргументов принимает метод и что он возвращает. В этом случае <init> (синтетический метод, используемый для инициализации объектов при их создании) не принимает аргументов и ничего не возвращает (V=void), в то время как метод add принимает два типа данных int (I=int32) и возвращает целое число.

Байт-код


Если присмотреться внимательно, мы поймем, что каждый метод в нашем разобранном классе имеет один атрибут с именем Code. Этот атрибут содержит часть байтов в качестве полезной нагрузки. Байты:

<init>:
[0 1 0 1 0 0 0 5 42 183 0 1 177 0 0 0 1 0 7 0 0 0 6 0 1 0 0 0 1]
add:
[0 2 0 2 0 0 0 4 26 27 96 172 0 0 0 1 0 7 0 0 0 6 0 1 0 0 0 3]


В спецификации, на этот раз в разделе bytecode, мы прочтем, что атрибут Code начинается со значения maxstack (2 байта), затем maxlocals (2 байта), длина кода (4 байта) и затем фактический код. Итак, наши атрибуты можно читать так:

<init>: maxstack: 1, maxlocals: 1, code: [42 183 0 1 177]
add: maxstack: 2, maxlocals: 2, code: [26 27 96 172]


Да, у нас есть только 4 и 5 байтов кода в каждом методе. Что означают эти байты?

Как я уже сказал, JVM это стековая машина. Каждая инструкция кодируется как один байт, за которым могут следовать дополнительные аргументы. Заглянув в спецификацию, мы увидим, что метод add имеет следующие инструкции:

26 = iload_0
27 = iload_1
96 = iadd
172 = ireturn


Точно такие же, как мы видели в выводе javap в начале! Но как это сделать?

Фреймы JVM


Когда метод выполняется внутри JVM, у него есть собственный стек для временных операндов, свои локальные переменные и фрагмент кода для выполнения. Все эти параметры хранятся в едином фрейме выполнения. Кроме того, фреймы содержат указатель текущей инструкции (насколько далеко мы продвинулись при выполнении байт-кода) и указатель на класс, в котором содержится метод. Последний нужен для доступа к пулу констант класса, а также к другим деталям.

Давайте создадим метод, который создает фрейм для конкретного метода, вызванного с заданными аргументами. Я буду использовать здесь тип interface{} в качестве типа Value, хотя использование правильного объединения типов (union types), конечно, было бы более безопасным.

type Frame struct {Class  ClassIP     uint32Code   []byteLocals []interface{}Stack  []interface{}}func (c Class) Frame(method string, args ...interface{}) Frame {for _, m := range c.Methods {if m.Name == method {for _, a := range m.Attributes {if a.Name == "Code" && len(a.Data) > 8 {maxLocals := binary.BigEndian.Uint16(a.Data[2:4])frame := Frame{Class:  c,Code:   a.Data[8:],Locals: make([]interface{}, maxLocals, maxLocals),}for i := 0; i < len(args); i++ {frame.Locals[i] = args[i]}return frame}}}}panic("method not found")}

Итак, мы получили Frame с инициализированными локальными переменными, пустым стеком и предварительно загруженным байт-кодом. Пришло время выполнить байт-код:

func Exec(f Frame) interface{} {for {op := f.Code[f.IP]log.Printf("OP:%02x STACK:%v", op, f.Stack)n := len(f.Stack)switch op {case 26: // iload_0f.Stack = append(f.Stack, f.Locals[0])case 27: // iload_1f.Stack = append(f.Stack, f.Locals[1])case 96:a := f.Stack[n-1].(int32)b := f.Stack[n-2].(int32)f.Stack[n-2] = a + bf.Stack = f.Stack[:n-1]case 172: // ireturnv := f.Stack[n-1]f.Stack = f.Stack[:n-1]return v}f.IP++}}

Наконец, мы можем собрать все вместе и запустить, вызвав наш метод add():

f, _ := os.Open("Add.class")class, _ := Load(f)frame := class.Frame("add", int32(2), int32(3))result := Exec(frame)log.Println(result)// OUTPUT:OP:1a STACK:[]OP:1b STACK:[2]OP:60 STACK:[2 3]OP:ac STACK:[5]5

Итак, всё работает. Да, это очень слабая JVM на минималках, но все же она делает то, что и должна JVM: загружает байт-код и интерпретирует его (конечно, настоящая JVM делает гораздо большее).

Чего не хватает?


Оставшихся 200 инструкций, среды выполнения, системы типов ООП и еще нескольких вещей.

Существует 11 групп инструкций, большинство из которых банальны:

  • Constants (помещает null, small number или значения из пула констант на стек).
  • Loads (помещает в стек локальные переменные). Подобных инструкций 32.
  • Stores (перемещают из стека в локальные переменные). Еще 32 скучных инструкции.
  • Stack (pop/dup/swap), как в любой стековой машине.
  • Math (add/sub/div/mul/rem/shift/logic). Для разных типов значений, всего 36 инструкций.
  • Conversions (int в short, int в float и т.д.).
  • Comparisons (eq/ne/le/). Полезно для создания условных выражений, например с if/else.
  • Control (goto/return). Пригодится для циклов и подпрограмм.
  • References. Самая интересная часть, поля и методы, исключения и мониторы объекта.
  • Extended. На первый взгляд выглядит не очень красивым решением. И, вероятно, со временем это не изменится.
  • Reserved. Здесь находится инструкция точки останова 0xca.

Большинство инструкций несложно реализовать: они берут один или два аргумента из стека, выполняют над ними некоторую операцию и отправляют результат. Единственное, что здесь следует иметь в виду, инструкции для типов long и double предполагают, что каждое значение занимает два слота в стеке, поэтому вам могут потребоваться дополнительные push() и pop(), что затрудняет группировку инструкций.

Для реализации References необходимо подумать об объектной модели: как вы хотите хранить объекты и их классы, как представлять наследование, где хранить поля экземпляров и поля классов. Кроме того, здесь вы должны быть осторожны с диспетчеризацией методов существует несколько инструкций invoke, и они ведут себя по-разному:

  • invokestatic: вызвать статический метод в классе, никаких сюрпризов.
  • invokespecial: вызвать метод экземпляра напрямую, в основном используется для синтетических методов, таких как <init> или приватных методов.
  • invokevirtual: вызвать метод экземпляра на основе иерархии классов.
  • invokeinterface: вызывает метод интерфейса, аналогичный invokevirtual, но выполняет другие проверки и оптимизацию.
  • invokedynamic: вызвать динамически вычисляемый call site, в новой версии Java 7, полезно для динамических методов и MethodHandles.

Если вы создаете JVM на языке без сборки мусора, в таком случае вам следует подумать, как ее выполнить: подсчет ссылок, алгоритм пометок (mark-and-sweep) и т. д. Обработка исключений путем реализации athrow, их распространения через фреймы и обработка с помощью таблиц исключений еще одна интересная тема.

Наконец, ваша JVM останется бесполезной, если нет runtime классов. Без java/lang/Object вы вряд ли даже увидите, как работает new инструкция при создании новых объектов. Ваша среда выполнения может предоставлять некоторые общие классы JRE из пакетов java.lang, java.io и java.util, или это может быть что-то более специфичное для домена. Скорее всего, некоторые методы в классах должны быть реализованы изначально, а не на Java. Это поднимет вопрос о том, как найти и выполнить такие методы, и станет еще одним крайним случаем для вашей JVM.

Другими словами, создать правильную JVM не так уж и просто, однако разобраться, как именно она работает, не сложно.

У меня, например, на это ушли всего одни летние выходные. Моей JVM еще предстоит долгий путь, но структура выглядит более или менее ясной: https://github.com/zserge/tojvm (замечания всегда приветствуются!)

Фактические фрагменты кода из этой статьи еще меньше и доступны здесь как gist.

Если появилось желание изучить вопрос лучше, вы можете подумать об использовании небольших JVM:


Надеюсь, эта статья не отбила ваш интерес к Java. Виртуальные машины это весело, а виртуальная машина Java действительно заслуживает внимательного рассмотрения.

Надеюсь, вам понравилась моя статья. Вы можете следить за моей работой через Github или Twitter, а также подписываться через rss.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru