Вступление


Недавно при работе над проектом учебной практики возникла потребность из своего кода порождать произвольный процесс и одновременно читать его stdout и stderr. Так как приложение пишется исключительно для linux, я решил заодно разобраться с epoll. Для запуска процесса на просторах интернета была найдена маленькая библиотека, делающая как раз то, что нужно, да еще и оборачивающая ввод-вывод в привычные потоки из стандартной библиотеки (речь о <iostream>).


Вооружившись несколькими статьями про epoll, я уже было собирался писать код, если бы не одно «но» — для epoll нужен доступ к «сырым» файловым дескрипторам, а автор библиотеки не предоставляет public-доступа к ним. Методы класса, возвращающие дескрипторы, скрыты под грифом «protected».

Что делать?


Самым простым было бы исправить код библиотеки и переместить нужные методы в public-секцию, еще лучше было бы форкнуть библиотеку и реализовать необходимый функционал самому. Но первое было бы некрасиво и сулило бы конфликтами при обновлении библиотеки, а второе заняло бы слишком много времени на разбор кода библиотеки и последующее тестирование под несколькими разными *nix-системами.

Поэтому в голову пришла безумная третья мысль: почему бы не попытаться как-то красиво «взломать» ООП и «легально» получить доступ к protected-методу без вмешательства в исходный код библиотеки? О том, какие преграды возникли на этом пути и как помог C++14 в их преодолении, и пойдет рассказ в данной публикации.

Тестовое окружение


Для примера используем следующий простой код:

#include <iostream>

class A {
protected:
	int f(){ std::cout << "Protected" << std::endl; return 0; }
};

int main(int argc, char **argv){
	A a;
	int val = 1;
	//val = a.f(); // как добраться до f()?
	
	return val;
}

Компиляция всех примеров производится под Ubuntu 16.04 с помощью gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4).

Предупреждение: в следующих разделах представлен код, который не рекомендуется применять на продакшене!

Идея 1 — static-метод


Итак, глаза зажглись, задача поставлена, но как ее решить? Вспоминаем правила наследования в ООП: protected поля и методы доступны только в областях видимости самого класса и классов, наследующих его.

Первый шаг понятен: создать класс, наследующий целевой (в нашем случае это класс «A»). А так как мы хотим вызвать защищенный метод у уже существующего объекта, добраться до него нам должен помочь статический метод нашей обертки:

class B : public A {
public:
	static int _f(A &a){ return a.f(); }
};

//в main():
val = B::_f(a);

Все оказалось так просто? Не тут-то было! C++ запрещает обращение к защищенным членам родительского класса из дочернего, о чем нам вежливо напоминает компилятор:

Лог компиляции
access_protected_fields_hack.cpp: In member function ‘int B::_f(A&)’:
access_protected_fields_hack.cpp:15:6: error: ‘int A::f()’ is protected
  int f(){ std::cout << "Protected" << std::endl; return 0; }
      ^
access_protected_fields_hack.cpp:20:27: error: within this context
  int _f(A &a){ return a.f(); }


Идея 2 — подмена типа


Чистые помыслы дали осечку, поэтому далее в ход идут более «грязные» методы: обманем компилятор таким образом, чтобы он считал, что «a» является объектом класса «B», и после этого вызовем у него наш публичный метод:

class B : public A {
public:
	int _f(){ return f(); }
};

//в main():
B *b = (B *) &a;
val = b->_f();

Бинго! Этот код делает то, что нужно, в консоли мы видим заветное «protected» и код возврата 0.

Мы не используем виртуальное наследование и наследуем только один класс, поэтому структура класса «B» должна остаться в точности такой же, как у родительского «A». А значит и все виртуальные методы тоже останутся по тем же смещениям, что и у родительского класса. Получается, что мы как бы заставляем компилятор считать, что нужный нам метод не защищенный, а публичный, при этом никак не меняя сам объект.

