четверг, сентября 12, 2013

Почему не стоит привязываться к устаревшей Java

Прочитал замечательную статью про то почему опасно оставаться на Java 6. Вкратце мораль такова:

  1. Oracle прекратил поддержку Java 6 в феврале этого года, т.е. никаких официальных патчей можно не ждать;
  2. Oracle регулярно выпускает security патчи для Java 7;
  3. Люди в черных шляпах делают реверс-инжиниринг этих патчей и атакуют машины на которых крутится Java 6, для которых этих патчей не будет;
  4. Машин на которых крутится Java 6 примерно 50% от всех машин с Java.
Ну и кроме этого там есть ссылка на статью показывающую что всё больше эксплойтов создается для Java native layer независимо от версии Java.

В общем конечно удивительного мало, учитывая популярность Java и любовь финансового сектора к решениям на Java. Но тут прям обострение.

вторник, августа 06, 2013

Лучшая фича C++ в C

Лучшая фича C++ - деструктор. Точно не помню кто это сказал, по-моему Страуструп. Если задуматься это действительно так - если убрать деструкторы, то смысл использовать C++ пропадает, так как практически всё в C++ что отличает его от C построено на деструкторах (ну и на исключениях наверное, хотя некоторые лишены возможности их использовать в приказном порядке).

Например в нижеприведенном коде главное не лямбда с кастомным deleter'ом, можно ведь сделать отдельный класс для счетчика и в деструкторе сделать то же самое что в лямбде (ну кроме delete) - лямбда, unique_ptr с deleter'ом всё это не критично для идеи(хотя конечно их наличие создаёт удобства), а вот если будет отсутствовать деструктор, то придется писать в C-стиле и возникнет вопрос "а зачем нам вообще C++".

Disclaimer: не ищите в коде особый смысл, просто можете рассматривать как заготовку для локального дебаг логгинга - не важно где и как(через return или через исключение) мы выйдем из функции, но в лог запишется состояние счетчика на момент выхода(понятно что в реальности можно писать дамп объекта например, а не счетчика):
Так вот оказывается в C в gcc есть нестандартное расширение для эмуляции деструктора. Расширение выполнено в виде атрибута cleanup. Ниже листинг кода на C аналогичный C++ выше, ну правда я слегка всё упростил и исключений нет (я не стал их эмулировать через setjmp()/longjmp(), так как пост не об этом) - но идея та же.
Не очень красиво, но красота в C наводится с помощью макросов, поэтому это исправимо. Так что к термину "C++ без исключений" можно добавить "C(си) с деструкторами".

Кстати что меня удивляет это то что в C++ нет designated initializer, хотя в C он есть с прошлого века. При работе с C-библиотеками иногда очень не хватает... Наверное есть весомые причины почему его нет в C++, но лезть копаться в стандартах и парсерах чтобы понять конкретную проблему я пока не готов. :)

вторник, апреля 16, 2013

Воскрешение логов из дампа памяти (core dump)

Вся заметка про специфику под *nix подобными операционками. Но идея вобщем будет такой же и под Windows - инструментарий другой.

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

Конечно можно воткнуть обработчик сигналов(signal handler) и прикрутив его попробовать форсировать сброс буфера(если логгер в принципе имеет такую возможность) на диск перед смертью программы. Проблема в том что если уже случилось что-то ужасное мы уже не можем быть уверены что логгер находится в рабочем состоянии на момент как мы получили сигнал и попали в обработчик.

