awesome-everything EN
↑ Обратно к восхождению

Базовый CS с нуля

Трассировка стека

Суть Трассировка стека — это снимок стека вызовов в момент возбуждения исключения: список кадров от места выброса вниз до точки входа программы. Читаемая сверху вниз, она показывает, что выбросило и через какую цепочку вызовов.
◷ 22 min

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

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

Цель

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

Идея

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

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

Трассировка печатается верхним кадром вперёд:

  • Верхняя строка — это кадр, возбудивший исключение — место выброса. Здесь ошибка фактически проявилась.
  • Каждая строка ниже — это вызывающая строки выше неё, идя вниз к точке входа программы. Нижняя строка — это самый внешний вызов, обычно main или точка входа.

Итак, читая сверху вниз, трассировка отвечает сразу на два вопроса: что выбросило (верхняя строка) и через какую цепочку вызовов программа дошла до этой точки (каждая строка ниже). Это стек вызовов из Блока 08, замороженный и напечатанный.

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

Код
1 function divide(a: number, b: number): number {
2 if (b === 0) throw new Error("divide by zero");
3 return a / b;
4 }
5
6 function getAge(birthYears: number, people: number): number {
7 return divide(birthYears, people);
8 }
9
10 function loadUser(): number {
11 return getAge(60, 0); // people = 0 — вызовет выброс
12 }
13
14 function main(): void {
15 loadUser();
16 }
17
18 main();
  • L2 Место выброса: divide обнаруживает b === 0 и возбуждает исключение. Это ВЕРХНЯЯ строка трассировки.
  • L7 getAge вызвала divide здесь — эта строка появляется в трассировке ниже divide.
  • L11 loadUser вызвала getAge с people = 0 — эта строка появляется ниже getAge.
  • L15 main вызвала loadUser здесь — эта строка появляется ниже loadUser.
  • L18 Точка входа программы: main() вызвана. НИЖНЯЯ строка трассировки.
Четыре вложенные функции. divide возбуждает исключение, когда b равно 0. Трассировка стека перечислит все четыре кадра, divide сверху.

Трассировка стека, печатаемая при аварийном завершении этой программы, выглядит так — верхним кадром вперёд:

1 Error: divide by zero
2 at divide (app.ts:2)
3 at getAge (app.ts:7)
4 at loadUser (app.ts:11)
5 at main (app.ts:15)
  • L1 Сообщение исключения — описание ошибки, переданное в new Error(...).
  • L2 ВЕРХНИЙ кадр: divide, строка 2 — место выброса. Здесь исключение было возбуждено.
  • L3 getAge, строка 7 — getAge ждала на строке 7, вызов divide.
  • L4 loadUser, строка 11 — loadUser ждала на строке 11, вызов getAge.
  • L5 НИЖНИЙ кадр: main, строка 15 — вызов loadUser из точки входа программы.
Вывод аварийного завершения. Читай сверху вниз: divide выбросило, вызвана из getAge, вызвана из loadUser, вызвана из main.
Пошаговый разбор

Пройди аварийное завершение шаг за шагом. Ячейки — это стек вызовов, снизу вверх (старейший слева). Смотри, как кадры кладутся по мере вложения вызовов, затем как исключение распространяется по мере разматывания рантаймом — каждый снятый кадр это одна строка, которую записывает трассировка.

1 function divide(a, b) {
2 if (b === 0) throw new Error('divide by zero');
3 return a / b;
4 }
5 function getAge(birthYears, people) {
6 return divide(birthYears, people);
7 }
8 function loadUser() {
9 return getAge(60, 0);
10 }
11 function main() {
12 loadUser();
13 }
14 main();

Почему это работает

Почему трассировка фиксируется при выбросе, а не при аварийном завершении? Потому что стек вызовов разрушается разматыванием. К моменту, когда программа фактически аварийно завершается, каждый кадр снят — стек пуст. Если бы рантайм ждал до этого момента, чтобы посмотреть, описывать было бы нечего. Поэтому он фиксирует снимок в миг возбуждения исключения, пока все кадры ещё на месте, и затем печатает этот зафиксированный снимок, когда разматывание заканчивается без обработчика.

Частая ошибка

Распространённая ошибка — читать трассировку снизу вверх и винить main, потому что main — знакомое имя внизу. Нижний кадр — это просто точка входа программы; он есть в каждой трассировке и почти никогда не виновник. Место выброса — это верхняя строка. Начинай чтение оттуда: эта функция, на этом номере строки — там, где ошибка проявилась.

Практика 0 / 5

У трассировки стека 4 строки: divide, getAge, loadUser, main (сверху вниз). Какой номер строки (считая верхнюю строку за 1) называет место выброса — функцию, возбудившую исключение?

Та же трассировка из 4 строк: divide, getAge, loadUser, main (сверху вниз). Считая верхнюю строку за 1, какая строка называет точку входа программы?

main вызывает loadUser, вызывает getAge, вызывает divide. divide выбрасывает. Трассировка стека, зафиксированная при выбросе, имеет сколько кадров?

Ни у одной функции в цепочке нет обработчика. Рантайм разматывает. Сколько кадров он снимает, прежде чем стек опустеет и программа аварийно завершится?

В трассировке строка 'at getAge (app.ts:7)' стоит прямо под 'at divide'. Строка 7 функции getAge — это какое событие? Введи 1, если это место, где getAge вызвала divide, 2 — если это место, где getAge возбудила своё исключение.

Проверь себя
Викторина

Что такое трассировка стека и как её читать?

Итог

Трассировка стека — это стек вызовов из Блока 08, замороженный и записанный в миг возбуждения исключения. Это список кадров, по одной строке на каждый, называющих функцию и строку, которую она исполняла. Он печатается верхним кадром вперёд: верхняя строка — это место выброса — функция и строка, где исключение было возбуждено — а каждая строка ниже — это вызывающая строки выше неё, вниз до точки входа программы внизу. Рантайм фиксирует этот снимок при выбросе, пока кадры ещё существуют, потому что разматывание затем снимает каждый кадр и опустошает стек. Чтобы прочитать трассировку, начни сверху: эта строка точно говорит, что выбросило и где; строки ниже говорят цепочку вызовов, что привела программу туда.

Продолжить восхождение ↑Неопределённое поведение
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.