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

В течение восьми лет такой была реальность моего взаимодействия с Dell Inspiron 5567. Этот необъяснимый баг возникал в каждой установленной ОС. В статье я расскажу историю о том, как погрузился в исходный код прошивки и обнаружил единственную команду-виновницу.

Введение

Этот ноутбук был моим напарником с седьмого класса школы. На этой машине я учился всему, от C++ до Python. Когда я не смог проапгрейдиться до Windows 11, вторую жизнь ему дал Linux Mint. Хотя в нём появился свой комплекс технических проблем, один баг оставался неизменным раздражителем во всех моих операционных системах: S3 Sleep.

Баг

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

Так как баг возникал и в Windows, и в Linux, я понимал, что причиной была не операционная система, а нечто более глубокое: сама прошивка.

Искра

https://github.com/Zephkek/Asus-ROG-Aml-Deep-Dive

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

В Linux (и даже в Windows) всё сводится к этим двум командам:

# Извлекаем все таблицы ACPI в двоичные файлы .dat; sudo для привилегий администратора в Linux
sudo acpidump -b

# Декомпилируем основную таблицу в человекочитемый ACPI Source Language (.dsl)
iasl -d *.dat

Вот и всё.

Сырой код в окрестностях точки основной проблемы

Я обнаружил в  dsdt.dsl "Method(_PTS". На самом деле, весь код находится в dsdt.dsl.

Примечание:

  • Я привёл все вызовы функций, чтобы точно мог показать вам весь процесс.

  • Я не показал области видимости, только методы.

  • Отступы сохранены, чтобы отличать один метод от другого.

    Method (_PTS, 1, NotSerialized)  // _PTS: Prepare To Sleep
    {
        If (Arg0)
        {
            PTS (Arg0)
            \_SB.TPM.TPTS (Arg0)
            \_SB.PCI0.LPCB.SPTS (Arg0)
            \_SB.PCI0.NPTS (Arg0)
            RPTS (Arg0)
        }
    }
    Method (PTS, 1, NotSerialized)
    {
    }
        Method (TPTS, 1, Serialized)
        {
            Switch (ToInteger (Arg0))
            {
                Case (0x04)
                {
                    RQST = Zero
                    FLAG = 0x09
                    SRSP = Zero
                    SMI = OFST /* \OFST */
                    Return (SRSP) /* \_SB_.TPM_.SRSP */
                }
                Case (0x05)
                {
                    RQST = Zero
                    FLAG = 0x09
                    SRSP = Zero
                    SMI = OFST /* \OFST */
                    Return (SRSP) /* \_SB_.TPM_.SRSP */
                }

            }
        }
                Method (SPTS, 1, NotSerialized)
                {
                    SLPX = One
                    SLPE = One
                    If ((Arg0 == 0x03))
                    {
                        AES3 = One
                    }
                }
                Method (NPTS, 1, NotSerialized)
                {
                    PA0H = PM0H /* \_SB_.PCI0.PM0H */
                    PALK = PMLK /* \_SB_.PCI0.PMLK */
                    PA1H = PM1H /* \_SB_.PCI0.PM1H */
                    PA1L = PM1L /* \_SB_.PCI0.PM1L */
                    PA2H = PM2H /* \_SB_.PCI0.PM2H */
                    PA2L = PM2L /* \_SB_.PCI0.PM2L */
                    PA3H = PM3H /* \_SB_.PCI0.PM3H */
                    PA3L = PM3L /* \_SB_.PCI0.PM3L */
                    PA4H = PM4H /* \_SB_.PCI0.PM4H */
                    PA4L = PM4L /* \_SB_.PCI0.PM4L */
                    PA5H = PM5H /* \_SB_.PCI0.PM5H */
                    PA5L = PM5L /* \_SB_.PCI0.PM5L */
                    PA6H = PM6H /* \_SB_.PCI0.PM6H */
                    PA6L = PM6L /* \_SB_.PCI0.PM6L */
                }
    Method (RPTS, 1, NotSerialized)
    {
        P80D = Zero
        D8XH (Zero, Arg0)
        ADBG (Concatenate ("_PTS=", ToHexString (Arg0)))
        If ((Arg0 == 0x03))
        {
            If (CondRefOf (\_PR.DTSE))
            {
                If ((\_PR.DTSE && (TCNT > One)))
                {
                    TRAP (0x02, 0x1E)
                }
            }
        }

        If ((IVCM == One))
        {
            \_SB.SGOV (0x02040000, Zero)
            \_SB.SGOV (0x02010002, Zero)
        }

        If (CondRefOf (\_SB.TPM.PTS))
        {
            \_SB.TPM.PTS (Arg0)
        }

        EV1 (Arg0, Zero)
    }