Кажется, что задача решена. Для доступа к защищенному методу мы наследуемся от класса целевого объекта и засоряем этим область видимости; подсматриваем, какой тип возвращаемого значения нам нужен для функции или поля… И что, так каждый раз? Условие задачи было в красивом «взломе». Но является ли красивым такое решение? Очевидно, что нет.

Идея 3 — пишем макрос


Чтобы макрос был удобным, он должен обладать следующими свойствами:

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

Определимся, какие меняющиеся от класса к классу части кода нужно вынести «за скобки»:

  • Целевой объект, к защищенному члену которого мы хотим обратиться;
  • Имя защищенного члена;
  • Тип возвращаемого значения для нашей публичной функции (соответствует типу поля/метода, к которому мы хотим получить доступ);
  • Тип класса, от которого идет наследование.

Первые два пункта законно занимают свои места в списке аргументов макроса, а вот остальные два попытаемся выяснить внутри макроса с помощью C++14.

Таким образом, имеем такое объявление:

#define ACCESS_PROTECTED(OBJ, FLD) <код макроса>

Теперь решаем возникшие проблемы:

Встраиваемость

Чтобы обеспечить встраиваемость в другие выражения, код макроса сам должен быть выражением. Тут к нам на помощь приходит C++11 с лямбда-функциями. Можно обернуть весь код макроса в нее и тут же на месте вызвать.

Проблема засорения области видимости

С давних времен C++ позволяет определять анонимные классы и структуры, что как раз нам и нужно. А лямбда-функция создает свою изолированную область видимости, так что переменные, которые мы будем использовать внутри нее, не будут видны снаружи.

На данном моменте код макроса выглядит так
#define ACCESS_PROTECTED(OBJ, FLD) (([](??? &o) -> ??? {	class : ??? {	public: ??? _f(){ return this->FLD; }	} *a = (??? *) &o;	return a->_f();})(OBJ))


Автоматический вывод типов

Начиная с C++11, в языке доступны ключевые слова «auto» и «decltype» для автоматического вывода типов. Однако только с C++14 их можно использовать в объявлениях лямбда-функций и методов. А это как раз то, что нам нужно.

Остается проблема только с типом класса, от которого идет наследование. Так как в лямбда-функцию объект попадает не копированием, а передачей на него ссылки, то decltype(o) от объекта вернет нам не сам тип класса, а тип ссылки на него. От такого типа наследоваться нельзя, и компилятор соответствующе поругается:

access_protected_fields_hack.cpp:7:8: error: base type ‘A&’ fails to be a struct or class type
  class : public decltype(o) {\

На помощь приходит std::remove_reference из заголовочного файла <type_traits>. Эта шаблонная структура предоставляет доступ к типу класса объекта независимо от того, был ли передан сам класс или только ссылка на него.

Получаем окончательный код:

#include <iostream>
#include <type_traits>

#define ACCESS_PROTECTED(OBJ, FLD) (([](auto &o) -> auto {	class : public std::remove_reference<decltype(o)>::type {	public: auto _f(){ return this->FLD; }	} *a = (decltype(a)) &o;	return a->_f();})(OBJ))

class A {
protected:
	int f(){ std::cout << "Protected" << std::endl; return 0; }
};

int main(int argc, char **argv){
	A a;
	int val = 1;
	val = ACCESS_PROTECTED(a, f());
	
	return val;
}

Мне кажется, красиво. А как считаете вы?

А что же с конечным кодом?


Для сравнения с кодом, генерируемым при использовании макроса, был скомпилирован код, в котором метод f() класса «A» попросту сделан публичным и вызван. В обоих случаях при компиляции использовался флаг -O3.

Генерируемый компилятором код функции main() оказался одинаковым для обоих случаев:

Ассемблерный код
0000000000400730 <main>:
  400730:	48 83 ec 08          	sub    $0x8,%rsp
  400734:	ba 09 00 00 00       	mov    $0x9,%edx
  400739:	be 14 09 40 00       	mov    $0x400914,%esi
  40073e:	bf 60 10 60 00       	mov    $0x601060,%edi
  400743:	e8 b8 ff ff ff       	callq  400700 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt>
  400748:	bf 60 10 60 00       	mov    $0x601060,%edi
  40074d:	e8 be ff ff ff       	callq  400710 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@plt>
  400752:	31 c0                	xor    %eax,%eax
  400754:	48 83 c4 08          	add    $0x8,%rsp
  400758:	c3                   	retq   
  400759:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)