Если при этом у вас многопоточное приложение вам придётся пробежаться по всем потокам(кроме одного который и обрабатывает сигналы) чтобы заблокировать в них обработку сигналов в старом добром C стиле (ну я по крайней мере не видел аналога для pthread_sigmask даже в стандарте C++11). Да и для C++11 политика "не пишите поточных async'ов не заблокировав им сигналы" мягко говоря вряд ли будет популярной особенно если вы используете std::async с std::launch::async | std::launch::deferred и не знаете будет тут отдельный поток или нет. И если какой-нибудь поток пропустим и в итоге обработчик сигнала вызовется хрен знает в каком потоке и в худшем для нас случае(например если вы используете сторонний асинхронный логгер и вообще не можете контролировать создание им потока - то и заблокировать сигналы для него скорее всего не сможете) он вызовется в том же потоке что и логгер в момент когда логгер сделал lock перед записью - так что если мы в обработчике вызовем flush для логгера, то получим deadlock в большинстве логгеров(теоретически рекурсивный mutex тут поможет, но во первых не факт что он безопасен в случае async signal, да и нам это не поможет если логгер в виде сторонней либы). Вобщем чтобы не уползти в обсуждение сигналов страниц на 5, примем за аксиому  - обработчик сигналов должен быть простым и тупым, например записать бэктрейс в stderr и умереть.

Т.е. всё просто, если уж нам суждено умереть от чего-то ужасного(segfault и тп) - умираем не пытаясь шаманить с логгером. А потом лезем с помощью gdb в core dump и пытаемся восстановить недописанные логи.

Для начала проверьте что core dump у вас разрешён:
ulimit -c

Если команда выводит 0, то установите unlimited:
ulimit -c unlimited

Теперь напишем небольшую программу(не вздумайте использовать этот логгер как образец для подражания - код хорош только для наших исследовательских целей, за появление чего-то похожего в продакшене положен расстрел на месте) с псевдологгером с буфером и уроним её с core dump'ом. В классе мы запишем в начало буфера более-менее уникальную строку чтобы потом найти с помощью неё начало буфера в gdb. В случае использования не рукописного буфера придётся записывать уникальную строку в сообщение. В разных логгерах это можно сделать по разному - либо через кастомный форматтер, либо через контекстный дескриптор и тп.
/**
 * Run logging and then make core dump with not flushed log messages.
*/
#include <iostream>
#include <string>
#include <sstream>
#include <cstdlib>

using namespace std;


//! Пишем в буфер и флашим когда размер буфера > flushSize_
class PseudoLogger {
    private:
        string buffer_;
        const string cookie_;
        size_t flushSize_ = 50u;

        PseudoLogger():cookie_("my cookie") {
            resetBuffer();
        }

        void resetBuffer() {
            buffer_ = cookie_;
        }

        void flush() {
            cout << buffer_.substr(cookie_.size());
            resetBuffer();
        }
    public:
        ~PseudoLogger() {
            flush();
        }

        static PseudoLogger& getInstance() {
            static PseudoLogger logger;
            return logger;
        }

        void Log(string message) {
            if (message.size() > flushSize_)
                message.resize(flushSize_);

            buffer_ += message;
            buffer_ += string("\n");

            if (buffer_.size() > flushSize_)
                flush();
        }
};


int main(int argc, char** argv) {
    PseudoLogger& logger = PseudoLogger::getInstance();

    for (int i=1; i<20; ++i) {
        std::stringstream out;
        out << i;
        logger.Log(string("test message ") + out.str());
    }

    abort();

    return 0;
}
посмотреть на ideone
После запуска программа выдаст:
test message 1 
test message 2 
test message 3 
... 
test message 16 
test message 17 
test message 18 
Aborted (core dumped) 

Т.е. логгер успел вывести все сообщения кроме последнего "test message 19".

Теперь запустим
gdb program.out core

В gdb запускаем
maintenance info sections

Там нас интересует раздел для core файла (перед ним будет раздел exec файла), мы увидим что-то вроде этого(понятно что всё что ниже зависит от ОС, компилятора и флагов компиляции):