Объяснение проблемы в этом сыром коде

После декомпиляции таблиц я начал трассировать метод _PTS (Prepare To Sleep). Он работает, как простой диспетчер, вызывающий последовательность других методов для подготовки различных аппаратных компонентов.

Большинство из них завели в тупик: локальный метод PTS был абсолютно пустым, а методы для северного моста (NPTS) и корневых портов (RPTS) просто выполняли стандартные процедуры сохранения состояния и отладки.

Интереснее была логика TPM, но он содержал лишь команды гибернации (S4) и выключения (S5), никак не касаясь сна S3. Виновников в этом коде не нашлось.

Главная проблема проявилась в южном мосте:

Method (SPTS, 1, NotSerialized)
{
    SLPX = One
    SLPE = One
    If ((Arg0 == 0x03))
    {
        AES3 = One
    }
}

Нет, не здесь. Покажу его в псевдокоде:

/*
================================================================================
 Southbridge_PrepareToSleep: забагованный метод
 
 Эта функция вызывается для того, чтобы передать последнюю команду "go to sleep" контроллеру основного питания
 материнской платы, находящемуся на южном мосте.
================================================================================
*/
void Southbridge_PrepareToSleep(int sleep_state) {
    // ОСНОВНАЯ ЛОГИЧЕСКАЯ ОШИБКА:
    // Эта функция должна выполнить по порядку два этапа:
    //   1. Присвоить значение регистру "sleep_type_register" оборудования, сообщающему,
    //      должен ли выполняться S3 (пауза/приостановка) или S5 (остановка/выключение).
    //   2. Присвоить значение "sleep_enable_bit", чтобы приказать оборудованию выполнять команду немедленно.
    //
    // Этот код полностью игнорирует Этап 1.

    // ----------------- РЕАЛЬНЫЙ ЗАБАГОВАННЫЙ КОД -----------------

    // Эта строка задаёт вспомогательный флаг. Это НЕ основная команда,
    // сообщающая оборудованию, в какое состояние сна переходить.
    SOUTHBRIDGE.some_sleep_flag = 1;         // В исходном ASL: SLPX = One

    // ЭТО ЭТАП 2 - КНОПКА "ВПЕРЁД".
    // Этот код немедленно запускает переход в сон, без необходимости предварительного
    // указания типа сна. Это причина бага.
    SOUTHBRIDGE.sleep_enable_bit = 1;        // В исходном ASL: SLPE = One
    
    // Этот блок 'if' - поломанная попытка прошивки обработать S3.
    // Он задаёт другой вспомогательный флаг, но ему всё равно не удаётся записать
    // основной sleep_type_register оборудования, поэтому оборудование не получает основную команду.
    if (sleep_state == S3_SUSPEND) {
        SOUTHBRIDGE.acpi_s3_enable_flag = 1; // В исходном ASL: AES3 = One
    }
}

Это единственный метод во всей последовательности, который выполняет безусловную запись в основной регистр запуска сна (SLPE). Все остальные методы отвечают за сохранение состояния или за обработку их собственного оборудования. Именно SPTS безрассудно жмёт кнопку «Вперёд» для всей системы, не задав предварительно значение «Вперёд куда?».

Объясню подробнее.

Присвоение регистру SLPE значения One буквально сообщает материнской плате: «Слушай, я уже разобрался с отдыхом, можешь выключать всё остальное».

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