В нем присутствует лишь заинлайненное тело функции A::f().

Заключение


Возможности из нового стандарта позволяют удобно решать задачи, которые ранее решить было проблематично или вовсе невозможно. Мне было интересно найти применение некоторым «фишкам» языка в реальном проекте. Надеюсь, для вас публикация тоже оказалась полезной и ее было интересно читать.

P.S.: Что же до основной задачи, которую я решал в своем приложении, то скрепя сердце пришлось выкинуть свеженаписанный макрос. Все-таки совесть не позволила применять такой код в опенсорсном приложении. Про epoll также пришлось забыть, а чтение из stderr и stdout было реализовано с помощью istream::read_some() и sleep-ом на 50 миллисекунд между вызовами.
Поделиться с друзьями
-->

Комментарии (25)


  1. fogone
    07.03.2017 15:18
    +22

    Паблик Морозов жив.


  1. lookid
    07.03.2017 15:25
    +4

    Давайте еще скажем, что кто-то не знал, что компилятору с -O3 или -Ofast вообще всё равно на ООП.


    1. khim
      07.03.2017 22:47

      MSVC — очень даже не всё равно, увы. Хотя может в последних версиях поправили, но до 2010й версии можно было только через __forceinline убедить от всего этого избавляться.


  1. apro
    07.03.2017 15:52
    +10

    #define protected public
    # include <external_header>
    #undef protected


    1. Sirikid
      07.03.2017 16:44
      +1

      А если класс в разделяемой библиотеке будет работать?


    1. iassasin
      07.03.2017 19:40
      -1

      Интересный вариант! Причем и для private-членов должно сработать.
      Но есть один недостаток: то для «взлома» мне нужно было самому осознанно писать имя макроса, а ваше делает видимым вообще все и без макросов.
      Мне все же больше понравился вариант с using.


  1. Psionic
    07.03.2017 16:00
    +1

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


    1. qw1
      07.03.2017 22:35

      Увы, многие компиляторы следуют стандарту

      auto x = &A::f;

      test.cpp(11): error C2248: 'A::f': cannot access protected member declared in class 'A'


      1. Psionic
        08.03.2017 00:16
        +1

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


        #include <iostream>
        
        using namespace std;
        
        class A 
        {
            int t;
            void printT()
            {
                cout<<"t="<<t<<endl;
            };
            int alpha() 
            {
                cout<<this<<"->alpha()"<<endl;
                this->printT();
                return 0;
            }
            public:
                    A():t(0){;} 
                    void setT(int _){this->t = _;}          
        };
        
        class B
        {
            virtual int beta()
            {
                cout<<this<<"->beta()"<<endl;
                return 0;
            }
            public:
                virtual void gamma(){};
        };
        
        class C:public B
        {
            public:
                int beta(){return 0;};
            private:
                void gamma(){cout<<"Private function of class C "<<this<<"->gamma() called"<<endl;}
        };
        
        int main()
        {
            A e;
            e.setT(800);
            __asm 
            {
                lea edx, A::alpha       
                lea ecx, e
                call edx
            }
            C* e2 = (C*)new B;
            e2->beta();
            delete e2;
            B* e3 = new C;
            e3->gamma();
            delete e3;
            return 0;
        }
        

        Это в студии 2008 работало.


        1. qw1
          08.03.2017 10:13
          +2

          Да, так и в 2015-й работает. Внутри __asm не проверяется уровень доступа. Но, решение x86-only, а я уже весь софт таргетирую под x64, без вариантов.


  1. Vass
    07.03.2017 16:17
    +4

    .


  1. stargazr
    07.03.2017 16:24

    Если в классе есть хоть один шаблонный метод, то можно и private-члены вытащить, написав его специализацию для какого-нибудь dummy-типа. Выйдет еще "красивее".


  1. orcanoid
    07.03.2017 19:12
    +3

    Другой вариант с наследованием:

    class A
    {
    protected:
        void f() {}
    };
    
    class B: public A
    {
    public:
        A::f;
    };
    
    int main()
    {
        B b;
        b.f();
        return 0; 
    }
    


  1. nail333
    07.03.2017 19:12
    +5

    Не обязательно задавать параметры метода, можно сделать проще с помощью using.

    class A
    {
    protected:
    	int f() { return 0; }
    };
    class B : public A
    {
    public:
    	using A::f;
    };
    
    A a;
    static_cast<B&>(a).f();
    


    1. iassasin
      07.03.2017 19:35

      Действительно, забыл про эту фишку. Спасибо!


  1. 4e1
    07.03.2017 20:57
    +6

    class B: public A {
    public: using A::f;
    };
    



  1. dkdi
    08.03.2017 18:41
    +2

    Такое преобразование является неопределённым поведением. Даже если прямо сейчас это работает, нет никакой гарантии, что поведение не изменится в самом ближайшем будущем.
    Подробнее про то, почему это UB можно посмотреть на stackoverflow (теория)(похожий пример)


  1. newnon
    09.03.2017 14:19

    del


  1. morzhovets
    09.03.2017 14:38

    Кажется, есть простое решение, вообще не использующее грубых приведений типов.
    Работает вроде бы в любом компиляторе, C++11 тоже не требуется.

    static int _f(A &a){ return (a.*&B::f)(); }
    


    В студии работает и A::f вместо B::f.


    1. qw1
      09.03.2017 15:08

      В студии работает и A::f вместо B::f.
      Проверил в 2015-й:
      static int _f(A &a){ return (a.*&A::f)(); }
      test.cpp(8): error C2248: 'A::f': cannot access protected member declared in class 'A'

      зы. А, этот static int _f нужно поместить внутрь класса B. Но от создания лишнего класса не уходим ((


      1. morzhovets
        09.03.2017 15:30

        Ну да, это поправка к тому, что в статье называется Идея 1. То есть дополнительный класс B со статическим методом, но без каких-либо грубых преобразований типов.

        А по этой ссылке можно найти (более извращенный) способ достучаться и до приватных полей и функций. Там еще в комментариях полезные ссылки.


  1. sermp
    09.03.2017 15:59
    +1

    Если у Вас метод protected, зачем все так сложно? Почему нельзя унаследоваться от A и работать уже с классом-наследником?


    1. iassasin
      09.03.2017 16:21

      Работа с целевым классом в используемой мной библиотеке ведется не напрямую, а через еще один класс.
      Потоки из стандартной библиотеки устроены таким образом, что сначала реализуется наследник std::streambuf, а потом он используется в другом классе, наследующем std::iostream. Чтобы правильно внедрить свой streambuf, пришлось бы разбираться еще и в классе-наследнике iostream.
      Если знаете способ обойти это, буду признателен за совет.


      1. sermp
        10.03.2017 16:09
        +1

        Вы можете описать задачу более конкретно?
        Потому что в статье ни слова про такие трудности (не считая одного упоминания iostream). В статье Вы просто пишите, что метод, возвращающий файловый дескриптор находится в секции protected.


        1. iassasin
          11.03.2017 03:37

          Попытался построить простую модель решаемой задачи:

          Код
          class TBase {};
          
          class Target : public TBase {
          	protected: int target_function(){ return 5; }
          };
          
          class Base {
          	private: TBase *tbase;
          	public: Base(TBase *t) : tbase(t) {}
          };
          
          class Main : public Base {
          	private: Target t;
          	public:
          		Main() : Base(&t) {}
          		const Target &getTarget(){ return t; }
          };
          
          int main(){
          	Main m;
          	int val = m.getTarget().target_function(); //нужно получить это
          }