Core file:

    0x0000->0x0944 at 0x00000394: note0 READONLY HAS_CONTENTS
    0x0000->0x0044 at 0x000003f0: .reg/4230 HAS_CONTENTS
    0x0000->0x0044 at 0x000003f0: .reg HAS_CONTENTS
    0x0000->0x00a0 at 0x00000570: .auxv HAS_CONTENTS
    0x0000->0x006c at 0x00000a14: .reg2/4230 HAS_CONTENTS
    0x0000->0x006c at 0x00000a14: .reg2 HAS_CONTENTS
    0x0000->0x0200 at 0x00000a94: .reg-xfp/4230 HAS_CONTENTS
    0x0000->0x0200 at 0x00000a94: .reg-xfp HAS_CONTENTS
    0x8048000->0x8048000 at 0x00001000: load1 ALLOC READONLY CODE
    0x804a000->0x804b000 at 0x00001000: load2 ALLOC LOAD READONLY HAS_CONTENTS
    0x804b000->0x804c000 at 0x00002000: load3 ALLOC LOAD HAS_CONTENTS
    0x8623000->0x8644000 at 0x00003000: load4 ALLOC LOAD HAS_CONTENTS
    0xb74b4000->0xb74b6000 at 0x00024000: load5 ALLOC LOAD HAS_CONTENTS
    0xb74b6000->0xb74b6000 at 0x00026000: load6 ALLOC READONLY CODE
    0xb74f7000->0xb74f8000 at 0x00026000: load7 ALLOC LOAD READONLY HAS_CONTENTS
    0xb74f8000->0xb74f9000 at 0x00027000: load8 ALLOC LOAD HAS_CONTENTS
посмотреть на ideone
Нас интересуют сегменты load1, load2 и тп(на самом деле как раз конкретно load1 и load2 не интересуют, но вобщем нас интересует подмножество load сегментов). Попробуем поискать в них, но не всё что мы найдём будет тем что надо, потому что  последовательность символов "my cookie" должна встречаться ещё как минимум один раз, но в реальности в зависимости реализации string, ключей компиляции и тп оно может встретиться ещё много раз. Поэтому мы после каждого найденного случая будем смотреть на этот кусок памяти, и если после "my cookie" идет нуль-терминатор, то это не то что нам нужно - нам надо чтобы сразу после "my cookie" шло что-то похожее на лог-сообщение (в нашем случае мы вообще-то знаем что это будет, но в реальности конечно нет). В нашем случае из-за убогой реализации и отсутствия оптимизации мы найдём порядочно совпадений. Скажу только что в реальности, с асинхронным логерром где буфер сделан не в виде std::string, а через хардкорный malloc и буфера тасуются между фронтэндом и бэкэндом вы тоже можете найти немало совпадений и потратить время чтобы вытащить то что вам надо. Итак ищем так (передаём "my cookie" посимвольно, потому что иначе find будет искать строку с нуль терминатором, а нам это как раз не надо):

посмотреть на ideone
Вобщем то что нам надо лежит по адресу 0x86232fc а по адресу 0x8623344 последний буфер который мы послали в cout. Как бы мы поняли в реальности(здесь то мы знали) что нам нужно то что в 0x86232fc а не в 0x8623344? Для этого в логи и пишут точное время для каждого сообщения - мы бы выбрали кусок памяти в котором сообщения моложе самого последнего сообщения записанного в лог-файл.

Многие из вас заметят вполне логично, что нафиг тут gdb если можно взять core файл и в каком-нибудь редакторе в нём поискать нашу метку. Правда в gdb мы видим что в некоторых сегментах можно было и не искать - например очевидно что в READONLY load2 не лежит наш буфер. Ну и если вы пишете свой логгер то проще в начало буффера записать не текст, а что-то более компактное и относительно уникальное - например адрес какого-нибудь метода из того же класса логгера. Тогда в gdb вы можете получить этот адрес (чтобы потом искать уже его) с помощью такой команды:
(gdb) info address PseudoLogger::PseudoLogger

Опять же удобно бэктрейс посмотреть, да много чего можно. Но дело не в инструменте - дело в принципе.

P. S. Тут я привёл листинги из gdb после сборки на 32битной системе, просто чтобы в ширину было всё поуже и попроще читать. Кроме длины адресов разницы c 64битной системой в нашем случае никакой.