Чтобы понять степень серьёзности этого бага, нужно разобраться, что же на самом деле выполняет SLPE = One. В южном мосте физически содержится специальный аппаратный блок, управляющий шинами питания материнской платы. Когда мы приказываем компьютеру перейти в сон S3, именно PMC южного моста отключает питание CPU, ОЗУ (частично), вентиляторов и других компонентов. Бит SLPE (Sleep Enable) — это непосредственная команда конкретно этому аппаратному блоку.

S3 (глубокий сон) и S5 (выключение)

Мы знаем, что метод-диспетчер _PTS выполняется следующим образом:

    Method (_PTS, 1, NotSerialized)  // _PTS: Prepare To Sleep
    {
        If (Arg0)
        {
            PTS (Arg0)
            \_SB.TPM.TPTS (Arg0)
            \_SB.PCI0.LPCB.SPTS (Arg0)
            \_SB.PCI0.NPTS (Arg0)
            RPTS (Arg0)
        }
    }

То есть поток выглядит так: PTS (полностью пустой метод) -> TPTS (TPM) -> SPTS (южный мост) -> NPTS (северный мост) -> RPTS (корневой порт).

Теперь взглянем на код SPTS.

                Method (SPTS, 1, NotSerialized)
                {
                    SLPX = One
                    SLPE = One
                    If ((Arg0 == 0x03))
                    {
                        AES3 = One
                    }
                }

В нём никак не учитывается S3. Условное ветвление выполняется после SLPE = One. Не имеет никакого смысла использовать это условие после присвоения.

После того, как понимаешь, что этот же код выполняется и для S5, возникает вопрос: как же тогда моему компьютеру удаётся правильно выключиться?

Рассмотрим метод TPTS:

        Method (TPTS, 1, Serialized)
        {
            Switch (ToInteger (Arg0))
            {
                Case (0x04)
                {
                    RQST = Zero
                    FLAG = 0x09
                    SRSP = Zero
                    SMI = OFST /* \OFST */
                    Return (SRSP) /* \_SB_.TPM_.SRSP */
                }
                Case (0x05)
                {
                    RQST = Zero
                    FLAG = 0x09
                    SRSP = Zero
                    SMI = OFST /* \OFST */
                    Return (SRSP) /* \_SB_.TPM_.SRSP */
                }

            }
        }

В TPTS те же конструкции написаны для состояний S4 (гибернация) и S5 (выключение). После сохранения ОЗУ на диск гибернация происходит отключением всей системы. TPTS спасает ситуацию.

Метод TPTS выполняется перед забагованным методом SPTS. Как можно увидеть из его кода, в TPTS есть отдельный Case для S5 (выключения), корректно подготавливающий оборудование.

TPTS не отвечает за сон S3 (да и не должен отвечать за него в этом случае).

Заключение

Моя удача зависела от мусора из регистра. Именно так происходило каждый раз, когда я закрывал крышку ноутбука... всё зависело от чтения мусорного значения.

Как бы я мог объяснить это себе тринадцатилетнему? Сколько часов я потратил на размышления об этой проблеме?..

Чёрт побери.


Понимаете ли вы, как я себя ощущаю?

Подходящий XKCD:

dependency.png

В мире ИИ-хайпа мы ЗАСЛУЖИВАЕМ, чтобы было больше технарей, декодирующих таблицы ACPI и позволяющих нам понять, стабильна ли система. Это единственное, чего требует мой лишённый иллюзий разум.

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


  1. Iselston
    18.09.2025 14:57

    С 2018 года использую Dell Inspiron 7577 под управлением Manjaro (но пробовал и Windows в течении полугода) и периодически тоже сталкиваюсь с проблемой, что при закрытии крышки ноут может ребутнуться вместо сна. Очень досадный баг, который не раз подпорчивал жизнь.

    Когда увидел превью статьи - была надежда, что автор опишет "волшебную пилюлю", так как сам по себе ноутбук хорош и до сих пор более чем перекрывает потребности.


  1. Javian
    18.09.2025 14:57

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

    P.S. на оффсайте есть Биос со статусом критический от 29 Jul 2021.