Vtable c: Виртуальные таблицы в С++ | Уроки С++
C++ vtables. Часть 1 (basics + multiple Inheritance) / undefined / Хабр
Всем привет! Перевод статьи подготовлен специально для студентов курса «Разработчик С++». Интересно развиваться в данном направлении? Приходите онлайн 13 декабря в 20:00 по мск. на мастер-класс «Практика использования Google Test Framework»!
В этой статье мы рассмотрим, как clang реализует vtables (таблицы виртуальных методов) и RTTI (идентификацию типа времени исполнения). В первой части мы начнем с базовых классов, а затем рассмотрим множественное и виртуальное наследование.
Обратите внимание, что в этой статье нам предстоит покопаться в двоичном представлении, сгенерированном для различных частей нашего кода, с помощью gdb. Это довольно низкий уровень, но я сделаю всю тяжелую работу за вас. Я не думаю, что большинство будущих постов будут описывать детали такого низкого уровня.
Дисклеймер: все написанное здесь зависит от реализации, может измениться в любой будущей версии, так что не следует на это полагаться. Мы рассматриваем это только в образовательных целях.
отлично, тогда давайте начнем.
Часть 1 — vtables — Основы
Давайте рассмотрим следующий код:
#include <iostream>
using namespace std;
class NonVirtualClass {
public:
void foo() {}
};
class VirtualClass {
public:
virtual void foo() {}
};
int main() {
cout << "Size of NonVirtualClass: " << sizeof(NonVirtualClass) << endl;
cout << "Size of VirtualClass: " << sizeof(VirtualClass) << endl;
}
$ # скомпилируйте и запустите main.cpp
$ clang++ main.cpp && ./a.out
Size of NonVirtualClass: 1
Size of VirtualClass: 8
NonVirtualClass
имеет размер 1 байт, потому что в C++ классы не могут иметь нулевой размер. Однако сейчас это не важно.
Размер VirtualClass
составляет 8 байт на 64-битной машине. Почему? Потому что внутри есть скрытый указатель, указывающий на vtable. vtables — это статические таблицы трансляции, создаваемые для каждого виртуального класса. Эта статья рассказывает об их содержании и о том, как они используются.
Чтобы получить более глубокое понимание того, как выглядят vtables, давайте рассмотрим следующий код с помощью gdb, чтобы выяснить, как распределена память:
#include <iostream>
class Parent {
public:
virtual void Foo() {}
virtual void FooNotOverridden() {}
};
class Derived : public Parent {
public:
void Foo() override {}
};
int main() {
Parent p1, p2;
Derived d1, d2;
std::cout << "done" << std::endl;
}
$ # скомпилируем наш код с отладочными символами и начнем отладку, используя gdb
$ clang++ -std=c++14 -stdlib=libc++ -g main.cpp && gdb ./a.out
...
(gdb) # установим gdb автоматически де-декорировать символы C++
(gdb) set print asm-demangle on
(gdb) set print demangle on
(gdb) # установим точку останова на main
(gdb) b main
Breakpoint 1 at 0x4009ac: file main. cpp, line 15.
(gdb) run
Starting program: /home/shmike/cpp/a.out
Breakpoint 1, main () at main.cpp:15
15 Parent p1, p2;
(gdb) # перейдем к следующей строке
(gdb) n
16 Derived d1, d2;
(gdb) # перейдем к следующей строке
(gdb) n
18 std::cout << "done" << std::endl;
(gdb) # выведем p1, p2, d1, d2 - мы скоро поговорим о том, что означает вывод
(gdb) p p1
$1 = {_vptr$Parent = 0x400bb8 <vtable for Parent+16>}
(gdb) p p2
$2 = {_vptr$Parent = 0x400bb8 <vtable for Parent+16>}
(gdb) p d1
$3 = {<Parent> = {_vptr$Parent = 0x400b50 <vtable for Derived+16>}, <No data fields>}
(gdb) p d2
$4 = {<Parent> = {_vptr$Parent = 0x400b50 <vtable for Derived+16>}, <No data fields>}
Вот что мы узнали из вышеизложенного:
— Несмотря на то, что у классов нет членов данных, существует скрытый указатель на vtable;
— vtable для p1 и p2 одинаков. vtables — это статические данные для каждого типа;
— d1 и d2 наследуют vtable-указатель от Parent, который указывает на vtable Derived;
— Все vtables указывают на смещение 16 (0x10) байтов в vtable. Это мы также обсудим позже.
Давайте продолжим нашу gdb-сессию, чтобы увидеть содержимое vtables. Я буду использовать команду x, которая выводит память на экран. Мы собираемся вывести 300 байтов в шестнадцатеричном формате, начиная с 0x400b40. Почему именно этот адрес? Потому что выше мы видели, что указатель vtable указывает на 0x400b50, а символ для этого адреса vtable for Derived+16 (16 == 0x10)
.
(gdb) x/300xb 0x400b40
0x400b40 <vtable for Derived>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b48 <vtable for Derived+8>: 0x90 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400b50 <vtable for Derived+16>: 0x80 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400b58 <vtable for Derived+24>: 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400b60 <typeinfo name for Derived>: 0x37 0x44 0x65 0x72 0x69 0x76 0x65 0x64
0x400b68 <typeinfo name for Derived+8>: 0x00 0x36 0x50 0x61 0x72 0x65 0x6e 0x74
0x400b70 <typeinfo name for Parent+7>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b78 <typeinfo for Parent>: 0x90 0x20 0x60 0x00 0x00 0x00 0x00 0x00
0x400b80 <typeinfo for Parent+8>: 0x69 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400b88: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b90 <typeinfo for Derived>: 0x10 0x22 0x60 0x00 0x00 0x00 0x00 0x00
0x400b98 <typeinfo for Derived+8>: 0x60 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400ba0 <typeinfo for Derived+16>: 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400ba8 <vtable for Parent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400bb0 <vtable for Parent+8>: 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400bb8 <vtable for Parent+16>: 0xa0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400bc0 <vtable for Parent+24>: 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
. ..
Примечание: мы смотрим на де-декорированные (demangled) символы. Если вам действительно интересно, _ZTV — это префикс для vtable, _ZTS — это префикс для строки типа (имени), а _ZTI для typeinfo.
Вот структура vtable Parent
:
Вот структура vtable Derived
:
1:
(gdb) # выясним, какой отладочный символ мы имеем для адреса
0x400aa0
(gdb) info symbol 0x400aa0
Parent::Foo() in section .text of a.out
2:
(gdb) info symbol 0x400a90
Parent::FooNotOverridden() in section .text of a.out
3:
(gdb) info symbol 0x400a80
Derived::Foo() in section .text of a.out
Помните, что указатель vtable в Derived указывал на смещение +16 байтов в vtable? Третий указатель является адресом указателя первого метода. Хотите третий метод? Нет проблем — добавьте 2 sizeof(void) к указателю vtable. Хотите запись typeinfo? перейдите к указателю перед ним.
Двигаемся дальше — как насчет структуры записей typeinfo?
Parent
:
А вот запись typeinfo Derived
:
1:
(gdb) info symbol 0x602090
vtable for __cxxabiv1::__class_type_info@@CXXABI_1.3 + 16 in section .bss of a.out
2:
(gdb) x/s 0x400b69
0x400b69 <typeinfo name for Parent>: "6Parent"
3:
(gdb) info symbol 0x602210
vtable for __cxxabiv1::__si_class_type_info@@CXXABI_1.3 + 16 in section .bss of a.out
4:
(gdb) x/s 0x400b60
0x400b60 <typeinfo name for Derived>: "7Derived"
Если вы хотите узнать больше о __si_class_type_info, вы можете найти некоторую информацию здесь, а также здесь.
Это исчерпывает мои навыки с gdb, а также завершает эту часть. Я предполагаю, что некоторые люди сочтут это слишком низким уровнем, или, возможно, просто не имеющим практической ценности. Если это так, я бы порекомендовал пропустить части 2 и 3, перейдя прямо к части 4.
Часть 2 — Множественное наследование
Мир иерархий одиночного наследования проще для компилятора. Как мы видели в первой части, каждый дочерний класс расширяет родительский vtable, добавляя записи для каждого нового виртуального метода.
Давайте рассмотрим множественное наследование, которое усложняет ситуацию, даже когда наследование реализуется только чисто от интерфейсов.
Посмотрим на следующий фрагмент кода:
class Mother {
public:
virtual void MotherMethod() {}
int mother_data;
};
class Father {
public:
virtual void FatherMethod() {}
int father_data;
};
class Child : public Mother, public Father {
public:
virtual void ChildMethod() {}
int child_data;
};
Обратите внимание, что есть 2 указателя vtable. Интуитивно я бы ожидал 1 или 3 указателя (Mother, Father и Child). На самом деле невозможно иметь один указатель (подробнее об этом далее), и компилятор достаточно умен, чтобы объединять записи дочерней vtable Child как продолжение vtable Mother, сохраняя таким образом 1 указатель.
Почему у Child не может быть одного указателя vtable для всех трех типов? Помните, что указатель Child может быть передан в функцию, принимающую указатель Mother или Father, и оба будут ожидать, что указатель this будет содержать правильные данные в правильных смещениях. Эти функции не обязательно должны знать о Child, и определенно не следует предполагать, что Child — это действительно то, что находится под указателем Mother/Father, которым они оперируют.
(1) Не имеет отношения к этой теме, но, тем не менее, интересно, что child_data фактически помещается в заполнении Father. Это называется «tail padding» и может быть темой будущего поста.
Вот структура vtable
:
В этом примере экземпляр Child будет иметь тот же указатель при приведении к указателю Mother. Но при приведении к указателю Father компилятор вычисляет смещение указателя this, чтобы указать на _vptr$Father часть Child (3-е поле в структуре Child, см. таблицу выше).
Другими словами, для данного Child c;: (void)&c != (void)static_cast<Father*>(&c). Некоторые люди не ожидают этого, и, возможно, однажды эта информация сэкономит вам время на отладку.
Я находил это полезным не один раз. Но подождите, это еще не все.
Что, если Child решил переопределить один из методов Father? Рассмотрим этот код:
class Mother {
public:
virtual void MotherFoo() {}
};
class Father {
public:
virtual void FatherFoo() {}
};
class Child : public Mother, public Father {
public:
void FatherFoo() override {}
};
Ситуация становится сложнее. Функция может принимать аргумент Father* и вызывать FatherFoo() для него. Но если вы передадите экземпляр Child, ожидается, что он вызовет переопределенный метод Child с правильным указателем this. Тем не менее, вызывающий не знает, что он действительно содержит Child. Он имеет указатель на смещение Child, где находится расположение Father. Кто-то должен выполнить смещение указателя this, но как это сделать? Какую магию выполняет компилятор, чтобы заставить это работать?
Прежде чем мы ответим на это, обратите внимание, что переопределение одного из методов Mother не очень мудрено, так как указатель this одинаков. Child знает, что нужно читать после vtable Mother, и ожидает, что методы Child будут сразу после нее.
Вот решение: компилятор создает метод ”переходник” (thunk), который исправляет указатель this, а затем вызывает “настоящий” метод. Адрес метода-переходника будет находиться под vtable Father, в то время как “настоящий” метод будет под vtable Child.
Вот vtable Child
:
0x4008e8 <vtable for Child>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x4008f0 <vtable for Child+8>: 0x60 0x09 0x40 0x00 0x00 0x00 0x00 0x00
0x4008f8 <vtable for Child+16>: 0x00 0x08 0x40 0x00 0x00 0x00 0x00 0x00
0x400900 <vtable for Child+24>: 0x10 0x08 0x40 0x00 0x00 0x00 0x00 0x00
0x400908 <vtable for Child+32>: 0xf8 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0x400910 <vtable for Child+40>: 0x60 0x09 0x40 0x00 0x00 0x00 0x00 0x00
0x400918 <vtable for Child+48>: 0x20 0x08 0x40 0x00 0x00 0x00 0x00 0x00
Что означает:
Объяснение: как мы видели ранее, у Child есть 2 vtables — одна используется для Mother и Child, а другая для Father. В vtable Father, FatherFoo() указывает на “переходник”, а в vtable Child указывает непосредственно на Child::FatherFoo().
А что в этом “переходнике”, спросите вы?
(gdb) disas /m 0x400820, 0x400850
Dump of assembler code from 0x400820 to 0x400850:
15 void FatherFoo() override {}
0x0000000000400820 <non-virtual thunk to Child::FatherFoo()+0>: push %rbp
0x0000000000400821 <non-virtual thunk to Child::FatherFoo()+1>: mov %rsp,%rbp
0x0000000000400824 <non-virtual thunk to Child::FatherFoo()+4>: sub $0x10,%rsp
0x0000000000400828 <non-virtual thunk to Child::FatherFoo()+8>: mov %rdi,-0x8(%rbp)
0x000000000040082c <non-virtual thunk to Child::FatherFoo()+12>: mov -0x8(%rbp),%rdi
0x0000000000400830 <non-virtual thunk to Child::FatherFoo()+16>: add $0xfffffffffffffff8,%rdi
0x0000000000400837 <non-virtual thunk to Child::FatherFoo()+23>: callq 0x400810 <Child::FatherFoo()>
0x000000000040083c <non-virtual thunk to Child::FatherFoo()+28>: add $0x10,%rsp
0x0000000000400840 <non-virtual thunk to Child::FatherFoo()+32>: pop %rbp
0x0000000000400841 <non-virtual thunk to Child::FatherFoo()+33>: retq
0x0000000000400842: nopw %cs:0x0(%rax,%rax,1)
0x000000000040084c: nopl 0x0(%rax)
Как мы уже обсуждали — смещения this и вызов FatherFoo(). И на сколько мы должны сместить this, чтобы получить Child? top_offset!
Обратите внимание, что я лично считаю, что имя non-virtual thunk чрезвычайно запутанно, поскольку это запись в виртуальной таблице для виртуальной функции. Я не уверен, что тут не виртуального, но это только мое мнение.
Пока на этом всё, в ближайшем будущем переведем 3 и 4 части. Следите за новостями!
C++ vtables. Часть 1 (basics + multiple Inheritance) / OTUS. Онлайн-образование corporate blog / Habr
Всем привет! Перевод статьи подготовлен специально для студентов курса «Разработчик С++». Интересно развиваться в данном направлении? Приходите онлайн 13 декабря в 20:00 по мск. на мастер-класс «Практика использования Google Test Framework»!
В этой статье мы рассмотрим, как clang реализует vtables (таблицы виртуальных методов) и RTTI (идентификацию типа времени исполнения). В первой части мы начнем с базовых классов, а затем рассмотрим множественное и виртуальное наследование.
Обратите внимание, что в этой статье нам предстоит покопаться в двоичном представлении, сгенерированном для различных частей нашего кода, с помощью gdb. Это довольно низкий уровень, но я сделаю всю тяжелую работу за вас. Я не думаю, что большинство будущих постов будут описывать детали такого низкого уровня.
Дисклеймер: все написанное здесь зависит от реализации, может измениться в любой будущей версии, так что не следует на это полагаться. Мы рассматриваем это только в образовательных целях.
отлично, тогда давайте начнем.
Часть 1 — vtables — Основы
Давайте рассмотрим следующий код:
#include <iostream>
using namespace std;
class NonVirtualClass {
public:
void foo() {}
};
class VirtualClass {
public:
virtual void foo() {}
};
int main() {
cout << "Size of NonVirtualClass: " << sizeof(NonVirtualClass) << endl;
cout << "Size of VirtualClass: " << sizeof(VirtualClass) << endl;
}
$ # скомпилируйте и запустите main. cpp
$ clang++ main.cpp && ./a.out
Size of NonVirtualClass: 1
Size of VirtualClass: 8
NonVirtualClass
имеет размер 1 байт, потому что в C++ классы не могут иметь нулевой размер. Однако сейчас это не важно.
Размер VirtualClass
составляет 8 байт на 64-битной машине. Почему? Потому что внутри есть скрытый указатель, указывающий на vtable. vtables — это статические таблицы трансляции, создаваемые для каждого виртуального класса. Эта статья рассказывает об их содержании и о том, как они используются.
Чтобы получить более глубокое понимание того, как выглядят vtables, давайте рассмотрим следующий код с помощью gdb, чтобы выяснить, как распределена память:
#include <iostream>
class Parent {
public:
virtual void Foo() {}
virtual void FooNotOverridden() {}
};
class Derived : public Parent {
public:
void Foo() override {}
};
int main() {
Parent p1, p2;
Derived d1, d2;
std::cout << "done" << std::endl;
}
$ # скомпилируем наш код с отладочными символами и начнем отладку, используя gdb
$ clang++ -std=c++14 -stdlib=libc++ -g main. cpp && gdb ./a.out
...
(gdb) # установим gdb автоматически де-декорировать символы C++
(gdb) set print asm-demangle on
(gdb) set print demangle on
(gdb) # установим точку останова на main
(gdb) b main
Breakpoint 1 at 0x4009ac: file main.cpp, line 15.
(gdb) run
Starting program: /home/shmike/cpp/a.out
Breakpoint 1, main () at main.cpp:15
15 Parent p1, p2;
(gdb) # перейдем к следующей строке
(gdb) n
16 Derived d1, d2;
(gdb) # перейдем к следующей строке
(gdb) n
18 std::cout << "done" << std::endl;
(gdb) # выведем p1, p2, d1, d2 - мы скоро поговорим о том, что означает вывод
(gdb) p p1
$1 = {_vptr$Parent = 0x400bb8 <vtable for Parent+16>}
(gdb) p p2
$2 = {_vptr$Parent = 0x400bb8 <vtable for Parent+16>}
(gdb) p d1
$3 = {<Parent> = {_vptr$Parent = 0x400b50 <vtable for Derived+16>}, <No data fields>}
(gdb) p d2
$4 = {<Parent> = {_vptr$Parent = 0x400b50 <vtable for Derived+16>}, <No data fields>}
Вот что мы узнали из вышеизложенного:
— Несмотря на то, что у классов нет членов данных, существует скрытый указатель на vtable;
— vtable для p1 и p2 одинаков. vtables — это статические данные для каждого типа;
— d1 и d2 наследуют vtable-указатель от Parent, который указывает на vtable Derived;
— Все vtables указывают на смещение 16 (0x10) байтов в vtable. Это мы также обсудим позже.
Давайте продолжим нашу gdb-сессию, чтобы увидеть содержимое vtables. Я буду использовать команду x, которая выводит память на экран. Мы собираемся вывести 300 байтов в шестнадцатеричном формате, начиная с 0x400b40. Почему именно этот адрес? Потому что выше мы видели, что указатель vtable указывает на 0x400b50, а символ для этого адреса vtable for Derived+16 (16 == 0x10)
.
(gdb) x/300xb 0x400b40
0x400b40 <vtable for Derived>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b48 <vtable for Derived+8>: 0x90 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400b50 <vtable for Derived+16>: 0x80 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400b58 <vtable for Derived+24>: 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400b60 <typeinfo name for Derived>: 0x37 0x44 0x65 0x72 0x69 0x76 0x65 0x64
0x400b68 <typeinfo name for Derived+8>: 0x00 0x36 0x50 0x61 0x72 0x65 0x6e 0x74
0x400b70 <typeinfo name for Parent+7>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b78 <typeinfo for Parent>: 0x90 0x20 0x60 0x00 0x00 0x00 0x00 0x00
0x400b80 <typeinfo for Parent+8>: 0x69 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400b88: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b90 <typeinfo for Derived>: 0x10 0x22 0x60 0x00 0x00 0x00 0x00 0x00
0x400b98 <typeinfo for Derived+8>: 0x60 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400ba0 <typeinfo for Derived+16>: 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400ba8 <vtable for Parent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400bb0 <vtable for Parent+8>: 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400bb8 <vtable for Parent+16>: 0xa0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400bc0 <vtable for Parent+24>: 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
. ..
Примечание: мы смотрим на де-декорированные (demangled) символы. Если вам действительно интересно, _ZTV — это префикс для vtable, _ZTS — это префикс для строки типа (имени), а _ZTI для typeinfo.
Вот структура vtable Parent
:
Вот структура vtable Derived
:
1:
(gdb) # выясним, какой отладочный символ мы имеем для адреса
0x400aa0
(gdb) info symbol 0x400aa0
Parent::Foo() in section .text of a.out
2:
(gdb) info symbol 0x400a90
Parent::FooNotOverridden() in section .text of a.out
3:
(gdb) info symbol 0x400a80
Derived::Foo() in section .text of a.out
Помните, что указатель vtable в Derived указывал на смещение +16 байтов в vtable? Третий указатель является адресом указателя первого метода. Хотите третий метод? Нет проблем — добавьте 2 sizeof(void) к указателю vtable. Хотите запись typeinfo? перейдите к указателю перед ним.
Двигаемся дальше — как насчет структуры записей typeinfo?
Parent
:
А вот запись typeinfo Derived
:
1:
(gdb) info symbol 0x602090
vtable for __cxxabiv1::__class_type_info@@CXXABI_1.3 + 16 in section .bss of a.out
2:
(gdb) x/s 0x400b69
0x400b69 <typeinfo name for Parent>: "6Parent"
3:
(gdb) info symbol 0x602210
vtable for __cxxabiv1::__si_class_type_info@@CXXABI_1.3 + 16 in section .bss of a.out
4:
(gdb) x/s 0x400b60
0x400b60 <typeinfo name for Derived>: "7Derived"
Если вы хотите узнать больше о __si_class_type_info, вы можете найти некоторую информацию здесь, а также здесь.
Это исчерпывает мои навыки с gdb, а также завершает эту часть. Я предполагаю, что некоторые люди сочтут это слишком низким уровнем, или, возможно, просто не имеющим практической ценности. Если это так, я бы порекомендовал пропустить части 2 и 3, перейдя прямо к части 4.
Часть 2 — Множественное наследование
Мир иерархий одиночного наследования проще для компилятора. Как мы видели в первой части, каждый дочерний класс расширяет родительский vtable, добавляя записи для каждого нового виртуального метода.
Давайте рассмотрим множественное наследование, которое усложняет ситуацию, даже когда наследование реализуется только чисто от интерфейсов.
Посмотрим на следующий фрагмент кода:
class Mother {
public:
virtual void MotherMethod() {}
int mother_data;
};
class Father {
public:
virtual void FatherMethod() {}
int father_data;
};
class Child : public Mother, public Father {
public:
virtual void ChildMethod() {}
int child_data;
};
Обратите внимание, что есть 2 указателя vtable. Интуитивно я бы ожидал 1 или 3 указателя (Mother, Father и Child). На самом деле невозможно иметь один указатель (подробнее об этом далее), и компилятор достаточно умен, чтобы объединять записи дочерней vtable Child как продолжение vtable Mother, сохраняя таким образом 1 указатель.
Почему у Child не может быть одного указателя vtable для всех трех типов? Помните, что указатель Child может быть передан в функцию, принимающую указатель Mother или Father, и оба будут ожидать, что указатель this будет содержать правильные данные в правильных смещениях. Эти функции не обязательно должны знать о Child, и определенно не следует предполагать, что Child — это действительно то, что находится под указателем Mother/Father, которым они оперируют.
(1) Не имеет отношения к этой теме, но, тем не менее, интересно, что child_data фактически помещается в заполнении Father. Это называется «tail padding» и может быть темой будущего поста.
Вот структура vtable
:
В этом примере экземпляр Child будет иметь тот же указатель при приведении к указателю Mother. Но при приведении к указателю Father компилятор вычисляет смещение указателя this, чтобы указать на _vptr$Father часть Child (3-е поле в структуре Child, см. таблицу выше).
Другими словами, для данного Child c;: (void)&c != (void)static_cast<Father*>(&c). Некоторые люди не ожидают этого, и, возможно, однажды эта информация сэкономит вам время на отладку.
Я находил это полезным не один раз. Но подождите, это еще не все.
Что, если Child решил переопределить один из методов Father? Рассмотрим этот код:
class Mother {
public:
virtual void MotherFoo() {}
};
class Father {
public:
virtual void FatherFoo() {}
};
class Child : public Mother, public Father {
public:
void FatherFoo() override {}
};
Ситуация становится сложнее. Функция может принимать аргумент Father* и вызывать FatherFoo() для него. Но если вы передадите экземпляр Child, ожидается, что он вызовет переопределенный метод Child с правильным указателем this. Тем не менее, вызывающий не знает, что он действительно содержит Child. Он имеет указатель на смещение Child, где находится расположение Father. Кто-то должен выполнить смещение указателя this, но как это сделать? Какую магию выполняет компилятор, чтобы заставить это работать?
Прежде чем мы ответим на это, обратите внимание, что переопределение одного из методов Mother не очень мудрено, так как указатель this одинаков. Child знает, что нужно читать после vtable Mother, и ожидает, что методы Child будут сразу после нее.
Вот решение: компилятор создает метод ”переходник” (thunk), который исправляет указатель this, а затем вызывает “настоящий” метод. Адрес метода-переходника будет находиться под vtable Father, в то время как “настоящий” метод будет под vtable Child.
Вот vtable Child
:
0x4008e8 <vtable for Child>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x4008f0 <vtable for Child+8>: 0x60 0x09 0x40 0x00 0x00 0x00 0x00 0x00
0x4008f8 <vtable for Child+16>: 0x00 0x08 0x40 0x00 0x00 0x00 0x00 0x00
0x400900 <vtable for Child+24>: 0x10 0x08 0x40 0x00 0x00 0x00 0x00 0x00
0x400908 <vtable for Child+32>: 0xf8 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0x400910 <vtable for Child+40>: 0x60 0x09 0x40 0x00 0x00 0x00 0x00 0x00
0x400918 <vtable for Child+48>: 0x20 0x08 0x40 0x00 0x00 0x00 0x00 0x00
Что означает:
Объяснение: как мы видели ранее, у Child есть 2 vtables — одна используется для Mother и Child, а другая для Father. В vtable Father, FatherFoo() указывает на “переходник”, а в vtable Child указывает непосредственно на Child::FatherFoo().
А что в этом “переходнике”, спросите вы?
(gdb) disas /m 0x400820, 0x400850
Dump of assembler code from 0x400820 to 0x400850:
15 void FatherFoo() override {}
0x0000000000400820 <non-virtual thunk to Child::FatherFoo()+0>: push %rbp
0x0000000000400821 <non-virtual thunk to Child::FatherFoo()+1>: mov %rsp,%rbp
0x0000000000400824 <non-virtual thunk to Child::FatherFoo()+4>: sub $0x10,%rsp
0x0000000000400828 <non-virtual thunk to Child::FatherFoo()+8>: mov %rdi,-0x8(%rbp)
0x000000000040082c <non-virtual thunk to Child::FatherFoo()+12>: mov -0x8(%rbp),%rdi
0x0000000000400830 <non-virtual thunk to Child::FatherFoo()+16>: add $0xfffffffffffffff8,%rdi
0x0000000000400837 <non-virtual thunk to Child::FatherFoo()+23>: callq 0x400810 <Child::FatherFoo()>
0x000000000040083c <non-virtual thunk to Child::FatherFoo()+28>: add $0x10,%rsp
0x0000000000400840 <non-virtual thunk to Child::FatherFoo()+32>: pop %rbp
0x0000000000400841 <non-virtual thunk to Child::FatherFoo()+33>: retq
0x0000000000400842: nopw %cs:0x0(%rax,%rax,1)
0x000000000040084c: nopl 0x0(%rax)
Как мы уже обсуждали — смещения this и вызов FatherFoo(). И на сколько мы должны сместить this, чтобы получить Child? top_offset!
Обратите внимание, что я лично считаю, что имя non-virtual thunk чрезвычайно запутанно, поскольку это запись в виртуальной таблице для виртуальной функции. Я не уверен, что тут не виртуального, но это только мое мнение.
Пока на этом всё, в ближайшем будущем переведем 3 и 4 части. Следите за новостями!
Vtable — это… Что такое Vtable?
Таблица виртуальных методов (англ. virtual method table, VMT) — координирующая таблица или vtable — механизм, используемый в языках программирования для поддержки динамического соответствия (или метода позднего связывания).
Допустим, программа содержит несколько классов в иерархии наследования: суперкласс Cat
и два подкласса HouseCat
и Lion
. Класс Cat
определяет виртуальную функцию speak
, так что его подклассы могут обеспечивать соответствующую реализацию (т. е. или «мяу» или «рык»).
Когда программа вызывает метод speak
по указателю Cat
(который может указывать на класс Cat
или любой подкласс Cat
), контекстное окружение (среда запуска) должна уметь определять, какая именно реализация вызывается, в зависимости от текущего типа указываемого объекта.
Существует множество различных способов реализации подобного динамического связывания, но решение при помощи vtable весьма распространено в C++ и родственных языках (как например, D и C#). Языки, в которых есть разделение на программный интерфейс объектов и их реализацию, как Visual Basic и Delphi, также склоняются к использованию аналогов vtable, так как это позволяет объектам использовать другую реализацию просто используя другой набор указателей метода.
Реализация
Координирующая таблица объекта содержит адреса динамически связанных методов объекта. Метод вызывается при выборке адреса метода из таблицы. Координирующая таблица будет той же самой для всех объектов, принадлежащих тому же классу, поэтому допускается ее совместное использование. Объекты, принадлежащие классам, совместимым по типу (например, стоящие на одной ступени в иерархии наследования), будут иметь схожие координирующие таблицы: адрес данного метода зафиксируется с одним и тем же самым смещением для всех классов, совместимых по типу. Таким образом, выбирая адрес метода из данной координирующей таблицы смещением получим метод, связанный с текущим классом объекта.[1]
В стандартах C++ нет четкого определения как должна реализовываться динамическая координация, но компиляторы зачастую используют некоторые вариации одной и той же базовой модели.
Обычно компилятор создает отдельную vtable для каждого класса. После создания объекта указатель на эту vtable, называемый виртуальный табличный указатель или vpointer, добавляется как скрытый член данного объекта (а зачастую как первый член). Компилятор также генерирует «скрытый» код в конструкторе каждого класса для инициализации vpointer’ов его объектов адресами соответствующей vtable.
Пример
Рассмотрим следующие объявления класса в синтаксисе C++:
class B1 { public: void f0() {} virtual void f1() {} int int_in_b1; }; class B2 { public: virtual void f2() {} int int_in_b2; };
используем для создания следующего класса:
class D : public B1, public B2 { public: void d() {} void f2() {} // переопределяем B2::f2() int int_in_d; };
и следующий фрагмент C++ кода:
B2 *b2 = new B2(); D *d = new D();
G++ 3.4.6 из набора GCC создает следующую 32-битную схему памяти для объекта b2
:[nb 1]
b2: +0: указатель на таблицу виртуальных методов B2 +4: значение int_in_b2 таблица виртуальных методов B2: +0: B2::f2()
а для объекта d
схема памяти будет такой:
d: +0: указатель на ТВМ D (для B1) +4: значение int_in_b1 +8: указатель на ТВМ D (для B2) +12: значение int_in_b2 +16: значение int_in_d Всего: 20 Bytes. ТВМ D (для B1): +0: B1::f1() // B1::f1() не переопределена ТВМ D (для B2): +0: D::f2() // B2::f2() переопределена D::f2()
Необходимо отметить, что невиртуальные функции (такие как f0
) в общем случае не могут появляться в vtable, но в некоторых случаях есть исключения (как, например, конструктор по умолчанию).
Переопределение метода f2()
в классе D
реализуется дублированием ТВМ B2
и заменой указателя на B2::f2()
указателем на D::f2()
.
Множественное наследование
Компилятор C++ реализует множественное наследование классов B1
и B2
в класс D
, используя две таблицы виртуальных методов, по одной для каждого базового класса. (Есть и другие способы реализации множественного наследования, но данный наиболее распространенный.) Это приводит к потребности в «указателях на адресную запись» (связках) при создании.
Рассмотрим следующий C++ код:
D *d = new D(); B1 *b1 = dynamic_cast<B1*>(d); B2 *b2 = dynamic_cast<B2*>(d);
В то время как d
и b1
указывают на одно место в памяти после выполнения данного кода, b2
будет указывать на участок памяти d+8
(смещение на восемь байт относительно участка d
). Таким образом, b2
указывает на область памяти внутри d
, что «выглядит» как сущность B2
, т.е. имеет ту же схему размещения в памяти, что и сущность B2
.
Вызов
Вызов d->f1()
происходит при разыменовании vpointer D::B1
из d
: просмотр записи о f1
в vtable, а затем разыменование этого указателя вызывает код.
В случае одиночного наследования (или в случае языка с поддержкой только одиночного наследования), если vpointer всегда является первым элементом в d
(как это происходит у многих компиляторов), то это решается следующим псевдо-C++ кодом:
В более общем случае, как упоминалось выше, вызов f1()
, D::f2()
и B2::f2()
на d
будет сложнее
*((d->/*указатель на таблицу виртуальных методов D (для B1)*/)[0])(d) *((d->/*указатель на таблицу виртуальных методов D (для B1)*/)[12])(d) *((d->/*указатель на таблицу виртуальных методов D (для B2)*/)[0])(d+8)
Для сравнения, вызов d->f0()
гораздо проще:
Эффективность
Виртуальный вызов требует как минимум дополнительно индексированного разыменования, а иногда дополнительной «адресной привязки» (fixup), схожей с невиртуальным вызовом, который является простым переходом к скомпилированному указателю. Поэтому вызов виртуальных функций по сути медленнее, чем вызов невиртуальных. Опыты показывают, что примерно 6-13% времени исполнения тратится просто на поиск соответствующей функции, при этом дополнительные расходы могут достигать[2].
К тому же, в среде где
Для избежания подобных потерь компиляторы обычно избегают использования vtable всегда, когда вызов может быть выполнен во время компиляции.
Таким образом, вышеприведеный вызов f1
может и не требовать просмотра vtable, так как компилятор может сообщить о том, что d
может иметь в этой точке только D
, а D
не переопределяет f1
. Или компилятор (как вариант, оптимизатор) может обнаружить отсутствие подклассов B1
в программе, переопределяющей f1
. Вызов B1::f1
или B2::f2
вероятно не потребует просмотра vtable благодаря реализации, определенной явным образом (хотя все еще требуется привязка по указателю ‘this’).
Сравнение с альтернативами
Vtable в общем случае жертвует производитльностью для достижения динамического выбора, но существует множство альтернатив ей, как например, выбор по двоичному дереву, обладающий более высокой производительностью, но различной скоростью исполнения[3].
Тем не менее, vtable предусмотрена только для единичной диспетчеризации (single dispatch) по специальному параметру «this», в отличие от множественной диспетчеризации (multiple dispatch) (как в Dylan), где типы всех параметров могут быть присвоены в ходе диспетчеризации.
Vtable также работает только если диспетчеризация ограничена известным набором методов, поэтому множество vtable могут быть помещены в простой массив во время компиляции, в отличие от языков с поддержкой утиной типизации (например, Python или JIT-компиляции), а время диспетчеризации часто не производит значимого влияния на общее время обработки, но несмотря на это, просмотр vtable заметно быстрее. Vtable также проще реализовывать и отлаживать, а кроме того еще и ближе к «философии языка Си» нежели хэш-таблицы строк.
Дополнительные источники
Виртуальные функции – низкоуровневый взгляд (статья)
См. также
Примечания
- ↑ Аргумент G++
-fdump-class-hierarchy
может быть использован для сброса ТВМ для «ручной» проверки. Для компилятора AIX VisualAge XlC используется-qdump_class_hierarchy
для сброса иерархии классов и схемы ТВФ.
Источники
- ↑ Ellis & Stroustrup 1990, pp. 227–232
- ↑ Driesen, Karel and Hölzle, Urs, «The Direct Cost of Virtual Function Calls in C++», OOPSLA 1996
- ↑ Zendra, Olivier and Driesen, Karel, «Stress-testing Control Structures for Dynamic Dispatch in Java», Pp. 105–118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM ’02)
Ссылки
- Margaret A. Ellis and Bjarne Stroustrup (1990) The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley. (ISBN 0-201-51459-1)
Wikimedia Foundation.
2010.
Arduino IDE: разбираемся с меню Tools и некоторыми другими функциями.
Итальянцы молодцы. Запустили в мир DIY настоящего монстра. С появлением экосистемы Arduino весь рынок цифрового DIY получил мощнейший толчок. Тысячи, сотни тысяч, миллионы пользователей принялись покупать оригинальные и совместимые платы для их последующего программирования и использования в своих поделках. Причина такой популярности в первую очередь связывают с низким порогом для входа в эту среду. Вам не нужно тратить много денег на приобретение плат: открытый стандарт дал дорогу на рынок множеству последователей и копировщиков. Вам так же не стоит сильно заморачиваться над программированием: поставляемая «в комплекте» Arduino IDE упрощена настолько, насколько это возможно. Пользователь просто пишет код и не задумывается о настройках компилятора, о том, что программировать при помощи Arduino IDE можно разными программаторами, а иногда и вообще голые микросхемы.
И вот, наступает момент, когда стандартная Arduino IDE становится тесновата, а ее неудобство начинает просто выбешивать, особенно, если пользователь знаком с более продвинутыми средами разработки по другим платформам и языкам программирования. Кто-то начинает мигрировать на что-то более интересное, например, на Atom или VS.Code в смеси с PlatformIO, а кто-то старается выжать из Arduino IDE все, что только можно.
Сегодня я выступлю как раз в роли подобного «особо интересующегося персонажа» и попробую вместе с читателем разобраться чуть-чуть побольше в тонкостях Arduino IDE. Все ниженаписанное более-менее соответствует версии 1.8.9 Arduino IDE.
Темная тема для Arduino IDE
Очень многих раздражает простенькое оформление Arduino IDE в корпоративных цветах Arduino. Все такое белое и «слепое». Долго работать в таком цветовом решении не всегда комфортно, особенно если это ночь, вокруг темно и все спят.
Вариант темной темы под Mac. Скриншот с GitHub.
Для такого случая сторонний разработчик разработал так называемую темную тему, тему которая позволяет вернуть знакомый с детства вариант черного фона и светлого текста на нем. Тема не идеальна, так как не позволяет полностью настроить все нюансы, но ограничение это связано не с самой темой, а с ограничениями Arduino IDE. Тему, при желании, можно подредактировать самостоятельно.
Меню Sketch
Я, как и большинство других пользователей Arduino IDE, загружаю свои скетчи в платы при помощи панели инструментов. Щелкнул на кнопку загрузки и скетч начал заливать в плату. Удобно. Но, как оказывается, меню Sketch (здесь и далее я буду использовать английский вариант интерфейса Arduino) содержит не мало интересных и полезных новшеств. Итак, кратенько пробежимся по ним.
Меню Sketch Arduino IDE
Пункты меню Very/Compile и Upload стандартны и знакомы всем. Именно они используются для проверки правильности кода и последующей нормальной загрузки. А вот пункт Upload using programmer для меня до недавнего времени оставался Terra incognita. Как оказалась, данный пункт необходимо применять при загрузки скетча без использования загрузчика (bootloader), но с использованием полноценного программатора. О загрузчиках и программаторах поговорим немного позже, пока же просто запомним этот пункт.
В этом же меню присутствует весьма полезный пункт Export compiled Binary. Применяется он в том же направлении, что и Upload using programmer. По умолчанию Arduino IDE компилирует исходный код в файл, который сохраняется во временную директорию и после завершения загрузки она должна очищаться. Временная директория находится в каких-то дебрях файловой системы, а искать там скомпилированный файл с непонятным наименованием — то еще удовольствие. Так вот функция Export compiled Binary как раз и применяется для того, чтобы сохранить уже откомпилированный скетч в виде бинарных-файлов в папке самого скетча. В дальнейшем скомпилированные скетчи могут быть прошиты при помощи внешнего программатора или же USB-TTL преобразователя.
Кстати, просмотреть папку скетча можно все через тоже самое меню Sketch при помощи пункта Show Sketch Folder. Кстати, при вызове Export compiled Binary компилируется сразу два файла: с и без загрузчика. Для чего это нужно, опять же немного ниже. При компилировании при помощи Export compiled Binary следует учитывать, что примеры из поставки Arduino так откомпилировать не выйдет. Папки, в которых хранятся эти скетчи имеют режим только для чтения, а IDE необходимо куда-то записать скомпилированные файлы (обычно в таком случае возникает ошибка «Export canceled, changes must first be saved.»). Поэтому тренируемся на своих собственных скетчах или же пересохраняем скетч в новую папку.
Меню Tools
Меню Tools в Arduino IDE дает куда больше интересного и полезного, нежели Sketch. Более того, сам вид меню зависит от того, какую платформу вы выбрали в качестве целевой.
Меню Tools Arduino IDE с выбранным микроконтроллером семейства AVR.
Начнем с общего и тех пунктов, которые есть в оригинальных платах, построенных на микроконтроллерах Atmel (ее все же купила Microchip).
Пункт Auto Format позволяет отформатировать ваш исходный текст по общепринятому в Arduino IDE стандарту. Отформатированный текст позволяет легче находить ошибки, да и сама программа выглядит опрятно. Так же, зачастую, при форматировании при помощи автоформата получается найти такие ошибки, как отсутствующие скобки, что при большом количестве вложений несколько затруднительно. Оная же функция вызывается и в самом редакторе в контекстном меню доступном по правой кнопке мышки.
Archive Sketch позволяет упростить навигацию по папке в которой хранятся все ваши скетчи. Напомню, что каждый скетч хранится в отдельной папке, куда складывается все, что к нему относится. Если скетчей у вас много, особенно если плодить версии в новых папках, то разобраться во всем этом нагромождении становится очень сложно. Для этого и можно применить функцию архивирования. Папка со скетчем будет сжата в архив ZIP, а затем стерта из каталога. Останется только архив.
Serial Plotter противовес Serial Monitor предназначен для вывода графика из данных выдаваемых в последовательный порт. Все, что от вас требуется для построения графика — выплевывать в порт цифры при помощи Serial.println(). Неплохой пример использования плоттера приведен на страницах проекта wikihandbk.
Приведенные выше пункты стандартны. Их использование, вкупе с Library Manager должны быть известны даже неопытным пользователям. А дальше попробуем погрузиться в чащобы Arduino IDE и мир программирования микроконтроллеров.
WiFi101 / WiFiNINA Firmware Updater
Сетевое взаимодействие для оригинальных плат Arduino всегда было ахиллесовой пятой. Их просто нет в стандартной поставке, а все решения в виде внешних плат настолько ди́ки, что просто пропадает всякое желание их использовать. По этой причине многие используют платы на основе ESP8266, которые дешевле, мощнее и сразу с WiFi на борту. Но итальянцы, да и ардуинщики вообще, ребята упорные, поэтому вместо того, чтобы переходить на нормальные платы, продолжают изобретать очередные ужасы, к которым и относятся WiFi101 и WiFiNINA.
WiFi 101 Shield. Взято откуда-то с просторов сети.
Собственно этот пункт меню предназначен для обновления прошивки и сертификатов соответствующих плат.
Arduino Uno с платой WiFi под библиотеку WiFiNINO
Дичь полнейшая и подобные платы лучше не использовать, так как они до жути непопулярны, стоят дорого, потребляют уйму энергии. В общем, гораздо проще и элегантнее все решается на базе плат семейства ESP. В целом на Arduino присутствует подробный туториал, где разжевано как и что обновляется при помощи данной функции.
ESP Exception Decoder
Плагин для Arduino IDE под непонятным названием ESP Exception Decoder предназначен для более легкой отладки программ под платформу ESP8266. Exception или «исключение» обозначает возникновение ошибочной ситуации которая не была корректно обработана кодом. Например, деление на 0 однозначно вызовет исключение, и работа программы будет прервана. В языке, на котором программируют в Arduino IDE нет возможности использовать блоки try {} catch {}, соответственно работа с исключениями невозможна (точнее по синтаксису он есть, но компилятор его отвергнет, по крайней мере для плат на основе AVR). Программист обязан предусматривать и проверять данные и предупреждать возникновение подобных ситуаций. Иначе результат может быть непредсказуемым.
На первоначальной экосистеме Arduino, построенной на Atmel AVR (об этом опять же дальше) нет места исключениям. И даже несмотря на то, что язык, применяемый для программирования в Arduino IDE есть ни что иное как диалект C++, в нем так же нет исключений и они не поддерживаются компилятором, ибо в AVR нет исключений, слишком там мало места для еще и исключений. Сильно жизнь это не осложняет, просто программы пишутся куда более аккуратно. А вот в экосистеме ESP, в том числе и под Arduino, исключения присутствуют.
И в этом возникает проблема. При возникновении исключительной ситуации, того же деления на 0, микроконтроллер, например, esp8266 выдаст в последовательный порт информацию о возникновении исключения, стек вызовов функций, где оно возникло, сохранит некоторые сведения во внутренней памяти (к ним можно будет обратиться после перезагрузки).
Вот такую вот информацию сохраняет esp8266 после перезагрузки. В этом случае произошла перезагрузка по питанию.
Система в чипах ESP более замороченная, чем в AVR и при возникновении нештатной ситуации выплевывает в последовательный порт некий стек адресов, по которым можно добраться до истинной причины возникновения ошибки. Но выдаются адреса в совсем нечитаемом человеком виде. Чтобы сопоставить строчку в исходном коде с конкретным адресом, выданным в стеке, и был придумал ESP Exception Decoder.
Намеренный вызов исключения на WeMos Mini
После возникновения ошибки необходимо перенести стек в декодировщик и он выдаст «путь» по которому можно дойти до строки в годе где и возникла исключительная ситуация. В некоторых случаях функция исключительно удобная. Ознакомиться с подробной инструкцией по применению декодировщика можно на страничке проекта ESP Arduino Core.
Get Board Info
Пожалуй, самая бесполезная функция, которое позволяет, в некоторых случаях, понять, какая плата подключена к компьютеру для прошивки. В большинстве случаев вы это и так знаете. А если не знаете, то функция отобразит, опять же, в большинстве случаев информацию только об оригинальных платах.
Burn Bootloader
Наконец-то мы начинаем подбираться к действительно интересным пунктам меню. Начнем с пункта загрузки загрузчика (Burn Bootloader). Но для начала попытаемся понять, что такое bootloader и для чего он нужен. В старые далекие ламповые времена тоже существовали программируемые микросхемы. Но программировались они весьма жестким способом. Использовался специальный программатор, в который вставлялась микросхема, происходило таинство, и микросхему можно было использовать в оборудовании.
Сей способ с одной стороны достаточно прост, но с другой он очень неудобен. Ведь чтобы сменить прошивку на уже прошитой микросхеме, если это вообще возможно, ее необходимо вытащить из устройства, установить в программатор и повторить процедуру. Что далеко не всегда возможно, так как микросхема может быть впаянной в плату и без специализированного оборудования достать ее оттуда нельзя или же само устройство летает на спутнике. Именно для этих целей была придумана методика ISP (In-System Programming), что в переводе означает ни что иное как «внутрисхемное программирование». ISP не стоит путать с SPI (Serial Peripheral Interface). ISP это именно внутрисхемная прошивка, а SPI это стандарт взаимодействия между устройствами. И пусть даже вас не вводят в заблуждения сильно похожие разъемы и наименования контактов. ISP и SPI это разные вещи.
В общем случае ISP позволяет программировать микроконтроллер через любой его интерфейс и даже программироваться самостоятельно с изменением своего кода посредством своего собственного же кода. Но для осуществления подобных фокусов требуется некая программа, которая будет сидеть в микроконтроллере и управлять этим программированием. Именно она и называется загрузчиком (bootloader). И именно благодаря ей есть возможность прошивать микроконтроллеры просто подключив их к USB-порту, или же применив простейший программатор на параллельном порту компьютера (у кого они еще остались), а в некоторых случаях обновление прошивки можно загружать вообще по воздуху применяя прием OTA (Over The Air).
Вообще микроконтроллер можно прошить и без загрузчика. Именно для этих целей и предназначен соответствующий пункт в меню Sketch. В этом случае места под ваш скетч в памяти микроконтроллера будет больше. Однако, в последствии уже нельзя будет загрузить обновление прошивки обыкновенным путем. Придется при помощи программатора либо прошить загрузчик, либо далее прошивать прошивку без загрузчиков.
Для Arduino существует несколько вариантов загрузчиков. Стандартный, который прошивается еще при изготовлении платы Arduino, многие не любят. Дескать он притормаживает при загрузке устройства в ожидании передачи данных для прошивки и только спустя десять секунд запускает ваш прошитый скетч. Дело в том, что некоторые платы Arduino, в которых не реализован механизм DTR (Data Terminal Ready) / RTS (Ready To Send) действительно нужно прошивать очень быстро иначе bootloader просто передаст управление той микропрограмме, что уже находится в микропроцессоре. Вот и начали все, кому не лень, модифицировать штатный загрузчик или же писать свой собственный.
Стандартные загрузчики для плат Arduino
Где брать загрузчики? Вообще стандартный загрузчик можно загрузить прямо из Arduino. Для этих целей и предназначен пункт меню Burn Bootloader. А сами загрузчики из стандартной поставки можно найти в папке с установленным Arduino в поддиректории hardware. Если же вы используете платы отличные от Arduino AVR, например, ESP3288, то все дополнительные загрузчики и прочие инструменты можно найти в личном профиле пользователя (AppData\Local).
Загрузчик для плат семейства ESP
Но если вы скачиваете дополнительные загрузчики и хотите установить их в Arduino IDE, то можно использовать папку hardware в вашем каталоге для скетчей. После перезагрузки IDE загрузчики из этой папки так же будут доступны, однако не следует забывать про файл boards.txt который должен присутствовать в подпапках папки hardware. Если у вас вдруг нет текстового редактора или же вы испытываете трудности с редактированием boards.txt то есть решение. Один энтузиаст создал специальную программу Arduino BOARDS.TXT Editor, которая позволяет с легкостью вносить изменения в указанный файл.
Так, где же брать эти самые альтернативные загрузчики? Искать в сети. Поисковики выдают просто немыслимое количество разнообразных вариантов. Для начала можно остановиться на OptiBoot, попробовать калькулятор «формирующий» загрузчик под конкретную плату или начать просматривать ссылки с официальной страницы загрузчиков Arduino.
И подводя промежуточный итог, если нам нужно загрузить прошивку без загрузчика (так для нее останется больше места) в микроконтроллер, то подключаем его к программатору и загружаем прошивку через Sketch – Upload Using Programmer. Если же хотим вернуться к обычной и удобной загрузке скетчей через среду Arduino по USB и без прочих танцев африканских народов, то прошиваем загрузчик обратно (или же мы купили микроконтроллер без загрузчика) уже посредством Tools – Burn Bootloader. В обоих случаях незабываем про корректный выбор платы, программатора и прочих настроек.
И еще раз, если в плате установлен загрузчик, то после перезагрузки платы он первым получит управление, послушает немного выделенные порты в надежде если вдруг там кто-то хочет залить прошивку. Если признаков заливания прошивки нет, то он со спокойной душой передает управление основной программе. А если в плате нет загрузчика, то тогда управление после перезагрузки сразу передается основной программе. Все просто, по крайней мере на пальцах.
AVR и прочие
Для лучшего понимания дальнейшей информации я хочу привести некоторые сведения о самих микроконтроллерах с которых пошло-поехало Arduino. Мы уже знаем, что оригинальные Arduino построены на микроконтроллерах (больших интегральных схемах, которые можно программировать и которые могут работать и сами по себе без внешней обвязки, подавай только напряжение) от компании Atmel (нынче Microchip).
У компании Atmel наработано несколько линеек микроконтроллеров, но нас в первую очередь интересует линейка AVR (кстати, аббревиатура AVR официально никак не расшифровывается), так как именно на ней и построены оригинальные Arduino. Линейка появилась аж в 1996 году и, пожалуй, была одна из первых на рынке предложившей решение, где микроконтроллер и память (ОЗУ и ПЗУ) расположены в рамках одного единственного кристалла. Повторюсь, чтобы система работала достаточно только одной микросхемы AVR на которую подается питание. Все остальное у нее уже есть внутри.
AVR линейка делится на несколько основных групп:
- tinyAVR – с уменьшенным количеством линий ввода-вывода, уменьшенным количеством памяти всех типов, обозначается как ATtinyxxx.
- megaAVR – увеличенное количество памяти всех типов, увеличенное количество линий ввода-вывода, расширенный набор для периферии и тому подобное. Обозначается как ATmegaxxx.
- XMEGA – ко всему увеличенному у megaAVR тут еще увеличили памяти и добавили встроенный контроллер для быстрой работы с памятью. Обозначается как ATxmegaxxx.
Именно микросхемы AVR обеспечивают поддержку таких функций как АЦП, ШИМ, SPI, I2C и прочих. Arduino же просто их использует (хотя с появлением более мощных микроконтроллеров, тех же esp8266, появилась возможность эмуляции вышеназванных функций программным способом).
И вопреки возможности прошивки AVR методом ISP посредством стандарта SPI через установленный загрузчик, для программирования микроконтроллеров применяется великое множество программаторов. Как простых и собираемых на коленке, так и сложных с внешним питанием и обилием всевозможных функций. Но прежде, чем мы перейдем к рассмотрению программаторов следует пояснить еще один момент. При программировании микроконтроллера с использованием внешнего программатора (без использования загрузчика) необходимо четко понимать, что такое fuses или не трогать настройки, связанные с ними, если такого понимания нет. Fuse/фьюзы/предохранительные биты — это настройки внутри микроконтроллера, в частности AVR. Они прошиваются точно так же, при помощи внешнего программатора (без применения ISP). И обычно применяются для настройки самого микроконтроллера, например, можно включить или отключить возможность самопрошивки контроллера, включение или отключение внешнего сброса устройства (и соответственно возможности его прошивки через bootloader), включение и отключение очистки памяти при перепрограммировании чипа и тому подобное. Подробнее ознакомиться с фьюзами можно (и нужно) в документации к конкретному контроллеру. К чипу ATmega328, одному из самых популярных чипов на Arduino, документация доступна на официальном сайте производителя.
Programmer:
Пожалуй, что пункт меню Tools под кодовым названием Programmer является самым таинственным местом всего Arduino IDE. Информации по данному пункту очень мало, и она настолько скудна что может привести к разочарованию даже самого стойкого прагматика. Очевидно, что данный пункт содержит выбор программаторов для программирования плат. И кажется, что одну и ту же плату можно запрограммировать кучей разных способов и программаторов. Но, а что же нам на этот счет говорит документация к Arduino IDE? А она нам вещает о том, что данный пункт меню Tools имеет смысл только при прошивке загрузчика. Во всех остальных случаях используемый «прошивальщик» зашит в тот самый boards.txt, что обсуждался немного ранее. Причем для микроконтроллеров AVR это всегда avrdude, а для ESP это всегда esptool. Кроме того, для плат AVR может применяться, в штатной поставке, целых два различных протокола «закачки» прошивки в микроконтроллер: avr109 и arduino. Разные протоколы, как не сложно догадаться, применяются для разных загрузчиков, установленных в конкретные микроконтроллеры.
В штатной поставке Arduino IDE с подключенной инфраструктурой ESP для прошивки загрузчика можно применить несколько типов программаторов. Кратко пройдемся по ним всем. По факту все AVR в Arduino IDE прошиваются посредством приблуды AVRDUDE (бывший AVRProg) и различия между «программаторами» только в используемом протоколе и настройках AVRDUDE. На текущий момент AVRDUDE поддерживает 39 программаторов, но в Arduino IDE используется только 14. Ознакомиться с настройками каждого из них можно в файле programmers.txt в той самой директории hardware, где складируется файлик boards.txt. Информация по настройкам для каждого из программатора доступна в файле avrdude.conf (располагается обычно в Arduino\hardware\tools\avr\etc) и в куцей документации к самому AVRDUDE.
AVR ISP
AVRISP есть ни что иное как фирменный программатор от Atmel. Он позволят программировать AVR. И похоже, что на этом все. Мне ну далось найти хоть сколько-нибудь достоверной информации про данный программатор, кажется, что он сошел со сцены и осел где-то в дебрях плат у аксакалов еще до появления Интернет.
Отладочная плата Atmel STK500. Фото производителя.
Единственное, что хоть как-то проливает свет на данный программатор, так это то, что он по факту является частью STK500 (такая отладочная плата для AVR от Atmel) версии 1, которая подкачается к компьютеру посредством последовательно порта.
AVRISP mkII
А вот AVRISP mkii является актуальным программатором, подключаемым к USB. По крайней мере о нем есть информация на сайте производителя и большинство безродных программаторов, которые продаются бесчисленными компаниями как раз и являются клонами именно этого программатора.
Фирменный программатор AVRISP mkII. Фото производителя.
Тем не менее, он так же является частью отладочной платы STK500, но уже версии 2, которая подключается к компьютеру уже по интерфейсу USB. Хотя, по отзывам очевидцев и версия 1 может использовать с USB посредством конвертера COM-USB.
USBtinyISP
Это один из программаторов, который можно собрать своими руками, он прост и дешев, а в качестве «сердца» у него используется чип tinyAVR. Вот только возникает вопрос – как же его прошивать, если он сам и предназначен для прошивки AVR?
Тем не менее, программатор поддерживается Arduino IDE и весьма популярен. Программатор был разработан в Нидерландах, но в настоящее время поддерживается AdaFruit.
ArduinoISP и ArduinoISP.org
Что подразумевали под этими двумя программаторами разработчики Arduino IDE остается только гадать, да угадывать. Сайт ArudinoISP.org дает перенаправление на туториал на сайте Arduino.cc по использованию Arduino as ISP. Единственное отличие этих двух от Arduino as ISP только лишь используемый протокол. Вероятно, что они были когда-то собраны в качестве поддержки особого загрузчика, а с течением времени исчезли с лица истории. По крайней мере в документации AVRDUDE никакого упоминания ни про arduinoisp ни про arduinoisporg просто нет.
USBasp
Еще один простой программатор, который можно собрать дома на коленке. И он тоже построен на чипе от Atmel. Проект был развернут немецкий программистом, а затем подхвачен всем миром. Как и с USBtinyISP чип от Atmel для программатора необходимо запрограммировать.
Parallel Programmer
Данный тип программатора считается одним из самый простых, если не самым простым. По сути «параллельный программатор» — распиновка параллельного порта компьютера и соответствующее программное обеспечение.
Для сборки данного программатора требуется компьютер с параллельным интерфейсом (раньше по нему было модно подключать принтеры) и всего три резистора двух номиналов. Подробная схема (от руки) и описание по сборке (с фотографиями) доступна на официальном сайте Arduino.
Arduino as ISP
Поскольку платы Arduino сами по себе являются микроконтроллерами, да еще и с навороченными входами/выходами, то их вполне можно использовать в качестве программаторов.
Для этого в плату Arduino закачивается скетч из примеров под именем ArduinoISP, соединяются проводки (между платой, в которую залит скетч и платой, которую нужно прошить), и прошивается загрузчик (не забываем выбрать программатор Arduino as ISP). Подробная инструкция доступна опять же на странице официального сайта Arduino.
Arduino Gemma
Если подходить формально, то Gemma это не программатор. Это микроскопическая плата от AdaFruit предназначенная для вшивания/встраивания в одежду/обувь. Она лишь частично совместима с Arduino, так как содержит очень мало вводов/выводов. Чтобы подключить ее к USB разработчику пришлось выдумать отдельный загрузчик, и отдельный PID для USB устройства. По этой причине прошивается Gemma как отдельный программатор (в реальности там встроенный USBtiny), имеет отдельный конфиг для AVRDUDE с жестко прописанным bootloader. Другими словами, прошить этой платой ничего нельзя, но зато ее можно прошить саму загрузчиком.
BusPirate as ISP
BusPirate это не только программатор, скорее это не совсем и вовсе не программатор, а одноплатный компьютер специализированный для изучения и программирования новых и неизвестных микропроцессоров. Его конструкция такова, что на одной плате собраны все мыслимые и не очень интерфейсы для исследования того, что же попало к вам в руки. Штука интересная и если вы уж работаете с таким, то должны знать, что с AVRDUDE у компьютера могут быть определенные проблемы, связанные с прошивкой самого BusPirate. Обходятся они, естественно, настройками AVRDUDE и некоторым шаманством с железом.
Atmel STK500 development board
Та самая отладочная плата, что упоминается в AVR ISP и AVR ISP mkII. В Arduino IDE не делается различия между версиями и предполагается, что AVRDUDE самостоятельно определит версию протокола обмена и осуществит самонастройку. Если же этого не происходит, то в programmers.txt нужно четко прописать stk500v1 или stk500v2 в качестве применяемого протокола.
Atmel JTAGICE3 (ISP mode) и Atmel JTAGICE3 (JTAG mode)
JTAGICE3 — современное решение для отладки чипов семейств AVR, SAM и Cortex. Разумеется, только тех чипов, что поддерживают внутричиповую отладку. Попутно эта коробочка еще и программирует их всех.
Atmel JTAGICE3 отладчик/программатор.
Продукт актуальный, у производителя имеется документация. Разница между программаторами только в применяемых протоколах. В ISP режиме применяется протокол JTAG3ISP, а в JTAG просто JTAG3.
Atmel-ICE (AVR)
Так же активный продукт Atmel и его можно купить. Как и JTAGICE3 он позволяет не только производить отладку у чипов, поддерживающих такую функцию, так и программировать их. Для прошивки используется протокол atmelice_isp. В общем использовать программаторы на подобие JTAGICE3 или Atmel-ICE только для прошивки 8-битных микроконтроллеров это как стрелять из пушки по клопам. Слишком уж мощные и дорогие решения для задачи, которая решается куда проще.
Обширное семейство ESP
Выше мы рассмотрели специфику прошивки контроллеров семейства AVR. Но при этом обойти не менее многочисленное и популярное семейство контроллеров ESP было бы просто некрасиво. Тем более, что при выборе одного из семейства, меню Tools значительно увеличивается в объеме. Пройдемся по меню и попробуем понять, что каждый из пунктов увеличенного меню означает. Здесь и далее я рассматриваю SDK ESP версии 2.5.0.
Меню Tools в Arduino IDE при выборе одной из плат семейства ESP.
Часть меню все же остается тем же самым, что и прежде, поэтому излишне повторяться не будем.
Upload speed
Позволяет выбрать скорость, с которой будет происходить загрузка прошивки и других частей в микроконтроллер. Платы семейства ESP могут комплектоваться мегабайтами памяти (в то время как AVR речь идет в лучшем случае о сотнях килобайт) и загрузка на скорости 9600 может занять непростительно долгий отрезок времени.
Поддерживаемые скорости прописываются для каждой из плат индивидуально.
CPU Frequency
Многие платы семейства ESP могут из коробки работать сразу на нескольких частотах. В Arduino регулировать частоту процессора можно при прошивке микроконтроллера. Опять же, доступные варианты указываются для каждой из плат отдельно.
Flash Size
Здесь указывается каким образом разделять всю доступную ПЗУ для хранения кода программы и для файлов. Микроконтроллеры ESP поддерживают память SPIFFS (Serial Peripheral Interface Flash File System). По сути, SPIFFS это файловая система для последовательного доступа. И у программиста есть возможность определить, сколько памяти отводить для файловой системы (туда можно записать, например, HTML-файлы или картинки) и сколько для своего скетча. Но тут есть некоторая тонкость. На текущий момент, насколько мне известно, как минимум платы EPS8266 не могут закачать скетч размерностью более 1 мегабайта. Есть обходные пути, но ведь еще нужно умудриться написать скетч, который в компилированном виде будет занимать более 1 мегабайта. Поэтому рекомендуется использовать как можно больший SPIFF, дабы уменьшить время загрузки скетча.
В тоже самое время есть сведения от потерпевших, что платы ESP32 могут испытывать проблемы связанные с «не хватает места для скетча». Но проблемы эти всего лишь из-за еще сырой поддержки плат EPS32 в инфраструктуре ESP для Arduino.
Debug port
Пункт может принимать значения Disabled, Serial и Serial1. Данная настройка предназначена для вывода отладочной информации в последовательный порт (хотя включить ее можно и программно, например, через Serial.setDebugOutput(true)). Если в скетче инициализируется соответствующий порт (Serial или Serial1), то в него могут выводиться отладочные сообщения, которые не мешают выводу в этот же порт другой информации.
При инициализации и включении вывода отладочной информации рекомендуется устанавливать максимальную скорость работы порта. В противном случае, при большом объеме вывода могут наблюдаться фризы исполнения программы или же сброс буфера вывода. Подробнее о выдаче отладочных сообщений можно прочитать в статье на сайте проекта ESP8266 Core.
У ESP обычно присутствует два последовательных порта. Первый, Serial использует UART0 и GPIO1 (TX) с GPIO3(RX). Второй, Serial1 использует UART1 и только GPIO2 (TX). Соответственно Serial1 не может быть использован на прием. Тем не менее, в обоих случаях есть возможность программного переназначения пинов, как и скорости, четности и прочих характеристик.
Debug Level
Настройка позволяет выделить те отладочные сообщения, которые важны пользователю. Выбор комбинаций оставляют огромное пространство для маневра, но при этом обязательно должна быть включена предыдущая функция.
lwIP Variant
Настройка предназначена для выбора варианта реализации сетевого стека IP. Прошу обратить внимание, тут именно lwIP (lightweight IP), первая буква L, а не i.
Вариант выбора IwIP
По умолчанию используется самый верхний вариант, уменьшающий использование памяти. Но если ваш микроконтроллер активно работает с сетью, то можно пожертвовать памятью и получить наивысшую скорость. Аналогично можно включить поддержку IPv6 или вернуться на стек версии 1.4, где такой поддержки не было вообще. Под features тут понимается всякого рода ICMP, IGMP и прочие навороты, без которых можно обойтись. Основная документация по версии ветки 2.х доступна на сайте разработчика. Постоянно растущий Wiki по теме lwIP доступен на страницах проекта FanDom.
VTables
Под VTables в IDE подразумевается механизм виртуальной таблицы для позднего связывания объектов. Не буду вдаваться в подробности, о них можно задумчиво почитать в Википедии. Остановлюсь лишь на самих доступных опциях, а их всего три:
- Flash – постоянное запоминающее устройство, таблицы хранятся в файловой системе. Метод по умолчанию в репозитарии GitHub.
- Heap – куча, оперативная память предназначенная для динамического выделения памяти.
- IRAM – внутренняя память микроконтроллера.
Понятно, что самый быстрый способ хранения и обработки виртуальных таблиц это IRAM, а самый медленный Flash. Но тут следует так же смотреть на то, как активно работает программа с объектами, есть ли множественное наследование и как часто объекты создаются и уничтожаются. В обычных применениях такая таблица и вовсе может быть статической и отлично проживать на Flash, с другой стороны, она может быть весьма крупной и просто не влезать в IRAM (область ОЗУ для хранения кода программы).
Exceptions
Пункт меню позволяет включить или отключить использование исключительных ситуаций. О них мы уже рассуждали выше, в том числе и в разделе об ESP Exception Decoder. При включении исключений программист в коде уже может использовать структуры try {} catch {}, что увеличивает потребление памяти, но позволяет легче программировать сложные алгоритмы. Напомню, что try-catch будет работать только для плат семейства ESP. Отрадно замечать, что программисты работающие над библиотеками для семейства ESP для Arduino уже начали использовать механизм try {} catch {} в своем коде на GitHub.
Erase Flash
Пункт предлагает стирать внутреннюю память микроконтроллера в следующих пропорциях:
- Only Sketch – стирает только пространство отведенное под скетч. Остальное остается нетронутым.
- Sketch + WiFi Settings – пространство для скетча и настройки WiFi. Дело в том, что при подключении к точке доступа микроконтроллер запоминает параметры подключения и в следующий раз подключается к той же точке быстрее.
- All Flash Contents – стирается все, включая файловую систему.
Прочее по ESP
В заключение статьи хотелось бы добавить, что при прошивке загрузчика в ESP всегда используется прошивальщик esptool. И всегда применяется один и тот же загрузчик eboot. Чтобы пользователь там не выставил в Programmer.
Итог
Статья получилась весьма увесистая, но я полагаю, что она даст ответы на некоторые животрепещущие вопросы. Подавляющее большинство информации приходилось искать по крупицам в Сети. Похоже, что с течением времени возрастает сложность даже такого простого инструмента как Arduino IDE и в скором времени может случиться так, что начинающий быстрее плюнет, чем начнет разбираться с мегатоннами документации, техник, приемов и способов, тем более что большинство из них поддерживается энтузиастами, что ведет к повышенному почкованию указанных техник, методик, документаций.
Однако, хочу надеться, что авторы Arduino IDE все же смогут удержать свое творение в приличных рамках и оставить его все таким же легким в освоении инструментом. Заодно хочу им напомнить, что документировать свое творение стоит постоянно иначе получается «не только лишь все, сегодня могут в завтра».
Update: разбираясь глубже с тем как настраивать свои собственные платы или же добавлять новые платы к Arduino IDE, наткнулся на исчерпывающую документацию по этому поводу.
Опубликовано автором kvv в следующих категориях:
DIY Soft не только лишь все статья
C ++ vtables — Часть 1 — Основы
(1204 слова)
Вт, 1 марта 2016 г.
В этой мини-серии статей мы рассмотрим, как clang реализует vtables и RTTI. В
в этой части мы начнем с некоторых основных классов, а позже рассмотрим несколько
наследование и виртуальное наследование.
Обратите внимание, что эта мини-серия будет включать в себя некоторые исследования двоичного кода.
сгенерирован для разных фрагментов кода через gdb. Это несколько
низкоуровневый (иш), но всю тяжелую работу я сделаю за вас.Я не
полагаю, что многие будущие посты будут такого низкого уровня.
Заявление об ограничении ответственности : все написанное здесь зависит от реализации, может измениться
в любой будущей версии, и на нее не следует полагаться. Мы изучаем это для
только по образовательным причинам.☑ Согласен
круто, приступим.
Часть 1 — vtables — Основы
Расчетное время чтения: ~ 15 минут.
Рассмотрим следующий код:
#include
используя пространство имен std;
class NonVirtualClass {
общественность:
void foo () {}
};
class VirtualClass {
общественность:
virtual void foo () {}
};
int main () {
cout << "Размер NonVirtualClass:" << sizeof (NonVirtualClass) << endl;
cout << "Размер виртуального класса:" << sizeof (VirtualClass) << endl;
}
$ # скомпилировать и запустить main.cpp
$ clang ++ main.cpp && ./a.out
Размер невиртуального класса: 1
Размер VirtualClass: 8
NonVirtualClass
имеет размер 1, потому что в C ++ класс
es не может иметь ноль
размер. Однако сейчас это не важно.
Размер VirtualClass
на 64-битной машине равен 8. Зачем? Потому что есть скрытый
указатель внутри него указывает на vtable
. vtable
s - статический перевод
таблицы, созданные для каждого виртуального класса.Эта серия постов посвящена их содержанию
и как они используются.
Чтобы лучше понять, как выглядит vtables
, давайте рассмотрим
следующий код с gdb, чтобы узнать, как устроена память:
#include
class Parent {
общественность:
virtual void Foo () {}
virtual void FooNotOverridden () {}
};
class Derived: public Parent {
общественность:
void Foo () override {}
};
int main () {
Родитель p1, p2;
Получено d1, d2;
std :: cout << "готово" << std :: endl;
}
$ # скомпилируем наш код с символами отладки и начнем отладку с помощью gdb
$ clang ++ -std = c ++ 14 -stdlib = libc ++ -g main.cpp && gdb ./a.out
...
(gdb) # попросить gdb автоматически распознать символы C ++
(gdb) установить печать asm-demangle на
(gdb) включить распечатку
(gdb) # установить точку останова на главном
(gdb) b main
Точка останова 1 по адресу 0x4009ac: файл main.cpp, строка 15.
(gdb) запустить
Стартовая программа: /home/shmike/cpp/a.out
Точка останова 1, main () в main.cpp: 15
15 Родитель p1, p2;
(gdb) # перейти к следующей строке
(gdb) сущ.
16 Получено d1, d2;
(gdb) # перейти к следующей строке
(gdb) сущ.
18 std :: cout << "готово" << std :: endl;
(gdb) # print p1, p2, d1, d2 - мы скоро поговорим о том, что означает вывод
(gdb) p p1
$ 1 = {_vptr $ Parent = 0x400bb8 }
(gdb) p p2
$ 2 = {_vptr $ Parent = 0x400bb8 }
(gdb) p d1
$ 3 = { = {_vptr $ Parent = 0x400b50 }, <Без полей данных>}
(gdb) p d2
$ 4 = { = {_vptr $ Parent = 0x400b50 }, <Без полей данных>}
Вот что мы узнали из вышеизложенного:
- Несмотря на то, что классы не имеют элементов данных, есть скрытый указатель на
vtable; - vtable для
p1
иp2
одинакова.vtables - статические данные для каждого типа; -
d1
иd2
наследуют указатель vtable отParent
, который указывает на
Получено vtable
; - Все vtables указывают на смещение в 16 (0x10) байтов в vtable. А также
обсудим это позже.
Давайте продолжим наш сеанс gdb, чтобы увидеть содержимое vtables. я буду
используйте команду x
, которая выводит память на экран. Прошу напечатать 300
байты в шестнадцатеричном формате, начиная с 0x400b40.Почему именно этот адрес? Потому что выше мы
увидел, что указатель vtable указывает на 0x400b50, а символ для этого адреса
- это vtable для Derived + 16
(16 == 0x10).
(гдб) х / 300ксб 0x400b40
0x400b40 : 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b48 : 0x90 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400b50 : 0x80 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400b58 : 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400b60 <имя typeinfo для производного>: 0x37 0x44 0x65 0x72 0x69 0x76 0x65 0x64
0x400b68 <имя typeinfo для Derived + 8>: 0x00 0x36 0x50 0x61 0x72 0x65 0x6e 0x74
0x400b70 <имя typeinfo для Parent + 7>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b78 : 0x90 0x20 0x60 0x00 0x00 0x00 0x00 0x00
0x400b80 : 0x69 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400b88: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400b90 : 0x10 0x22 0x60 0x00 0x00 0x00 0x00 0x00
0x400b98 : 0x60 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400ba0 : 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400ba8 : 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400bb0 : 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400bb8 : 0xa0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400bc0 : 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
...
Примечание. Мы рассматриваем символы без запутывания. Если вы действительно хотите знать, _ZTV - это
префикс для vtable, _ZTS - это префикс для строки типа (имя), а _ZTI - для
тип-информация.
Вот макет vtable Parent
:
Адрес | Значение | Значение |
---|---|---|
0x400ba8 | 0x0 | top_offset (подробнее об этом позже) |
0x400bb0 | 0x400b78 | Указатель на информацию типа для родительского (также часть приведенного выше дампа памяти) |
0x400bb8 | 0x400aa0 | Указатель на Parent :: Foo () 1 . _vptr родительского элемента указывает здесь. |
0x400bc0 | 0x400a90 | Указатель на Parent :: FooNotOverridden () 2 |
Вот Полученный макет vtable
:
Адрес | Значение | Значение |
---|---|---|
0x400b40 | 0x0 | top_offset (подробнее об этом позже) |
0x400b48 | 0x400b90 | Указатель на информацию о типе для производного (также часть приведенного выше дампа памяти) |
0x400b50 | 0x400a80 | Указатель на Derived :: Foo () 3 . Получено очков _vptr. |
0x400b58 | 0x400a90 | Указатель на Parent :: FooNotOverridden () (то же, что и Parent ) |
1:
(gdb) # узнаем какой отладочный символ у нас для адреса 0x400aa0
(gdb) информационный символ 0x400aa0
Parent :: Foo () в разделе .text файла a.out
2:
(gdb) информационный символ 0x400a90
Parent :: FooNotOverridden () в разделе.текст a.out
3:
(gdb) информационный символ 0x400a80
Derived :: Foo () в разделе .text файла a.out
Помните, что указатель vtable в Derived
указывает на смещение +16 байтов
в таблицу? Третий указатель - это адрес первого указателя метода.
Хотите 3-й способ? Нет проблем - добавьте 2 * sizeof (void *)
к vtable-pointer.
Хотите запись typeinfo? перейти к указателю раньше.
Продолжаем - как насчет макета записей typeinfo?
Родительские
:
Адрес | Значение | Значение |
---|---|---|
0x400b78 | 0x602090 | Вспомогательный класс для методов type_info 1 |
0x400b80 | 0x400b69 | Строка, представляющая имя типа 2 |
0x400b88 | 0x0 | 0 означает отсутствие родительской записи typeinfo |
А вот Полученная запись типа информации
:
Адрес | Значение | Значение |
---|---|---|
0x400b90 | 0x602210 | Вспомогательный класс для методов type_info 3 |
0x400b98 | 0x400b60 | Строка, представляющая имя типа 4 |
0x400ba0 | 0x400b78 | Указатель на родительскую запись типа |
1:
(gdb) информационный символ 0x602090
vtable для __cxxabiv1 :: __ class_type_info @@ CXXABI_1.3 + 16 в разделе .bss из a.out
2:
(гдб) х / с 0x400b69
0x400b69 <имя типа информации для родителя>: "6Parent"
3:
(gdb) информационный символ 0x602210
vtable для __cxxabiv1 :: __ si_class_type_info @@ CXXABI_1.3 + 16 в разделе .bss файла a.out
4:
(гдб) х / с 0x400b60
0x400b60 <имя typeinfo для производного>: "7Derived"
Если вы хотите узнать больше о __si_class_type_info
, вы можете найти некоторую информацию
Вот,
а также
Вот.
Это исчерпывает мои навыки работы с GDB, а также завершает этот пост. Я предполагаю, что некоторые люди
сочтет это слишком низким уровнем или, может быть, просто бездействующим. Если да, то рекомендую
пропуская части 2 и 3, сразу переходим к части 4.
Виртуальный указатель в C ++ | VPTR и VTABLE | VFPTR и VFTABLE
vfptr, vftable, виртуальные функции
Компилятор C ++ создает скрытый член класса, называемый виртуальным указателем, или короче vfptr, когда есть одна или несколько виртуальных функций.Этот vfptr является указателем, который указывает на таблицу указателей функций. Эта таблица также создается компилятором и называется таблицей виртуальных функций или vftable. Каждая строка vftable - это указатель функции, указывающий на соответствующую виртуальную функцию.
VFPTR | VFTABLE | ФУНКЦИЯ |
b.vfptr -> | Vftable [0] -> | base :: funct1 () |
Vftable [1] -> | base :: funct2 () |
Чтобы выполнить позднее связывание, компилятор создает эту vftable таблицу для каждого класса, содержащего виртуальные функции, и для класса, производного от него.Компилятор помещает адреса виртуальных функций для этого конкретного класса в "vftable".
Когда вызов виртуальной функции выполняется через указатель базового класса, компилятор незаметно вставляет код для получения VFPTR и поиска адреса функции в VFTABLE, вызывая, таким образом, правильную функцию и вызывая позднее / динамическое связывание.
Компилятор присваивает при строительстве
класс базовый | |
{ | |
виртуальный void funct1 ( void ); | |
виртуальный void funct2 ( void ); | |
}; | |
/ * Объект * / | |
основание b; | |
/ * Компилятор C ++ внутри делает это * / | |
б.vptr = адрес b.vtable; | |
b.vtable [0] = & base :: funct1; | |
b.vtable [1] = & base :: funct2; | |
Изображение
Печать vfptr, исходный код vtables
#include | |
с использованием пространства имен std; | |
класс базовый | |
{ | |
общественные: | |
виртуальный void funct1 ( void ) = 0; | |
виртуальный void funct2 ( void ) = 0; | |
}; | |
класс производный: общедоступный базовый | |
{ | |
общественные: | |
производное () | |
{ | |
cout << "Объект построен" << endl; | |
cout << "Адрес объекта" << this << endl; | |
cout << "vfptr address is" << * ( void **) this << endl; | |
cout << "адрес функции1 равен" << * ( void **) (* ( void **) this) << endl; | |
cout << "адрес функции 2 равен" << * (( void **) (* ( void **) this) +1) << endl; | |
} | |
пусто функция1 ( пусто ); | |
пусто funct2 ( пусто ); | |
}; | |
пустота производная :: funct1 ( пустота ) | |
{ | |
} | |
пустота производная :: funct2 ( пустота ) | |
{ | |
} | |
int main ( int argh, char * argv []) | |
{ | |
производное d; | |
пусто *** vfptr = ( пусто ***) & d; | |
пусто ** vtable = ( пусто **) * vfptr; | |
cout << "Из основного" << endl; | |
cout << "Адрес d:" << & d << endl; | |
cout << "vfptr is" << vtable << endl; | |
cout << "Адрес функции1:" << vtable [0] << endl; | |
cout << "Адрес функции 2:" << vtable [1] << endl; | |
возврат 0; |
Справочник команд по основам конфигурации Cisco IOS - показать через show fm summary [Поддержка]
показать
Чтобы проверить конфигурацию множественного связующего дерева (MST), используйте команду show .в подрежиме конфигурации MST.
показать [ текущий | на рассмотрении ]
Описание синтаксиса
текущий | (Необязательно) Отображает текущую конфигурацию, которая используется для запуска MST. |
на рассмотрении | (Необязательно) Отображает отредактированную конфигурацию, которая заменит текущую конфигурацию. |
По умолчанию
У этой команды нет настроек по умолчанию.
Командные режимы
Подрежим конфигурации MST
История команд
Выпуск | Модификация |
---|---|
12,2 (14) SX | Поддержка этой команды была введена в Supervisor Engine 720. |
12,2 (17d) SXB | Поддержка этой команды в Supervisor Engine 2 была расширена до версии 12.2 (17d) SXB. |
12,2 (33) SRA | Эта команда была интегрирована в Cisco IOS версии 12.2 (33) SRA. |
Руководство по использованию
Вывод на дисплей команды show pending - это отредактированная конфигурация, которая заменит текущую конфигурацию, если вы введете команду exit для выхода из режима конфигурации MST.
При вводе команды show без аргументов отображаются ожидающие конфигурации.
Примеры
В этом примере показано, как отобразить отредактированную конфигурацию:
Маршрутизатор
(config-mst) # показать в ожидании
Ожидающая конфигурация MST
Имя [zorglub]
Версия 31415
Экземпляр Vlan сопоставлен
-------- ----------------------------------------- ----------------------------
0 4001-4096
2 1010, 1020, 1030, 1040, 1050, 1060, 1070, 1080, 1090, 1100, 1110
1120
3 1-1009, 1011-1019, 1021-1029, 1031-1039, 1041-1049, 1051-1059
1061-1069, 1071-1079, 1081-1089, 1091-1099, 1101-1109, 1111-1119
1121-4000
------------------------------------------------- -----------------------------
Маршрутизатор
(config-mst) #
В этом примере показано, как отобразить текущую конфигурацию:
Router (config-mst) # показать текущий
Текущая конфигурация MST
Имя []
Редакция 0
Сопоставлено
экземпляров Vlan
-------- ----------------------------------------- ----------------------------
0 1-4094
------------------------------------------------- ------------------------------
Связанные команды
Команда | Описание |
---|---|
Экземпляр | Сопоставляет VLAN или набор VLAN с экземпляром MST. |
имя (подрежим конфигурации MST) | Задает имя региона MST. |
ревизия | Устанавливает номер версии для конфигурации MST. |
Показать остов мст | Отображает информацию о протоколе MST. |
Конфигурация связующего дерева mst | Вход в подрежим MST-конфигурации. |
показать <команда> добавить
Чтобы перенаправить и добавить вывод любой команды show в существующий файл, используйте команду show | добавить команду в привилегированном режиме EXEC.
показать команду | добавить url
Описание синтаксиса
команда | Любая команда Cisco IOS show . |
| добавить url | Добавление этого синтаксиса перенаправляет вывод команды в расположение файла, указанное в универсальном указателе ресурсов (URL).Труба (|) обязательна. Файловая система Cisco IOS (IFS) использует URL-адреса для указания расположения файловой системы, каталога и файла. Типичные элементы URL включают: префикс: [каталог /] имя файла Префиксы могут быть локальными местоположениями файлов, например flash: или disk0: . Кроме того, вы можете указать сетевые местоположения, используя следующий синтаксис: ftp: [[ // [ имя пользователя [: пароль] @ ] местоположение ] / каталог] / имя файла tftp: [[ // расположение ] каталог /] / имя файла Префикс rcp: не поддерживается. |
Командные режимы
Привилегированный EXEC
История команд
Выпуск | Модификация |
---|---|
12,0 (21) ю | Эта команда была введена. |
12.2 (13) Т | Эта команда была интегрирована в Cisco IOS версии 12.2 (13) T. |
Руководство по использованию
Чтобы отобразить все префиксы URL-адресов, которые поддерживаются этой командой, используйте команду show | добавить? команда.
Эта команда добавляет вывод команды show в конец указанного файла.
Примеры
В следующем примере выходные данные команды show tech-support перенаправляются в существующий файл на Диске 1 с именем файла «showoutput».txt. "Этот вывод добавляется в конец любых существующих данных в файле.
Маршрутизатор
№ показать техническую поддержку | добавить disk1: showoutput.txt
Связанные команды
Команда | Описание |
---|---|
показать <команда> перенаправление | Перенаправляет вывод любой команды show в указанный файл. |
показать тройник <команда> | Копирует вывод команды show в файл при отображении на терминале. |
показать <команда> начало
Чтобы начать вывод любой команды show из указанной строки, используйте команду show | команда begin в режиме EXEC.
показать команду | начало регулярное выражение
Описание синтаксиса
команда | Любая поддерживаемая команда show . |
| | Вертикальная черта (символ «вертикальная черта») указывает, что следует спецификация обработки вывода. |
регулярное выражение | Любое регулярное выражение, найденное в , показывает вывод команды . Вывод шоу начнется с первого экземпляра этой строки (вывод до этой строки не будет выводиться на экран). Строка чувствительна к регистру. Используйте круглые скобки, чтобы указать буквальное использование пробелов. |
/ | Задает поиск по запросу --More--, который начинает нефильтрованный вывод с первой строки, содержащей регулярное выражение. |
- | Задает фильтр в приглашении --More--, который отображает только выходные строки, не содержащие регулярного выражения. |
+ | Задает фильтр в приглашении --More--, который отображает только выходные строки, содержащие регулярное выражение. |
Командные режимы
EXEC
История команд
Выпуск | Модификация |
---|---|
8.3 | Появилась команда show . |
12,0 (1) т | Это расширение команды show было введено .. |
12,2 (33) SRA | Эта команда была интегрирована в Cisco IOS версии 12.2 (33) SRA. |
Руководство по использованию
Аргумент регулярного выражения чувствителен к регистру и учитывает сложные требования сопоставления.Используйте круглые скобки, чтобы указать буквальное использование пробелов. Например, | begin u указывает, что вывод шоу должен начинаться с любой строки, содержащей u; | begin (u) указывает, что вывод шоу должен начинаться с любой строки, содержащей пробел и u вместе (в строке есть слово, которое начинается со строчной u).
Для поиска оставшихся выходных данных команды show используйте следующую команду в приглашении --More -:
/ регулярное выражение
Вы можете указать поиск с фильтром в любом запросе --More-. (Ctrl-Shift-6) или Ctrl - z .
Примечание После того, как вы укажете фильтр для команды show , вы не сможете указать другой фильтр в следующем запросе --More -. Первый указанный фильтр остается до завершения вывода команды more или до тех пор, пока вы не прервете вывод. Использование ключевого слова , начало не является фильтром.
Поскольку предыдущий вывод не сохраняется, вы не можете искать или фильтровать предыдущий вывод.или комбинации Ctrl-Z, так как есть --More - подсказки для полного прекращения вывода.
Примеры
Ниже приводится частичный пример вывода интерфейса шоу | Команда begin , которая начинает нефильтрованный вывод с первой строки, содержащей регулярное выражение «Ethernet». В приглашении --More-- пользователь указывает фильтр для отображения только тех строк в оставшемся выводе, которые содержат регулярное выражение «Serial».
Маршрутизатор # показать интерфейс | начать Ethernet
Ethernet0 включен, протокол линии работает
Оборудование - Lance, адрес 0060.837c.6399 (bia 0060.837c.6399)
Описание: IP-адрес 172.1.2.14 255.255.255.0
Интернет-адрес 172.1.2.14/24
.
.
.
0 потерянный перевозчик, 0 нет перевозчика
0 отказов выходного буфера, 0 выходных буферов выгружено
--Подробнее--
+ серийный
фильтрация...
Serial1 включен, протокол линии работает
Serial2 включен, протокол линии работает
Serial3 включен, линейный протокол не работает
Serial4 не работает, линейный протокол не работает
Serial5 включен, протокол линии работает
Serial6 включен, протокол линии работает
Serial7 включен, протокол линии работает
Связанные команды
Команда | Описание |
---|---|
подробнее | Начинает нефильтрованный вывод команды more с первой строки, содержащей указанное вами регулярное выражение. |
подробнее | Фильтрует больше выходных данных команды , чтобы исключить строки, содержащие определенное регулярное выражение. |
другие | Фильтрует еще вывод команды , чтобы отображались только строки, содержащие определенное регулярное выражение. |
показать <команда> исключить | Фильтры показывают выходные данные команды , поэтому исключаются строки, содержащие определенное регулярное выражение. |
показать <команда> включить | Фильтры показывают выходные данные команды , поэтому отображаются только строки, содержащие определенное регулярное выражение. |
показать <команда> исключить
Чтобы отфильтровать выходные данные команды show , чтобы исключить строки, содержащие конкретное регулярное выражение, используйте команду show | исключить команду в режиме EXEC.
показать команду | исключить регулярное выражение
Описание синтаксиса
команда | Любая поддерживаемая команда show . |
| | Вертикальная черта (символ «вертикальная черта») указывает, что следует спецификация обработки вывода. |
регулярное выражение | Любое регулярное выражение, найденное в , показывает вывод команды . |
/ | Задает поиск по запросу --More--, который начинает нефильтрованный вывод с первой строки, содержащей регулярное выражение. |
Командные режимы
EXEC
История команд
Выпуск | Модификация |
---|---|
12.0 (1) Т | Эта команда была введена. |
12,2 (33) SRA | Эта команда была интегрирована в Cisco IOS версии 12.2 (33) SRA. |
Руководство по использованию
Аргумент регулярного выражения чувствителен к регистру и учитывает сложные требования сопоставления.
Вы можете указать новый поиск при каждом запросе --More--.или комбинации Ctrl-Z, так как есть --More - подсказки для полного прекращения вывода.
Примеры
Ниже приводится частичный образец вывода шоу | Команда exclude , используемая с командой show buffers . Исключены строки, содержащие регулярное выражение «0 промахов». В приглашении --More-- пользователь ищет регулярное выражение «Serial0», которое продолжает отфильтрованный вывод с первой строки, содержащей «Serial0.«
Маршрутизатор # показывает буферы | исключить 0 промахов
Буферные элементы:
398 в свободном списке (максимально разрешено 500)
Общественные буферные пулы:
Маленькие буферы, 104 байта (всего 50, постоянных 50):
50 в свободном списке (20 мин., Макс. 150)
551 попадание, 3 промаха, 0 обрезок, 0 создано
больших буфера, 1524 байта (всего 50, постоянных 50):
49 в свободном списке (5 мин., Макс. 150)
очень больших буфера, 4520 байт (всего 10, постоянных 10):
.
.
.
Огромные буферы, 18024 байта (всего 0 постоянных 0):
0 в свободном списке (0 мин., 4 макс. Разрешено)
--Подробнее--
/ Серийный0
фильтрация ...
буфера Serial0, 1543 байта (всего 64, постоянных 64):
16 в свободном списке (0 мин., Макс. 64)
48 просмотров, 0 откатов
Связанные команды
Команда | Описание |
---|---|
подробнее | Начинает нефильтрованный вывод команды more с первой строки, содержащей указанное вами регулярное выражение. |
подробнее | Фильтрует больше выходных данных команды , чтобы исключить строки, содержащие определенное регулярное выражение. |
другие | Фильтрует еще вывод команды , чтобы отображались только строки, содержащие определенное регулярное выражение. |
показать <команда> начало | Ищет вывод любой команды show и отображает вывод первого экземпляра указанной строки. |
показать <команда> включить | Фильтры показывают выходные данные команды , поэтому отображаются только строки, содержащие определенное регулярное выражение. |
показать <команда> включить
Чтобы отфильтровать выходные данные команды show , чтобы отображались только строки, содержащие определенное регулярное выражение, используйте команду show | включить команду в режиме EXEC.
показать команду | включить регулярное выражение
Описание синтаксиса
команда | Любая поддерживаемая команда show . |
| | Вертикальная черта (символ «вертикальная черта») указывает, что следует спецификация обработки вывода. |
регулярное выражение | Любое регулярное выражение, найденное в , показывает вывод команды . Используйте круглые скобки для включения пробелов в выражение. |
/ | Задает поиск по запросу --More--, который начинает нефильтрованный вывод с первой строки, содержащей регулярное выражение. |
Командные режимы
EXEC
История команд
Выпуск | Модификация |
---|---|
12.0 (1) Т | Эта команда была введена. |
12,2 (33) SRA | Эта команда была интегрирована в Cisco IOS версии 12.2 (33) SRA. |
Руководство по использованию
Аргумент регулярного выражения чувствителен к регистру и учитывает сложные требования сопоставления.
Вы можете указать новый поиск при каждом запросе --More--.или комбинации Ctrl-Z, так как есть --More - подсказки для полного прекращения вывода.
Примеры
Ниже приводится частичный пример вывода интерфейса шоу | включить команду . Он отображает только строки, содержащие регулярное выражение «(есть)». Скобки заставляют включать пробелы до и после «есть». Использование круглых скобок гарантирует, что в вывод будут включены только строки, содержащие "is" с пробелом до и после него.Строки со словами типа «отключить» будут исключены, так как в строке «is» нет пробелов.
Маршрутизатор # показать интерфейс | включить (есть)
ATM0 отключен административно, протокол линии не работает
Аппаратное обеспечение
- ATMizer BX-50
Номеронабиратель 1 включен (спуфинг), протокол линии работает (спуфинг)
Неизвестное оборудование
DTR пульсирует в течение 1 секунды при сбросе
Ethernet0 включен, протокол линии работает
Оборудование - Lance, адрес 0060.837c.6399 (bia 0060.837c.6399)
Интернет-адрес: 172.21.53.199/24
Ethernet1 включен, протокол линии работает
Оборудование - Lance, адрес - 0060.837c.639c (bia 0060.837c.639c)
Интернет-адрес 5.5.5.99/24
Serial0: 0 не работает, линейный протокол не работает
Оборудование DSX1
.
.
.
--Подробнее--
В приглашении --More-- пользователь ищет регулярное выражение «Serial0: 13», которое продолжает отфильтрованный вывод с первой строки, содержащей «Serial0: 13».«
/ Серийный номер0: 13
фильтрация ...
Serial0: 13 не работает, линейный протокол не работает
Оборудование DSX1
Интернет-адрес 11.0.0.2/8
0 ошибок вывода, 0 коллизий, 2 сброса интерфейса
Используемых временных интервалов: 14, задержка передатчика равна 0 флагов
Связанные команды
Команда | Описание |
---|---|
подробнее | Начинает нефильтрованный вывод команды more с первой строки, содержащей указанное вами регулярное выражение. |
подробнее | Фильтрует больше выходных данных команды , чтобы исключить строки, содержащие определенное регулярное выражение. |
другие | Фильтрует еще вывод команды , чтобы отображались только строки, содержащие определенное регулярное выражение. |
показать <команда> начало | Выполняет поиск в выводе любой команды show и отображает вывод из первого экземпляра указанной строки. |
показать <команда> исключить | Фильтры показывают выходные данные команды , поэтому исключаются строки, содержащие определенное регулярное выражение. |